feat(server): BuyerAgentRegistry Phase 1 Stage 1 — types + factories (#1269)#1293
Conversation
…nctions (#1269) Adds the durable buyer-agent identity surface: `BuyerAgent` record, kind- discriminated `AdcpCredential`, and `BuyerAgentRegistry` with three factory functions (`signingOnly`, `bearerOnly`, `mixed`) that encode the implementer posture at construction. The credential answers "who signed?" / "who holds this token?"; the `BuyerAgent` answers "who is this counterparty in our books?" — a durable commercial relationship distinct from the per-request credential. Same pattern as an SSP's `buyer_id` row keyed to a DSP regardless of credential type. This stage ships the types and factories only. Stage 2 wires the resolve seam in the dispatcher; Stage 3 ships the `ResolvedAuthInfo` migration shim; Stage 4 adds status enforcement / multi-credential conflict / log redaction; Stage 5 adds the caching decorator and conformance fixtures. All Phase 1 stages are no-wire-emission and ship in 3.0.x. Phase 2 (#1292) — framework-level `billing_capability` enforcement and the AdCP-3.1 error-code emission from adcontextprotocol/adcp#3831 — is gated on the SDK's 3.1 cutover. 19 unit tests cover factory contracts (kind-routing, null on unhonored kinds, construction-time validation, resolver-throw propagation), `BuyerAgent` shape semantics, and `BillingMode` Set-membership. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…_sig credentials Three fixes from expert review on PR #1293 (no blockers, all in-scope for Stage 1): 1. Doc-clarify that the legacy `ResolvedAuthInfo` synthesis is Stage 3's job, not Stage 1's. The previous comment promised behavior the code didn't implement. 2. `ResolveBuyerAgentByCredential` JSDoc now requires implementations to switch on `credential.kind` and reject unrecognized kinds. Closes the foot-gun where a `WHERE token = $1` lookup against an api-key table could mis-resolve when handed an `http_sig` credential whose `keyid` collides with an existing api-key value — the credential variants share no key namespace. 3. Runtime guard against malformed `http_sig` credentials. A misbehaving authenticator could produce `kind: 'http_sig'` without a non-empty `agent_url`; without the guard, `signingOnly` and `mixed` would silently pass `undefined` to the resolver. `signingOnly` now returns null on malformed; `mixed` rejects rather than falling through to the bearer path (which would make `mixed` strictly weaker than `signingOnly` — a malformed http_sig could otherwise bypass signed- path enforcement by routing through the bearer table). Test coverage extended to 22 cases (+3): malformed-http_sig refusal in both `signingOnly` and `mixed`; empty-agent_url refusal. Deferred items (Stages 2, 3, Phase 2) recorded on #1269 / #1292. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Review fixes pushed (b7ac472)Three-expert review (ad-tech-protocol / code-reviewer / security-reviewer): no blockers. Three small fixes addressed in this commit; four deferred items recorded for later stages. Fixed in this PR
Deferred to later stages (recorded)
Cross-linked in #1269 and #1292. Validation
Confirmed non-issues from review
🤖 Generated with Claude Code |
Summary
Phase 1 Stage 1 of #1269 — durable buyer-agent identity surface, types and factory functions only. Pure additive, no runtime wiring; later stages thread the registry through the dispatcher.
What's in
BuyerAgentinterface withreadonlyfields, set-valuedbilling_capabilities, optionaldefault_account_terms,allowed_brands,aliases(rotation reservation).AdcpCredentialdiscriminated union —kind: 'api_key' | 'oauth' | 'http_sig'. Thehttp_sigvariant carries the cryptographically-verifiedagent_url(per spec(accounts): buyer-agent identity model + billing error-code coverage for sync_accounts adcp#3831's derivation rule); the bearer variants don't.BuyerAgentStatus('active' | 'suspended' | 'blocked') andBuyerAgentBillingMode(re-export of wireBillingParty).BuyerAgentRegistryProtocol + three factory functions:signingOnly({ resolveByAgentUrl })— bearer kinds resolve tonull; onlyhttp_sigroutes to the resolver.bearerOnly({ resolveByCredential })— all credential kinds route through the adopter's mapping.mixed({ resolveByAgentUrl, resolveByCredential })—http_sigroutes to the signed path, bearer kinds to the credential path; signed preferred when both present.BuyerAgentResolveInput— minimal input shape forresolve(), decoupled fromResolvedAuthInfoto keep the registry surface stable across the migration cycle.What's NOT in (later Stage)
ResolvedAuthInfo.credentialfield with two-minor deprecation → Stage 3.suspended/blocked→ 403), multi-credential conflict resolution, credential redaction → Stage 4.CachingBuyerAgentRegistrydecorator + conformance fixtures → Stage 5.billing_capabilityenforcement + AdCP-3.1 error-code emission → Phase 2 (feat(server): BuyerAgentRegistry Phase 2 — wire billing-capability enforcement (AdCP 3.1) #1292), gated on SDK 3.1 cutover.Why types-first
Three reasons to land Stage 1 alone:
v3-identity-bundle-design.mdround-2 RFC hasBuyerAgent/BuyerAgentRegistryas its spine — cross-language shape parity benefits from the JS types being concrete and merged.Test plan
npm run typecheckclean.test/lib/buyer-agent-registry.test.js— 19/19 pass:signingOnly: routeshttp_sig, returnsnullforapi_key/oauth, returnsnullfor absent credential, throws on missing resolver, propagates resolver throws.bearerOnly: routes all kinds (includinghttp_sig), returnsnullfor absent credential, throws on missing resolver.mixed: signed path takes precedence; bearer falls through; null on absent.BuyerAgentshape:readonlysemantics survive serialization; Set-membership works.npm run format:checkclean.server-decisioning-from-platform,server-decisioning-to-wire-account, new file): 150/150 pass.Cross-links
docs/proposals/v3-identity-bundle-design.md(cross-language shape parity)🤖 Generated with Claude Code