@afauthhq/worker
v0.5.3
Published
AFAuth Cloudflare Workers bindings — createWorker, DurableObjectNonceStore, KvNonceStore, D1AccountStore
Readme
@afauthhq/worker
Cloudflare Workers bindings for the AFAuth Protocol. Wraps
@afauthhq/server in a Worker-native router and provides storage
implementations backed by Durable Objects, KV, and D1.
Quickstart
import {
AFAuthNonceDO,
createNonceDurableObject,
createWorker,
DurableObjectNonceStore,
KvRevocationList,
} from "@afauthhq/worker";
import {
consoleEmailHandler,
MemoryAccountStore,
type DiscoveryDocument,
} from "@afauthhq/server";
// Re-export the nonce DO base class under whatever class_name your
// wrangler.toml binding declares (default: `AFAuthNonceDO`).
export class AFAuthNonceDO extends createNonceDurableObject() {}
interface Env {
AFAUTH_NONCE_DO: DurableObjectNamespace;
AFAUTH_REVOCATIONS: KVNamespace;
}
const discovery: DiscoveryDocument = { /* ... */ };
const accounts = new MemoryAccountStore(); // replace with durable impl
export default {
fetch(req, env: Env, ctx) {
const handler = createWorker({
nonceStore: new DurableObjectNonceStore(env.AFAUTH_NONCE_DO),
revocationList: new KvRevocationList(env.AFAUTH_REVOCATIONS),
serviceDid: discovery.service_did,
accounts,
recipients: { email: consoleEmailHandler },
discovery,
baseUrl: "https://api.example.com",
extractOwnerSession: async (req) => /* your session extraction */ null,
});
return handler.fetch!(req, env, ctx);
},
};wrangler.toml binding for the DO:
[[durable_objects.bindings]]
name = "AFAUTH_NONCE_DO"
class_name = "AFAuthNonceDO"
[[migrations]]
tag = "v1"
new_classes = ["AFAuthNonceDO"]Spam-resistant defaults
createWorker mirrors new Server({...}) rather than the higher-level
defineService factory — the Worker
needs explicit options for its DO bindings, KV namespaces, and
extractOwnerSession. To get the same spam-resistance, pass
trustAttestor() and declare attested_only on the discovery doc:
import { trustAttestor } from "@afauthhq/server";
const discovery: DiscoveryDocument = {
/* ... */
billing: {
unclaimed_mode: "attested_only",
accepted_attestors: ["afauth-trust"],
},
};
createWorker({
/* ... */
attestor: trustAttestor(),
discovery,
// §10.4.4 "same human, same bucket". Because `createWorker` mirrors
// `new Server` (not `defineService`), per-principal uniqueness is NOT
// defaulted — pass it explicitly. `D1SubHUniquenessStore` gives an
// atomic, durable claim via its UNIQUE index:
subHUniqueness: new D1SubHUniquenessStore(env.AFAUTH_DB),
});Agents that haven't run afauth trust link will be rejected with
401 attestation_required; the afauth signup
CLI guides them through the link flow on that error. A second agent for a
human who already has an account here is rejected with
409 principal_already_registered. To free a human's slot when their
unclaimed account expires, pass the same store to sweepExpiredAccounts
(subHUniqueness).
Nonce store: pick DO, not KV
§5.6 requires the seen-nonce set be shared and atomic across
verifier instances. Cloudflare KV is shared but offers no atomic
check-and-set: a get-then-put window admits cross-isolate
replay during the freshness window.
| Store | Atomic? | Shared? | When to use |
|---|---|---|---|
| DurableObjectNonceStore | yes | yes | recommended for production |
| KvNonceStore | no | yes | dev only, or single-region low-value deployments where the trade-off is documented |
| MemoryNonceStore (from @afauthhq/server) | yes | no | tests only |
DurableObjectNonceStore partitions by keyid so unrelated agents
fan out across distinct actor instances; only requests from the same
agent share an actor and serialize against each other.
Exports
createWorker(opts)— returns anExportedHandlerrouting the five AFAuth endpoints to@afauthhq/serverhandlers. Routing is done with a small in-house router (ADR-0002).createNonceDurableObject()— factory that returns a Durable-Object base class implementing the §5.6 atomic check-and-set protocol. Subclass it in your Worker module and register the subclass inwrangler.toml.DurableObjectNonceStore—NonceStorethat delegates to the DO above. Spec-compliant atomic insert; recommended for production.KvNonceStore—NonceStorebacked by Cloudflare KV. Has a known eventual-consistency replay window; see the JSDoc on the class. Suitable for dev/low-value deployments only.KvRevocationList—RevocationListbacked by Cloudflare KV (§8.3). Durable; no TTL.KvAttestedFreshnessStore—AttestedFreshnessStorebacked by Cloudflare KV (§10.7). Stores each account'sattestedUntil; the KV entry's TTL is set to the remaining window, so lapsed sessions self-evict. Pass toServerviaattestedSession: { store }.KvRateLimiter—RateLimiterbacked by Cloudflare KV (§11.3). Fixed-window counter per key; eventually-consistent reads mean racing isolates may over-count (fail-safe per §11.3), never under-count.D1AccountStore—AccountStorebacked by Cloudflare D1 (§6 + §7.3). Every ADR-0004 named atomic op usesD1.batch()for transactional grouping. The schema lives atmigrations/0001_init.sql; apply viawrangler d1 migrations apply <db-name>before first use. Schema is portable to standard Postgres/MySQL with minor syntactic changes.D1SubHUniquenessStore—SubHUniquenessStorebacked by Cloudflare D1 (§10.4.4), in the same database asD1AccountStore. Enforces "at most one account per human":claim()is atomic via the composite PRIMARY KEY(iss, sub_h)(INSERT … ON CONFLICT DO NOTHING), which aKvNonceStore-style get-then-put could not give — so there is no KV variant. Schema lives atmigrations/0002_subh_uniqueness.sql; apply after0001. Pass viasubHUniqueness(and tosweepExpiredAccountsto release slots on expiry).WorkerOptions— extendsServerOptionswith the requiredextractOwnerSessioncallback for the claim-completion route.
See also
AFAuthHQ/spec— protocol spec.@afauthhq/server— the handlerscreateWorkerdispatches to.examples/worker/— runnable reference Worker that prefers DO when its binding is configured and falls back to KV (with a warning) otherwise.
