Skip to content

feat(signing): default stores + upstream, add createAgentSignedFetch preset#917

Merged
bokelley merged 4 commits intomainfrom
bokelley/signing-defaults
Apr 24, 2026
Merged

feat(signing): default stores + upstream, add createAgentSignedFetch preset#917
bokelley merged 4 commits intomainfrom
bokelley/signing-defaults

Conversation

@bokelley
Copy link
Copy Markdown
Contributor

@bokelley bokelley commented Apr 24, 2026

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 — 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.

  • createExpressVerifier — gets the same defaults, symmetric with verifySignatureAsAuthenticator. Previously the serve() 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 — 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.

  • 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:

    // fetch.ts
    import { createAgentSignedFetch } from '@adcp/client/signing';
    
    export const signedFetch = createAgentSignedFetch({
      signing: {
        kid: 'my-agent-2026',
        alg: 'ed25519',
        private_key: JSON.parse(process.env.ADCP_PRIV_KEY!),
        agent_url: 'https://agent.example.com',
      },
      sellerAgentUri: 'https://seller.example.com',
    });

    Multi-seller buyers still use buildAgentSigningFetch directly with a URL-dispatching getCapability.

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 shared signedFetch importable anywhere.

Module-level init() would regress host-aware multi-agent serve() (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-level export 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 4 createExpressVerifier and requireAuthenticatedOrSigned examples, and the Step 3 capability-aware fetch example can collapse into a single createAgentSignedFetch({ signing, sellerAgentUri }) call.

Test plan

  • New tests: test/lib/signing-defaults-and-preset.test.js — 12/12 pass
    • 3 for verifySignatureAsAuthenticator default stores (accepts valid signature, default replay store rejects replays, separate authenticator instances get isolated stores)
    • 3 for createExpressVerifier default stores (accepts valid signature, default replay store rejects replays with 401 WWW-Authenticate: Signature, separate middleware instances get isolated stores)
    • 2 for buildAgentSigningFetch default upstream (falls back to globalThis.fetch, throws TypeError when globalThis.fetch is unavailable)
    • 4 for createAgentSignedFetch preset (returns FetchLike, cold-cache no-sign, required_for sign, default cache wiring)
  • Regression: existing 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.js still green (84/84)
  • npm run build clean
  • tsc --noEmit clean (pre-push)

🤖 Generated with Claude Code

…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>
Comment thread test/lib/signing-defaults-and-preset.test.js Fixed
…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>
Comment thread test/lib/signing-defaults-and-preset.test.js Fixed
bokelley and others added 2 commits April 24, 2026 17:53
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 bokelley merged commit 7227b96 into main Apr 24, 2026
9 checks passed
@bokelley bokelley deleted the bokelley/signing-defaults branch April 24, 2026 21:59
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.
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant