@afauthhq/server
v0.7.1
Published
AFAuth server SDK — Verifier, Server, NonceStore, AccountStore, RecipientHandler
Readme
@afauthhq/server
Service SDK for the AFAuth Protocol. Verifies signed requests per §5.5/§5.6, runs the owner-invitation and claim-completion ceremonies per §7, handles pre-claim key rotation per §8.1, and serves the discovery and account-introspection endpoints.
Quickstart
import {
consoleEmailHandler,
defineService,
MemoryAccountStore,
MemoryNonceStore,
} from "@afauthhq/server";
const server = defineService({
baseUrl: "https://api.example.com",
serviceDid: "did:web:api.example.com",
accounts: new MemoryAccountStore(),
recipients: { email: consoleEmailHandler },
nonceStore: new MemoryNonceStore(),
// attestation: "required" (default) wires trustAttestor() and sets
// discovery.billing.unclaimed_mode = "attested_only".
// Override with "optional" (migration path) or "off" (paid/read-only).
});defineService flips three protocol switches ON by default: it advertises
attested_only in the discovery doc (§9.2), configures trustAttestor()
as the verifier (§10), and enforces per-principal uniqueness (§10.4.4).
The net effect is spam-resistance out of the box — un-attested implicit
signups are rejected with 401 attestation_required, and a second account
for a human who already has one (same (iss, sub_h)) is rejected with
409 principal_already_registered. "Same human, same bucket" holds by
default. The default slot is a process-local MemorySubHUniquenessStore, so
supply a durable, atomic SubHUniquenessStore (D1SubHUniquenessStore) in
production — or pass subHUniqueness: false to allow many agents per human
(fleet operators).
For MultiAttestor setups, custom HmacAttestor, or fully custom
discovery, use new Server({...}) — see Advanced configuration.
Advanced configuration
import {
consoleEmailHandler,
MemoryAccountStore,
MemoryNonceStore,
MemoryRevocationList,
Server,
} from "@afauthhq/server";
const server = new Server({
nonceStore: new MemoryNonceStore(),
revocationList: new MemoryRevocationList(),
serviceDid: "did:web:api.example.com",
accounts: new MemoryAccountStore(),
recipients: { email: consoleEmailHandler },
discovery: { /* see @afauthhq/agent DiscoveryDocument */ },
baseUrl: "https://api.example.com",
// §7.2: redirect_url is rejected unless its host is in this list.
redirectAllowList: ["yourapp.com"],
// §6.3: implicit signup on first touch. Default true.
implicitSignup: true,
});Exports
defineService(opts)— opinionated convenience factory. Returns aServerwithattestation: "required"defaults (unclaimed_mode: "attested_only"+trustAttestor()). Passattestation: "optional"or"off"to opt out. Overridediscovery,attestor, etc. for partial customizations; drop tonew Server({...})for full control.Server— five endpoint handlers (handleDiscovery,handleOwnerInvitation,handleClaimCompletion,handleKeyRotation,handleAccountIntrospection) plusrevoke(did)for §8.4 owner-initiated revocation.Verifier— standalone request verifier (§5.5 + §5.6). Use directly as an edge plugin (Appendix E) or as the front half ofServer. Accepts an optionaldidResolver— default isdid:key-only.assertFreshOwnerSession(session, { maxAgeSeconds })— §7.5 freshness floor for post-claim owner-binding routes (revoke, rotate, change-credential, add-recovery-contact, ...). Throwsowner_session_too_stale(403) when the session'sauthenticatedAtis missing or older than the window. Call this from YOUR owner-binding routes; the SDK does not call it automatically (it is NOT enforced byhandleClaimCompletion).- Rate limiter (§11.3) —
RateLimiter,RateLimitConfig,RateLimitDecision,MemoryRateLimiter,ServerRateLimits. PassrateLimiter+rateLimitstoServerto gate per-route per-DID buckets; over-limit calls return429 rate_limit_exceededwithRetry-After. - Attestation (§10) —
Attestor,AttestationClaims,HmacAttestor(HS256 shared-secret),JwksAttestor(asymmetric via JWKS URL),MultiAttestor(dispatch byiss). PassattestortoServer; §9.2attested_onlyenforced automatically on the implicit-signup path. - Attested sessions (§10.7) —
server.verifyAttested(req)gates your own authenticated routes on a currently-valid attestation, refreshing the window when the agent re-presents one and challenging with401 attestation_requiredwhen it lapses. Configure viaattestedSession: { store, mode }(needs anattestor);AttestedSessionGateis the standalone primitive. Stores:AttestedFreshnessStore+MemoryAttestedFreshnessStorehere,KvAttestedFreshnessStorein@afauthhq/worker. - Per-principal uniqueness (§10.4.4) —
SubHUniquenessStore+MemorySubHUniquenessStoreenforce "at most one account per human": a signup whose verified(iss, sub_h)already holds an account is rejected with409 principal_already_registered. ON by default indefineServicerequiredmode. PasssubHUniquenessa durable, atomic store (D1SubHUniquenessStorein@afauthhq/worker) for production, orfalseto allow agent fleets. The slot follows key rotations (§8.1/§8.2) and is released on account expiry — pass the store tosweepExpiredAccounts(server.subHUniquenessStoreexposes the configured instance). - Stores —
NonceStore+MemoryNonceStore(lazy GC on every Nth insert),AccountStore+MemoryAccountStore(atomic invitation supersession with O(1) reverse index),RevocationList+MemoryRevocationList. Production-grade durable stores ship in@afauthhq/worker(D1AccountStore,D1SubHUniquenessStore,KvNonceStore,KvRevocationList,KvAttestedFreshnessStore,KvRateLimiter). - Recipient handlers —
RecipientHandler<R>interface; shipsconsoleEmailHandlerfor local development (logs the magic link toconsole.error).
See also
AFAuthHQ/spec— protocol spec.@afauthhq/worker— Cloudflare Workers bindings that route requests to this Server's handlers.@afauthhq/agent→TrustClient— the agent side of the default path: how an agent links to a human attrust.afauth.organd mints theAFAuth-AttestationJWT this Server'sattested_onlydefault requires.
