npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

solana-agent-auth

v0.1.0

Published

Signed HTTP requests with Solana — the Solana equivalent of ERC-8128

Readme

solana-agent-auth

Signed HTTP requests with Solana. The Solana equivalent of ERC-8128.

Every HTTP request carries a cryptographic proof that it came from a specific Solana keypair. The server verifies the proof without needing sessions, tokens, or shared secrets.

Built on RFC 9421 (HTTP Message Signatures) with native Ed25519 support.

Install

npm install solana-agent-auth

Quick Start

Client (sign requests)

import { createSignerClient } from 'solana-agent-auth'

const signer = {
  publicKey: wallet.publicKey,  // Solana Address (base58)
  signMessage: (msg) => wallet.signMessage(msg),  // returns 64-byte Uint8Array
}

const client = createSignerClient(signer)

// Sign and send in one call
const response = await client.fetch('https://api.example.com/orders', {
  method: 'POST',
  body: JSON.stringify({ side: 'buy', amount: 1.5 }),
})

// Or sign without sending
const signedRequest = await client.signRequest('https://api.example.com/data')

Server (verify requests)

import { createVerifierClient } from 'solana-agent-auth'

const verifier = createVerifierClient({ nonceStore })

const result = await verifier.verifyRequest({ request })

if (result.ok) {
  console.log(`Authenticated: ${result.publicKey}`)
  console.log(`Binding: ${result.binding}`)       // "request-bound" | "class-bound"
  console.log(`Replayable: ${result.replayable}`)  // false (nonce present) | true
} else {
  console.log(`Rejected: ${result.reason}`)        // e.g. "expired", "replay", "bad_signature_check"
}

Verification is pure Ed25519 math -- no RPC calls, no on-chain lookups, no network requests. The built-in verifier is used automatically. No external crypto library needed.

Why

Traditional HTTP authentication is credential-based. JWTs, API keys, and session cookies can all be reused if compromised, require issuance handshakes, and don't bind to specific requests. For AI agents and programmatic clients, this is especially painful -- an agent needs to authenticate to an API it has never interacted with before, prove it controls a specific wallet, and do this without a human in the loop.

solana-agent-auth replaces bearer credentials with per-request Ed25519 signatures. The client signs each HTTP request with its Solana key. The server verifies the signature. No registration, no shared secrets -- if you have a Solana keypair, you can authenticate.

How It Works

What Gets Signed

The signature covers a canonical representation of the HTTP request following RFC 9421:

"@authority": api.example.com
"@method": POST
"@path": /orders
"@query": ?market=SOL-USD
"content-digest": sha-256=:X48E9qOokqqrvdts8nOJRJN3OWDUoyWxBf7kbu9DBPE=:
"@signature-params": ("@authority" "@method" "@path" "@query" \
  "content-digest");created=1772587263;expires=1772587323;\
  nonce="cedf9c3d7a664e0b";keyid="solana:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU"

This UTF-8 byte string is signed directly with Ed25519. No wrapper, no envelope -- exactly what RFC 9421 specifies.

The Headers

A signed request carries additional headers:

| Header | Purpose | |--------|---------| | Signature-Input | Declares what was signed and the metadata (timestamps, nonce, signer identity) | | Signature | The 64-byte Ed25519 signature, base64-encoded | | Content-Digest | SHA-256 hash of the request body (auto-computed when body is present) |

KeyId Format

The keyid parameter identifies who signed the request:

solana:<base58-encoded-public-key>

Example: solana:7xKXtg2CW87d97TXJSDpbD5jBkheTqA83TZRuJosgAsU

Security Model

Two orthogonal axes controlled by the signer, evaluated by the verifier:

Request Binding

| Mode | What's Signed | Use Case | |------|--------------|----------| | Request-bound (default) | @authority + @method + @path + @query + content-digest | Authorizes exactly one HTTP request | | Class-bound | Only specified components (minimum: @authority) | Authorizes a class of requests (like a scoped token) |

Replay Protection

| Mode | Mechanism | Use Case | |------|-----------|----------| | Non-replayable (default) | Nonce included, server tracks consumed nonces | Single-use authorization | | Replayable | No nonce, time-window only | Idempotent reads, high-frequency polling |

The strongest posture (request-bound + non-replayable) is the default and is the baseline that every compliant verifier MUST accept.

Conformance

Following the ERC-8128 spec:

  • Baseline (MUST accept): Request-Bound + Non-Replayable signatures
  • Optional (MAY accept): Replayable or Class-Bound signatures
  • Verifiers that accept Replayable signatures MUST implement early invalidation mechanisms (replayableNotBefore and/or replayableInvalidated)
  • All signatures MUST include keyid, created, expires, and cover at least @authority

API

Signing

signRequest(input, signer, opts?)

Sign a request and return a new Request with Signature-Input, Signature, and Content-Digest headers.

import { signRequest } from 'solana-agent-auth'

