feat(signing): default stores + upstream, add createAgentSignedFetch preset#917
Merged
feat(signing): default stores + upstream, add createAgentSignedFetch preset#917
Conversation
…preset Three ergonomic upgrades to the RFC 9421 signing surface. All three are backwards compatible — existing callers that pass explicit stores or an upstream fetch are unaffected. 1. `verifySignatureAsAuthenticator` defaults `replayStore` and `revocationStore` to fresh `InMemoryReplayStore` / `InMemoryRevocationStore` instances when omitted. The defaults are constructed once at authenticator wire-up so replay detection actually works across requests on a single instance. Wire explicit stores for multi-replica deployments where replay state must be shared. 2. `buildAgentSigningFetch` defaults `upstream` to `globalThis.fetch` when omitted. Throws a clear `TypeError` if `globalThis.fetch` isn't available at wire-up, rather than binding `undefined` and failing cryptically on first request. 3. `createAgentSignedFetch(options)` — new single-seller buyer preset. Bundles `buildAgentSigningFetch` with a `CapabilityCache` lookup keyed by the target seller's `agent_uri`, so adapter authors write one call instead of wiring the cache and capability accessor by hand. Multi-seller buyers still use `buildAgentSigningFetch` directly with a URL-dispatching `getCapability`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…h authenticator) Adds `replayStore` / `revocationStore` defaults on `createExpressVerifier` to match the defaults just landed on `verifySignatureAsAuthenticator`. Previously, the `serve()` path could omit both stores while the raw-Express path required them — asymmetric ergonomics for the same underlying verifier. Defaults are constructed once at middleware wire-up so replay detection works across requests; each middleware instance gets its own isolated defaults. Extends the interface by breaking the `extends Omit<VerifyRequestOptions, 'operation'>` chain and re-declaring `replayStore` / `revocationStore` as optional on `ExpressMiddlewareOptions`. Existing callers that pass explicit stores are unaffected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Flagged by code-quality bot on #917. Leftover from an earlier draft that generated keys inline before I switched to the shipped test vectors file. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
bokelley
added a commit
that referenced
this pull request
Apr 24, 2026
…edOrSigned Apply the review feedback on #914 against the SDK ergonomics shipped in #916 (mcpToolNameResolver) and #917 (defaults + createAgentSignedFetch preset): SIGNING-GUIDE.md - Step 3 (buyer-side): lead with createAgentSignedFetch one-call preset for the single-seller case; keep buildAgentSigningFetch as the multi-seller fallback. Drops the verbose CapabilityCache wiring from the headline example. - Step 4 (seller-side, Express middleware): drop explicit InMemoryReplayStore / InMemoryRevocationStore (now default per #917). Use mcpToolNameResolver instead of the inlined JSON-RPC parser. Add a one-liner about swapping in shared stores for horizontally scaled fleets. - Step 4 (composing with bearer auth): replace the three-variable manual composition (signatureAuth / bearerAuth / requireSignatureWhenPresent) with requireAuthenticatedOrSigned in one call. Now also enforces the spec's request_signature_required 401 via requiredFor: [...MUTATING_TASKS] — previously absent from the example. BUILD-AN-AGENT.md - Mirror the requireAuthenticatedOrSigned + mcpToolNameResolver + MUTATING_TASKS pattern in the Request Signing snippet so the two guides agree. Functional surface unchanged; the manual composition still works for callers that need finer control. The recommended path is just shorter.
This was referenced Apr 24, 2026
Merged
bokelley
added a commit
that referenced
this pull request
Apr 25, 2026
* docs: add request signing guide and surface it from README + BUILD-AN-AGENT Adds docs/guides/SIGNING-GUIDE.md covering the full RFC 9421 signing workflow: key generation, JWKS/brand.json publication, client-side signing, server-side verification, webhook signing, capability declaration, key rotation, and conformance testing. Updates README.md to link the guide from the AI Agents section and rewrites the Security > Request Signing section with better orientation. Adds a Request Signing section to BUILD-AN-AGENT.md showing requireSignatureWhenPresent composition and webhook signer config. * chore: add empty changeset for docs-only PR * docs(signing): collapse manual auth composition to requireAuthenticatedOrSigned Apply the review feedback on #914 against the SDK ergonomics shipped in #916 (mcpToolNameResolver) and #917 (defaults + createAgentSignedFetch preset): SIGNING-GUIDE.md - Step 3 (buyer-side): lead with createAgentSignedFetch one-call preset for the single-seller case; keep buildAgentSigningFetch as the multi-seller fallback. Drops the verbose CapabilityCache wiring from the headline example. - Step 4 (seller-side, Express middleware): drop explicit InMemoryReplayStore / InMemoryRevocationStore (now default per #917). Use mcpToolNameResolver instead of the inlined JSON-RPC parser. Add a one-liner about swapping in shared stores for horizontally scaled fleets. - Step 4 (composing with bearer auth): replace the three-variable manual composition (signatureAuth / bearerAuth / requireSignatureWhenPresent) with requireAuthenticatedOrSigned in one call. Now also enforces the spec's request_signature_required 401 via requiredFor: [...MUTATING_TASKS] — previously absent from the example. BUILD-AN-AGENT.md - Mirror the requireAuthenticatedOrSigned + mcpToolNameResolver + MUTATING_TASKS pattern in the Request Signing snippet so the two guides agree. Functional surface unchanged; the manual composition still works for callers that need finer control. The recommended path is just shorter. * fix(docs+signing): apply expert review on signing guide Six must-fix items raised by code-reviewer + ad-tech-protocol-expert on the post-polish state: 1. MUTATING_TASKS imports were wrong — only @adcp/client (root) exports it, not @adcp/client/server. Buyer copies would have compile-errored. Split the imports. 2. Capability override key is `request_signing`, not `signed_requests`. Wrong key gets silently dropped — verifier wires up but get_adcp_capabilities advertises nothing, so buyers don't sign. Fixed in both guides; add a clarifying sentence so the trap is visible. 3. Widen `mcpToolNameResolver` parameter type from `IncomingMessage & { rawBody?: string }` to `{ rawBody?: string }`. Function only reads rawBody, so widening lets it satisfy both the `verifySignatureAsAuthenticator` (IncomingMessage-shaped) and `createExpressVerifier` (ExpressLike-shaped) call sites without casts. No runtime change. 4. Error code table was wrong — `missing_signature` / `invalid_signature` etc. don't exist. The real codes (cross-checked against compliance/cache/3.0.0/test-vectors/ request-signing/negative/) are `request_signature_*`. Replace the seven-row table with the full 15-row table from the vectors and note that they're a separate signature-error namespace surfaced via WWW-Authenticate (not entries in enums/error-code.json). 5. Signature coverage misstated. content-digest is conditional on covers_content_digest, not always covered. Reword. 6. brand.json shape was wrong. agents[] requires {type, url, id}; `capabilities` and `adcp_use` arrays don't exist in schemas/cache/3.0.0/brand.json. Fix the example, list the `type` enum. Also reframed the "mandatory in 3.1+" claim to "4.0, spend-committing operations" per schemas/cache/3.0.0/protocol/get-adcp-capabilities-response.json, and softened `requiredFor: [...MUTATING_TASKS]` to a narrow example with MUTATING_TASKS as the upper bound (the spec stance in 3.0 is "empty by default; populate selectively per counterparty"). Conformance vector counts (12 positive + 27 negative = 39) verified against the disk layout. --------- Co-authored-by: Brian O'Kelley <bokelley@scope3.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Four ergonomic upgrades to the RFC 9421 signing surface. All backwards compatible — existing callers that pass explicit stores or an upstream fetch are unaffected. Lands on top of #916 (
mcpToolNameResolver).verifySignatureAsAuthenticator— defaultsreplayStoreandrevocationStoreto freshInMemoryReplayStore/InMemoryRevocationStoreinstances when omitted. The defaults are constructed once at authenticator wire-up so replay detection actually works across requests on a single instance. Wire explicit stores for multi-replica deployments where replay state must be shared.createExpressVerifier— gets the same defaults, symmetric withverifySignatureAsAuthenticator. Previously theserve()path could omit both stores while the raw-Express path required them — same underlying verifier, asymmetric ergonomics. Now both shapes behave identically on omission.buildAgentSigningFetch— defaultsupstreamtoglobalThis.fetchwhen omitted. Throws a clearTypeErrorifglobalThis.fetchisn't available at wire-up, rather than bindingundefinedand failing cryptically on first request.createAgentSignedFetch(options)— new single-seller buyer preset. BundlesbuildAgentSigningFetchwith aCapabilityCachelookup keyed by the target seller'sagent_uri, so adapter authors write one call instead of wiring the cache and capability accessor by hand:Multi-seller buyers still use
buildAgentSigningFetchdirectly with a URL-dispatchinggetCapability.Motivation
Follow-up from the PR #914 signing-guide review — ported to the spec repo at adcp#3064. Ben asked for "base tooling setup as defaults with override capability" plus an
init()entrypoint that configures a sharedsignedFetchimportable anywhere.Module-level
init()would regress host-aware multi-agentserve()(5.16) — one process running N agents with N signing keys can't use a singleton. This PR takes the defaults-with-override half directly and ships the "import once, use anywhere" ergonomic via a factory preset instead of a singleton. The usage pattern Ben wants works identically — module-levelexport const signedFetch = createAgentSignedFetch({...})— but stays multi-tenant-safe, testable, and consistent with every other SDK config surface (createAdcpServer,serve,createSigningFetch,createExpressVerifier).Once this merges, the adcp#3064 signing guide can drop the explicit
new InMemoryReplayStore()/new InMemoryRevocationStore()lines from its Step 4createExpressVerifierandrequireAuthenticatedOrSignedexamples, and the Step 3 capability-aware fetch example can collapse into a singlecreateAgentSignedFetch({ signing, sellerAgentUri })call.Test plan
test/lib/signing-defaults-and-preset.test.js— 12/12 passverifySignatureAsAuthenticatordefault stores (accepts valid signature, default replay store rejects replays, separate authenticator instances get isolated stores)createExpressVerifierdefault stores (accepts valid signature, default replay store rejects replays with 401WWW-Authenticate: Signature, separate middleware instances get isolated stores)buildAgentSigningFetchdefault upstream (falls back toglobalThis.fetch, throwsTypeErrorwhenglobalThis.fetchis unavailable)createAgentSignedFetchpreset (returns FetchLike, cold-cache no-sign, required_for sign, default cache wiring)auth-signature-compose.test.js+webhook-signing-vectors.test.js+storyboard-webhook-signature.test.js+request-signing-runner-integration.test.js+request-signing-e2e.test.jsstill green (84/84)npm run buildcleantsc --noEmitclean (pre-push)🤖 Generated with Claude Code