Run an AFAuth service on Cloudflare Workers with @afauthhq/worker.
@afauthhq/worker wires @afauthhq/server into a deployable Cloudflare Worker. createWorker routes the five AFAuth endpoints (discovery, owner-invitation, claim-completion, key-rotation, account-introspection) to the matching handlers, and the package ships Cloudflare-native durable stores so nothing critical lives in process memory.
createWorker(opts) takes everything Server does, plus an extractOwnerSession — the one place the Worker needs your auth, because claim completion and the owner-gated key endpoints depend on a human-authenticated session, not the agent signature. It returns a standard ExportedHandler.
import { consoleEmailHandler, MemoryAccountStore, MemoryNonceStore, MemoryRevocationList, type OwnerSession,} from "@afauthhq/server";import { createNonceDurableObject, createWorker, D1AccountStore, DurableObjectNonceStore, KvRevocationList,} from "@afauthhq/worker";// Export the §5.6 nonce Durable Object so wrangler can register it.export class AFAuthNonceDO extends createNonceDurableObject() {}interface Env { AFAUTH_NONCE_DO?: DurableObjectNamespace; // preferred: atomic nonce store AFAUTH_ACCOUNTS?: D1Database; // durable, multi-agent accounts AFAUTH_REVOCATIONS?: KVNamespace; SERVICE_DID?: string; BASE_URL?: string;}const worker: ExportedHandler<Env> = { fetch(req, env, ctx) { const serviceDid = env.SERVICE_DID ?? "did:web:example.com"; const handler = createWorker({ serviceDid, baseUrl: env.BASE_URL ?? "https://example.com", // Prefer durable; fall back to memory for local dev only. nonceStore: env.AFAUTH_NONCE_DO ? new DurableObjectNonceStore(env.AFAUTH_NONCE_DO) : new MemoryNonceStore(), accounts: env.AFAUTH_ACCOUNTS ? new D1AccountStore(env.AFAUTH_ACCOUNTS) : new MemoryAccountStore(), revocationList: env.AFAUTH_REVOCATIONS ? new KvRevocationList(env.AFAUTH_REVOCATIONS) : new MemoryRevocationList(), recipients: { email: consoleEmailHandler }, discovery: { /* afauth_version, service_did, endpoints, ... */ }, // Return null to reject claim / owner-key ops with 401. Replace // with real session verification (signed cookie, IdP JWT). extractOwnerSession: async (_req): Promise<OwnerSession | null> => null, }); return handler.fetch!(req, env, ctx); },};export default worker;
Memory stores lose every account, nonce, and revocation on isolate recycle — fine for a demo, never for production. Each has a Cloudflare-native backing:
Store
Memory (dev)
Durable (production)
Notes
Nonce (§5.6)
MemoryNonceStore
DurableObjectNonceStore, then KvNonceStore
DO gives atomic check-and-set; KV is eventually consistent
Accounts (§6)
MemoryAccountStore
D1AccountStore
groups a human’s agents by (iss, sub_h)
Revocation (§8.3)
MemoryRevocationList
KvRevocationList
durable; survives recycles
Attested session (§10.7)
MemoryAttestedFreshnessStore
KvAttestedFreshnessStore
self-evicts on window lapse
The nonce store is the one to get right. §5.6 requires atomic, cross-instance check-and-set. DurableObjectNonceStore provides it; KvNonceStore does a get-then-put that two racing edge isolates can both win, admitting a narrow replay within the signature’s expires window. Use the Durable Object for anything with value behind the signature; reserve KvNonceStore for low-value or single-region deployments.
0001_init.sql creates the accounts / agents / invitations tables with the (iss, sub_h) UNIQUE index that makes multi-agent grouping atomic across isolates.
3
Deploy and verify
wrangler deploycurl https://api.example.com/.well-known/afauth # should return your discovery docwrangler tail # watch live logs — magic links, warnings
The reference Worker has an AFAUTH_DEV_TRUST_HEADER escape hatch that trusts an X-Owner-Session header for local testing. It’s trivially forgeable — never set it in production. Your real extractOwnerSession verifies an authenticated session from your claim page (signed cookie, IdP-issued JWT) and returns an OwnerSession, or null to fail closed with 401 owner_authentication_required.