Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
1fed2b3
feat(identity): UCP profile signing helpers (jose peer dep)
vvillait88 May 8, 2026
7748db3
chore(deps): bump deps + override fast-uri to fix GHSA-q3j6-qgpj-74h6
vvillait88 May 8, 2026
a58fd3a
hardening(identity): UCP signing security + ergonomics fixes
vvillait88 May 8, 2026
f1fa39b
hardening: round-2 reviewer findings (security + test gaps)
vvillait88 May 8, 2026
c9634a2
hardening: round-3 reviewer findings (parity + ergonomics)
vvillait88 May 8, 2026
2534df0
hardening(identity): round-4 UCP signing reviewer findings
vvillait88 May 8, 2026
34d5e27
hardening(identity): round-5 UCP signing reviewer findings
vvillait88 May 8, 2026
d8bb5b4
hardening(identity): round-6 UCP signing reviewer findings
vvillait88 May 9, 2026
debc41f
docs(ucp): round-7 README rotation Cache-Control note
vvillait88 May 9, 2026
a3f5e05
hardening(identity): round-9 UCP signing reviewer findings
vvillait88 May 9, 2026
7f4db23
hardening(identity): round-11 UCP signing reviewer findings
vvillait88 May 9, 2026
95cd95a
hardening(identity): align UCP verifier error precedence with python …
vvillait88 May 9, 2026
e25b648
hardening(identity): align UCP header check order with python sibling
vvillait88 May 9, 2026
d5897c0
hardening(identity): pre-decode JWS header for crit+typ precedence pa…
vvillait88 May 9, 2026
e14057f
hardening(identity): reject U+2028/U+2029 in stableStringify
vvillait88 May 9, 2026
9627a21
hardening(identity): align buildUCPProfile claims coalescing with pyt…
vvillait88 May 9, 2026
4293500
hardening(identity): round-26 UCP signing reviewer findings
vvillait88 May 9, 2026
b9510a6
hardening(identity): align crit shape validation with python sibling
vvillait88 May 9, 2026
18aabcc
hardening(identity): add typed-claims cross-lang fixture
vvillait88 May 9, 2026
27f3a3c
docs: drop em dashes, update example count, add signed-ucp-merchant
vvillait88 May 9, 2026
0f2092d
test: cover uncovered branches to clear 90% global coverage gate
vvillait88 May 9, 2026
d0e450b
feat(identity): vendor-namespace UCP signing typ + capability name (1…
vvillait88 May 9, 2026
a99eba8
fix(identity): align hand-crafted capability fixture schema URL with SDK
vvillait88 May 9, 2026
dc40653
test(identity): refresh cross-lang fixture corpus with corrected py e…
vvillait88 May 9, 2026
bbcd862
test(identity): assert UCPPaymentHandler omits config when caller doe…
vvillait88 May 9, 2026
6d8bd41
chore(identity): drop fixture one-shots, document UCP per-element sha…
vvillait88 May 9, 2026
13d3e61
fix(identity): rename agentscoreSchemaUrl → agentscore_schema_url (py…
vvillait88 May 10, 2026
e2434f0
fix(identity)!: spec-compliant UCP profile shape (ucp envelope + map-…
vvillait88 May 10, 2026
1b927c8
docs: update README + signed-ucp-merchant example for new spec-compli…
vvillait88 May 10, 2026
fcbace1
fix(identity): regenerate cross-lang fixture corpus for spec-complian…
vvillait88 May 10, 2026
56aa9ae
docs(claude): clarify signed-ucp-merchant example is vendor-extension…
vvillait88 May 10, 2026
2791495
fix(examples): auto-detect alg from JWK shape (parity with python sib…
vvillait88 May 10, 2026
ac601c3
feat(a2a): add UCP extension declaration to A2A agent card builder
vvillait88 May 10, 2026
495a2f5
fix(identity): cross-lang parity tightening from spec audit
vvillait88 May 10, 2026
4315fde
chore(deps): bump mppx 0.6.16 -> 0.6.17
vvillait88 May 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
45 changes: 23 additions & 22 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ Every helper is extracted from a real consumer, not speculated.
| Subpath | What it is |
|---|---|
| `@agent-score/commerce/identity/{hono,express,fastify,nextjs,web}` | Trust gate middleware (KYC, age, sanctions, jurisdiction) |
| `@agent-score/commerce/identity/policy` | Framework-agnostic per-product / per-tier compliance policy helpers `PolicyBlock`, `policyToGateOptions`, `runGateWithEnforcement`, `shippingCountryAllowed`, `shippingStateAllowed` |
| `@agent-score/commerce/identity/policy` | Framework-agnostic per-product / per-tier compliance policy helpers: `PolicyBlock`, `policyToGateOptions`, `runGateWithEnforcement`, `shippingCountryAllowed`, `shippingStateAllowed` |
| `@agent-score/commerce/payment` | Networks/USDC/rails registries, paymentauth.org directive builders, `createX402Server` (peer-dep `@x402/core` + `@coinbase/x402` for the Coinbase facilitator), `buildX402AcceptsFor402` (one-call helper for the 402-emit path: builds the requirements via the registered scheme so `extra.name` matches the on-chain USDC contract per network), `createMppxServer` (peer-dep `mppx`), `processX402Settle` (verify+settle in one call), dispatch-by-network, signer extraction, WWW-Authenticate header, Settlement-Overrides header |
| `@agent-score/commerce/discovery` | Discovery probe middleware, Bazaar wrapper, `/.well-known/mpp.json` builder, `llms.txt` builder, `skill.md` builder (Claude-Skill-compatible agent-discovery manifest), OpenAPI snippets, `noindexNonDiscoveryPaths` Hono middleware |
| `@agent-score/commerce/challenge` | 402-body builders: accepted_methods, identity metadata, how_to_pay, agent_instructions, build402Body, `buildValidationError` (4xx body builder) |
Expand All @@ -18,7 +18,7 @@ Every helper is extracted from a real consumer, not speculated.

## Architecture

Single TypeScript package, tsup-built CJS + ESM with subpath exports. Per-framework identity adapters expose the same surface `agentscoreGate`, `captureWallet`, `getAgentScoreData`, `verifyWalletSignerMatch`, `getGateDegradedState`, `getGateQuotaInfo` with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim).
Single TypeScript package, tsup-built CJS + ESM with subpath exports. Per-framework identity adapters expose the same surface (`agentscoreGate`, `captureWallet`, `getAgentScoreData`, `verifyWalletSignerMatch`, `getGateDegradedState`, `getGateQuotaInfo`) with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim).

| Directory | Contents |
|---|---|
Expand All @@ -30,13 +30,13 @@ Single TypeScript package, tsup-built CJS + ESM with subpath exports. Per-framew
| `src/stripe-multichain/` | Stripe multichain PaymentIntent helpers |
| `src/api/` | `AgentScore` re-export from sdk |
| `examples/` | Runnable single-file Hono apps for each common scenario |
| `tests/` | Vitest, one file per surface, ~360+ tests |
| `tests/` | Vitest, one file per surface, ~750+ tests |

Peer-dep pattern: payment/x402/mppx/stripe modules `dynamic import` at runtime vendors install only what they use (`@x402/core`, `@x402/evm`, `@coinbase/x402`, `mppx`, `@solana/mpp`, `@solana/kit`, `stripe`). Missing peer dep throws a guiding error with the install command. x402 in this SDK is EVM-only; Solana SPL payments go through MPP `solana/charge` (`@solana/mpp/server`).
Peer-dep pattern: payment/x402/mppx/stripe modules `dynamic import` at runtime, so vendors install only what they use (`@x402/core`, `@x402/evm`, `@coinbase/x402`, `mppx`, `@solana/mpp`, `@solana/kit`, `stripe`). Missing peer dep throws a guiding error with the install command. x402 in this SDK is EVM-only; Solana SPL payments go through MPP `solana/charge` (`@solana/mpp/server`).

## Examples

`examples/` contains full single-file Hono apps for the most common merchant scenarios copy-paste templates, not frameworks:
`examples/` contains full single-file Hono apps for the most common merchant scenarios; copy-paste templates, not frameworks:

| Example | Scenario |
|---|---|
Expand All @@ -45,28 +45,29 @@ Peer-dep pattern: payment/x402/mppx/stripe modules `dynamic import` at runtime
| `multi-rail-merchant.ts` | Full agent-commerce: identity + Tempo MPP + x402 + Stripe SPT |
| `stripe-multichain-merchant.ts` | Stripe-anchored multichain (PaymentIntent → tempo/base/solana deposit addresses) |
| `variable-cost-merchant.ts` | Pay-per-actual-usage on **two protocols**: x402 upto (Permit2 + Settlement-Overrides) AND MPP tempo session (channel + SSE + mid-stream vouchers) |
| `compliance-merchant.ts` | Regulated-goods merchant — full compliance gate + custom `onDenied` composing the denial helpers (`verificationAgentInstructions`, `isFixableDenial`, `buildSignerMismatchBody`, `buildContactSupportNextSteps`, `denialReasonToBody`/`denialReasonStatus`) |
| `per-product-policy-merchant.ts` | Multi-product merchant where each product carries its own compliance policy — wine has hard gate (KYC + 21 + state allowlist), tee has none (anonymous), limited print uses `enforcement: 'soft'` (request KYC, accept anonymous, stamp `identity_status: 'unverified'`). Demonstrates `PolicyBlock`, `policyToGateOptions`, `runGateWithEnforcement`, `shippingCountryAllowed`, `shippingStateAllowed`. |
| `compliance-merchant.ts` | Regulated-goods merchant: full compliance gate + custom `onDenied` composing the denial helpers (`verificationAgentInstructions`, `isFixableDenial`, `buildSignerMismatchBody`, `buildContactSupportNextSteps`, `denialReasonToBody`/`denialReasonStatus`) |
| `per-product-policy-merchant.ts` | Multi-product merchant where each product carries its own compliance policy: wine has hard gate (KYC + 21 + state allowlist), tee has none (anonymous), limited print uses `enforcement: 'soft'` (request KYC, accept anonymous, stamp `identity_status: 'unverified'`). Demonstrates `PolicyBlock`, `policyToGateOptions`, `runGateWithEnforcement`, `shippingCountryAllowed`, `shippingStateAllowed`. |
| `signed-ucp-merchant.ts` | Signed UCP profile (`/.well-known/ucp`) + JWKS endpoint (`/.well-known/jwks.json`). AgentScore's `agentscore-profile+jws` is a vendor extension on top of UCP for trust-mode verifiers (Visa AP2 pilots, regulated-commerce verifiers) that opt into auditable cryptographic provenance — UCP §6 itself does NOT mandate signing; Pura Vida and other Shopify-backed UCP merchants ship unsigned in production. Wires ephemeral-for-dev / env-JWK-for-prod signing, kid rotation, and `Cache-Control` posture. Uses `generateUCPSigningKey`, `signUCPProfile`, `buildJWKSResponse`, `ucpSigningKeyFromJWK`, `UCPVerificationError`. |

## Identity model

Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator-Token`). Default checks operator-token first, then wallet. Address normalization is network-aware via `src/identity/address.ts`: EVM lowercased, Solana base58 preserved verbatim — used for cache keys, wallet→operator resolves, and signer-match comparisons.
Two identity types: wallet (`X-Wallet-Address`) and operator-token (`X-Operator-Token`). Default checks operator-token first, then wallet. Address normalization is network-aware via `src/identity/address.ts`: EVM lowercased, Solana base58 preserved verbatim. Used for cache keys, wallet→operator resolves, and signer-match comparisons.

Denial reason codes: `missing_identity`, `identity_verification_required`, `token_expired`, `invalid_credential`, `wallet_signer_mismatch`, `wallet_auth_requires_wallet_signing`, `wallet_not_trusted`, `api_error`, `payment_required`. Each carries a structured `agent_instructions` JSON block describing concrete recovery actions. See `src/identity/_response.ts` and `src/core.ts` for the canned action copy.

`createSessionOnMissing` auto-mints a verification session when no identity is present and returns 403 with `verify_url` + poll instructions instead of a bare denial. `verifyWalletSignerMatch` (per-adapter) recovers the signer from MPP/x402 credentials and compares against `linked_wallets[]` for cross-chain wallet-stack matching.

Captured wallets: `captureWallet(ctx, { walletAddress, network, idempotencyKey })` is fire-and-forget reads `operator_token` stashed during gating and POSTs to `/v1/credentials/wallets`. No-ops for wallet-authenticated requests.
Captured wallets: `captureWallet(ctx, { walletAddress, network, idempotencyKey })` is fire-and-forget; reads `operator_token` stashed during gating and POSTs to `/v1/credentials/wallets`. No-ops for wallet-authenticated requests.

Wallet-signer-match: `verifyWalletSignerMatch(ctx, { signer, network })` makes a single `/v1/assess` call with `resolve_signer` set; the API resolves both wallets and emits a `signer_match` verdict in the same response — collapses the legacy 2 follow-up assess calls into one round trip. Repeat lookups for the same `(claimed, signer)` pair hit a per-cache-entry `signerMatchBySigner` sub-map and skip the API entirely. Falls back to a 2-resolve path when the API doesn't emit `signer_match` (canary rollout safety). Signer recovery covers x402 EIP-3009 (EVM `from` address), Tempo MPP (`did:pkh:eip155` source), and Solana MPP `solana/charge` (via `did:pkh:solana` source when set, otherwise by decoding the credential's signed-tx payload to read the SPL `TransferChecked` authority pull mode only, requires the `@solana/kit` optional peer).
Wallet-signer-match: `verifyWalletSignerMatch(ctx, { signer, network })` makes a single `/v1/assess` call with `resolve_signer` set; the API resolves both wallets and emits a `signer_match` verdict in the same response, collapsing the legacy 2 follow-up assess calls into one round trip. Repeat lookups for the same `(claimed, signer)` pair hit a per-cache-entry `signerMatchBySigner` sub-map and skip the API entirely. Falls back to a 2-resolve path when the API doesn't emit `signer_match` (canary rollout safety). Signer recovery covers x402 EIP-3009 (EVM `from` address), Tempo MPP (`did:pkh:eip155` source), and Solana MPP `solana/charge` (via `did:pkh:solana` source when set, otherwise by decoding the credential's signed-tx payload to read the SPL `TransferChecked` authority; pull mode only, requires the `@solana/kit` optional peer).

### Fail-open (opt-in)

`failOpen: true` on `agentscoreGate({...})` flips infra-failure handling: 429 / 5xx / network-timeout return `{ kind: 'allow', degraded: true, infraReason: 'quota_exceeded' | 'api_error' | 'network_timeout' }` instead of throwing. Per-adapter `getGateDegradedState(c)` exposes the flag for merchant logging/alerting; `withAgentScoreGate` (Next.js / Web Fetch) propagates `degraded` + `infraReason` directly on the handler's `gate` arg. Default stays `failOpen: false` regulated commerce should keep it. Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of the flag.
`failOpen: true` on `agentscoreGate({...})` flips infra-failure handling: 429 / 5xx / network-timeout return `{ kind: 'allow', degraded: true, infraReason: 'quota_exceeded' | 'api_error' | 'network_timeout' }` instead of throwing. Per-adapter `getGateDegradedState(c)` exposes the flag for merchant logging/alerting; `withAgentScoreGate` (Next.js / Web Fetch) propagates `degraded` + `infraReason` directly on the handler's `gate` arg. Default stays `failOpen: false`; regulated commerce should keep it. Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of the flag.

### Mount posture: gate-first vs gate-conditional

`agentscoreGate(...)` returns a vanilla framework middleware. Mount it directly when the route is AgentScore-only (`app.use('/purchase', gate)` in Hono / Express, `dependencies=[Depends(gate)]` in FastAPI, etc.)every request runs identity + policy. To support **anonymous discovery by any spec-compliant x402 wallet** (Coinbase awal, Phantom, Solflare, ), wrap the gate so it fires only when a payment credential is attached:
`agentscoreGate(...)` returns a vanilla framework middleware. Mount it directly when the route is AgentScore-only (`app.use('/purchase', gate)` in Hono / Express, `dependencies=[Depends(gate)]` in FastAPI, etc.); every request runs identity + policy. To support **anonymous discovery by any spec-compliant x402 wallet** (Coinbase awal, Phantom, Solflare, ...), wrap the gate so it fires only when a payment credential is attached:

```ts
const _gate = agentscoreGate({ /* opts */ });
Expand All @@ -85,15 +86,15 @@ Anonymous POST flows through to the handler unauthenticated and gets a 402 with

### `compatible_clients` field on emitted 402s

`buildAgentInstructions` emits a `compatible_clients` field in the 402 body, derived automatically from `howToPay` per-rail list of CLIs the AgentScore team has smoke-verified end-to-end. Vendors override with `buildAgentInstructions({ howToPay, compatibleClients: {...} })` to add their own tested clients. Set to an empty object `{}` to suppress the default. Same data is published as `core/docs/integrations/x402-clients.mdx` for human-side rationale + per-rail commands.
`buildAgentInstructions` emits a `compatible_clients` field in the 402 body, derived automatically from `howToPay`: per-rail list of CLIs the AgentScore team has smoke-verified end-to-end. Vendors override with `buildAgentInstructions({ howToPay, compatibleClients: {...} })` to add their own tested clients. Set to an empty object `{}` to suppress the default. Same data is published as `core/docs/integrations/x402-clients.mdx` for human-side rationale + per-rail commands.

## Tooling

- **Bun** package manager.
- **ESLint 9** linting.
- **tsup** CJS + ESM build with subpath exports.
- **Vitest** tests.
- **Lefthook** pre-commit lint, pre-push typecheck.
- **Bun**: package manager.
- **ESLint 9**: linting.
- **tsup**: CJS + ESM build with subpath exports.
- **Vitest**: tests.
- **Lefthook**: pre-commit lint, pre-push typecheck.

```bash
bun install
Expand All @@ -112,16 +113,16 @@ During local development the sdk dep is `link:@agent-score/sdk`. Run `bun link`
1. Create a branch
2. Make changes
3. Lefthook runs lint on commit, typecheck on push
4. Open a PR CI runs automatically
4. Open a PR; CI runs automatically
5. Merge (squash)

## Rules

- **No silent refactors**
- **Never commit .env files or secrets**
- **Use PRs** never push directly to main
- **Helpers are protocol translations + configurable opinions, not opinionated frameworks** vendor variation is config, not API redesign
- **Extract from real consumers** every helper lifts from working production code
- **Use PRs**: never push directly to main
- **Helpers are protocol translations + configurable opinions, not opinionated frameworks**: vendor variation is config, not API redesign
- **Extract from real consumers**: every helper lifts from working production code

## Releasing

Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ Thanks for your interest in contributing! Here's how to get started.

- All PRs require 1 approval before merging
- Squash merge to `main` is the standard
- Keep PRs focused one feature or fix per PR
- Keep PRs focused; one feature or fix per PR
- Include tests for new functionality
- Make sure CI passes before requesting review

Expand Down
Loading
Loading