const signed = await signRequest('https://api.example.com/data', signer, {
  label: 'sol',           // default
  binding: 'request-bound', // default
  replay: 'non-replayable', // default
  ttlSeconds: 60,           // default
})

// Also supports (input, init, signer, opts) overload:
const signed = await signRequest(
  'https://api.example.com/tx',
  { method: 'POST', body: '{"amount":100}' },
  signer
)

signedFetch(input, signer, opts?)

Sign and send a request in one call.

import { signedFetch } from 'solana-agent-auth'

const response = await signedFetch('https://api.example.com/data', signer, {
  fetch: customFetchImpl,  // optional, defaults to globalThis.fetch
})

createSignerClient(signer, defaults?)

Create a client with a bound signer and default options.

import { createSignerClient } from 'solana-agent-auth'

const client = createSignerClient(signer, {
  ttlSeconds: 120,
  fetch: customFetch,
})

// client.signRequest(input, opts?) -> Promise<Request>
// client.signedFetch(input, opts?) -> Promise<Response>
// client.fetch(input, opts?)       -> Promise<Response> (alias for signedFetch)

// All support (input, init, opts?) overload for RequestInit:
await client.fetch('https://api.example.com/tx', { method: 'POST', body: '...' })

Verification

verifyRequest(args)

Verify a signed request. Uses the built-in Ed25519 verifier by default.

import { verifyRequest } from 'solana-agent-auth'

const result = await verifyRequest({
  request,
  nonceStore,
  // verifyMessage is optional -- defaults to built-in Ed25519 verifier
  policy: {
    clockSkewSec: 5,
    maxValiditySec: 300,
  },
})

if (result.ok) {
  result.publicKey   // Address (base58)
  result.label       // "sol"
  result.components  // ["@authority", "@method", "@path", ...]
  result.params      // { created, expires, keyid, nonce? }
  result.replayable  // boolean
  result.binding     // "request-bound" | "class-bound"
}

createVerifierClient(args)

Create a verifier with bound defaults. Minimal setup -- only a NonceStore is required.

import { createVerifierClient } from 'solana-agent-auth'

const verifier = createVerifierClient({
  nonceStore,
  // verifyMessage is optional -- built-in Ed25519 verifier used by default
  defaults: {
    clockSkewSec: 5,
    maxValiditySec: 300,
  },
})

const result = await verifier.verifyRequest({ request })

Types

interface SolanaHttpSigner {
  publicKey: Address           // @solana/kit Address type (base58 string)
  signMessage: (message: Uint8Array) => Promise<Uint8Array>  // 64-byte Ed25519 sig
}

interface NonceStore {
  consume(key: string, ttlSeconds: number): Promise<boolean>
  // Returns true if newly stored (nonce not seen before)
  // Returns false if already consumed (replay attempt)
}

type VerifyResult =
  | { ok: true; publicKey: Address; label: string; components: string[];
      params: SignatureParams; replayable: boolean; binding: BindingMode }
  | { ok: false; reason: VerifyFailReason; detail?: string }

Sign Options

type SignOptions = {
  label?: string              // default: "sol"
  binding?: BindingMode       // "request-bound" | "class-bound"
  replay?: ReplayMode         // "non-replayable" | "replayable"
  created?: number            // unix seconds; default: now
  expires?: number            // unix seconds; default: created + ttlSeconds
  ttlSeconds?: number         // default: 60
  nonce?: string | (() => Promise<string>)
  contentDigest?: ContentDigestMode  // "auto" | "recompute" | "require" | "off"
  components?: string[]       // extra components to sign
}

Verify Policy

type VerifyPolicy = {
  label?: string                     // preferred label; default: "sol"
  strictLabel?: boolean              // reject if label not found; default: false
  additionalRequestBoundComponents?: string[]
  classBoundPolicies?: string[] | string[][]
  replayable?: boolean               // accept replayable sigs; default: false
  replayableNotBefore?: (keyid: string) => number | null | Promise<number | null>
  replayableInvalidated?: (args: { keyid, created, expires, label, signature, signatureBase, signatureParamsValue }) => boolean | Promise<boolean>
  maxSignatureVerifications?: number  // default: 3
  now?: () => number                 // unix seconds
  clockSkewSec?: number              // default: 0
  maxValiditySec?: number            // default: 300
  maxNonceWindowSec?: number
  nonceKey?: (keyid: string, nonce: string) => string  // default: `${keyid}:${nonce}`
}

Verify Failure Reasons

