@slicekit/erc8128
v0.3.3
Published
Minimal ERC-8128 implementation for signing and verifying HTTP requests with Ethereum accounts
Readme
@slicekit/erc8128
Sign and verify HTTP requests with Ethereum wallets using ERC-8128.
Features
- Fetch-native — Works in browsers, workers, Node.js 18+, Bun, Deno
- RFC 9421 compliant — HTTP Message Signatures with Ethereum extension
- Request binding — Sign URL, method, headers, and body
- Replay protection — Non-replayable by default (nonce required); nonce omission is only for explicitly allowed replayable signatures
Installation
npm install @slicekit/erc8128Quick Start
Sign a request
import { createSignerClient } from '@slicekit/erc8128'
import { privateKeyToAccount } from 'viem/accounts'
const account = privateKeyToAccount('0x...')
const signer = {
chainId: 1,
address: account.address,
signMessage: (message) => account.signMessage({ message: { raw: message } }),
}
const client = createSignerClient(signer)
const response = await client.fetch('https://api.example.com/orders', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ amount: '100' }),
})Verify a request
import { createVerifierClient } from '@slicekit/erc8128'
import { createPublicClient, http } from 'viem'
import { mainnet } from 'viem/chains'
const publicClient = createPublicClient({ chain: mainnet, transport: http() })
const verifier = createVerifierClient({
verifyMessage: publicClient.verifyMessage,
nonceStore
})
const result = await verifier.verifyRequest({ request })
if (result.ok) {
console.log(`Authenticated: ${result.address}`)
}Core API
createSignerClient(signer, options?)
Creates a client with a configured signer.
const client = createSignerClient(signer)
client.fetch(input, init?) // Sign and send
client.signRequest(input, init?) // Sign onlycreateVerifierClient({ verifyMessage, nonceStore, defaults? })
Creates a client with verification dependencies.
const verifier = createVerifierClient({
verifyMessage,
nonceStore
})
verifier.verifyRequest({
request,
policy, // optional
setHeaders // optional
})verifyRequest({ request, verifyMessage, nonceStore, policy?, setHeaders? })
Verifies a signed request.
type VerifyResult =
| {
ok: true
address: Address
chainId: number
label: string
components: string[]
params: SignatureParams
replayable: boolean
binding: "request-bound" | "class-bound"
}
| { ok: false; reason: VerifyFailReason }formatDiscoveryDocument(config)
Formats the /.well-known/erc8128 discovery document for your server.
import { formatDiscoveryDocument } from '@slicekit/erc8128'
const doc = formatDiscoveryDocument({
verificationEndpoint: 'https://api.example.com/erc8128/verify',
invalidationEndpoint: 'https://api.example.com/erc8128/invalidate',
maxValiditySec: 300,
routePolicy: {
'/api/public/*': { replayable: true },
'/api/orders/*': [
{
methods: ['POST', 'PUT'],
additionalRequestBoundComponents: ['content-type'],
},
{
methods: ['GET'],
classBoundPolicies: [['@authority', '@path']],
},
],
default: { replayable: false },
},
})
// {
// verification_endpoint: "https://api.example.com/erc8128/verify",
// invalidation_endpoint: "https://api.example.com/erc8128/invalidate",
// max_validity_sec: 300,
// route_policies: { ... }
// }invalidation_endpoint is only included when at least one policy enables replayable. Route policy entries set to false are filtered out.
signRequest(input, init?, signer, options?)
Signs a fetch Request and returns a new Request with signature headers.
signedFetch(input, init?, signer, options?)
Signs and sends a request in one call.
Options
Sign options
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| binding | "request-bound" | "class-bound" | "request-bound" | Components to sign |
| replay | "non-replayable" | "replayable" | "non-replayable" | Non-replayable includes nonce; replayable omits nonce |
| ttlSeconds | number | 60 | Signature validity window |
| label | string | "eth" | Signature label |
| components | string[] | — | Override signed components |
Verify policy
| Option | Type | Default | Description |
|--------|------|---------|-------------|
| maxValiditySec | number | 300 | Max allowed validity window |
| clockSkewSec | number | 0 | Allowed clock drift |
| label | string | — | Preferred signature label |
| strictLabel | boolean | false | Require exact label match |
| replayable | boolean | false | Allow replayable (nonce-less) signatures |
| additionalRequestBoundComponents | string[] | — | Extra components required for request-bound |
| classBoundPolicies | string[] \| string[][] | — | undefined disables class-bound, [] means authority-only, other entries require @authority plus those components |
Nonce store
To enable replay protection, implement NonceStore:
interface NonceStore {
consume(key: string, ttlSeconds: number): Promise<boolean>
}Documentation
Full documentation: erc8128.slice.so
License
MIT