| Reason | Meaning | |--------|---------| | missing_headers | Signature-Input or Signature header not present | | label_not_found | No signature with the expected label | | bad_signature_input | Failed to parse Signature-Input header | | bad_signature | Signature did not verify | | bad_signature_bytes | Could not decode signature from base64 | | bad_signature_check | Cryptographic verification threw or returned false | | bad_keyid | keyid is not a valid solana:<address> | | bad_time | created/expires are not valid integers or expires <= created | | not_yet_valid | Current time is before created | | expired | Current time is after expires | | validity_too_long | expires - created exceeds maxValiditySec | | nonce_required | Nonce present but nonceStore missing | | nonce_window_too_long | expires - created exceeds maxNonceWindowSec | | replay | Nonce already consumed | | replayable_not_allowed | Replayable sig but policy.replayable is false | | replayable_invalidation_required | Replayable sig but no invalidation mechanism configured | | replayable_not_before | created is before the replayableNotBefore cutoff | | replayable_invalidated | replayableInvalidated returned true | | not_request_bound | Signature doesn't cover required request-bound components | | class_bound_not_allowed | Class-bound sig but no classBoundPolicies configured | | digest_required | content-digest in components but header missing | | digest_mismatch | Content-Digest header doesn't match recomputed body hash |

NonceStore Implementation

The NonceStore interface has one method. Here's a Redis example:

import { createClient } from 'redis'

const redis = createClient()

const nonceStore: NonceStore = {
  async consume(key: string, ttlSeconds: number): Promise<boolean> {
    // SET NX with TTL -- atomic check-and-insert
    const result = await redis.set(`nonce:${key}`, '1', { EX: ttlSeconds, NX: true })
    return result === 'OK'
  },
}

In-memory (for development/testing):

const nonceStore: NonceStore = {
  seen: new Map<string, number>(),
  async consume(key: string, ttlSeconds: number): Promise<boolean> {
    const now = Date.now()
    // Clean expired entries
    for (const [k, exp] of this.seen) {
      if (exp < now) this.seen.delete(k)
    }
    if (this.seen.has(key)) return false
    this.seen.set(key, now + ttlSeconds * 1000)
    return true
  },
}

Comparison with ERC-8128

This library is a direct port of @slicekit/erc8128, adapted for Solana's Ed25519 identity model.

| Concern | ERC-8128 (Ethereum) | solana-agent-auth (Solana) | |---------|-------------------|--------------------------| | Algorithm | secp256k1 | Ed25519 | | Message wrapping | ERC-191 envelope required | Raw signing (RFC 9421 native) | | Account types | EOA + Smart Contract (two verification paths) | All Ed25519 (single code path) | | Verification | ecrecover or ERC-1271 RPC call | Pure Ed25519 verify -- no network needed | | verifyMessage | Required (bring your own viem + RPC) | Optional (ships built-in verifier) | | Key to address | keccak256(publicKey)[12:] (lossy) | Public key is the address | | keyid format | eip8128:<chainId>:<0xAddress> | solana:<base58Pubkey> | | Default label | eth | sol | | Signature return type | Hex (0x...) | Uint8Array (64 bytes) | | Deterministic | No (ECDSA k-value) | Yes (Ed25519) | | Signature size | 65 bytes | 64 bytes | | Success result | { address, chainId } | { publicKey } |

Spec Compliance

The implementation is 34/35 compliant with the ERC-8128 specification, with Ethereum-specific parts (ERC-191, secp256k1, chain-id, ERC-1271) replaced by Solana equivalents (raw Ed25519, base58 addresses). The single partial compliance (§5.2: invalidation requests must be authenticated with request-bound signatures) is an application-layer concern outside the scope of a signing library.

Architecture

src/
├── sign.ts                    # signRequest(), signedFetch()
├── verify.ts                  # verifyRequest()
├── client.ts                  # createSignerClient()
├── verifierClient.ts          # createVerifierClient()
├── index.ts                   # barrel exports
└── lib/
    ├── types.ts               # all types + SolanaAuthError
    ├── keyId.ts               # formatKeyId(), parseKeyId()
    ├── nonce.ts               # resolveNonce()
    ├── utilities.ts           # base64, sha256, toRequest, etc.
    ├── defaultVerify.ts       # built-in Ed25519 verifier
    ├── verifyUtils.ts         # buildAttempts, time/nonce checks
    ├── acceptSignature.ts     # Accept-Signature response header
    ├── engine/
    │   ├── serializations.ts       # RFC 8941 structured field serialization
    │   ├── createSignatureBase.ts  # RFC 9421 signature base construction
    │   ├── createSignatureInput.ts # Signature-Input / Signature parsing
    │   ├── contentDigest.ts        # Content-Digest (RFC 9530)
    │   └── signatureHeaders.ts     # signature candidate selection
    └── policies/
        ├── isRequestBound.ts       # request-bound classification
        └── normalizePolicies.ts    # policy normalization

Standards

  • RFC 9421 -- HTTP Message Signatures
  • RFC 9530 -- Digest Fields (Content-Digest)
  • RFC 8941 -- Structured Field Values
  • ERC-8128 -- Signed HTTP Requests with Ethereum (the Ethereum equivalent)

Development

bun install
bun test       # 225 tests
bun run build

License

MIT