feat(identity): UCP profile signing helpers (1.3.4)#12
Merged
vvillait88 merged 35 commits intomainfrom May 10, 2026
Merged
Conversation
Add four helpers for UCP §6 trust-mode profiles: generateUCPSigningKey, signUCPProfile, verifyUCPProfile, buildJWKSResponse. Profiles are signed with JWS Compact Serialization over a JCS-canonicalized body; verifiers look up the kid in the merchant's JWKS and validate against the canonical body of the presented profile. Both EdDSA (Ed25519) and ES256 are supported. Cross-language byte parity with agentscore-commerce Python SDK: profiles signed by either flavor verify in the other. Bumps to 1.3.4. jose is an optional peer dep; vendors install it only when publishing signed UCP profiles. /.well-known/jwks.json is added to the default discovery path allowlist. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- mppx 0.6.15 → 0.6.16 (dev) - @types/node 25.6.0 → 25.6.2 (dev) - override: fast-uri ^3.1.1 (resolves to 3.1.2) — addresses GHSA-q3j6-qgpj-74h6 in ajv's transitive dep, surfaced via the OSV dependency scan on this branch's CI Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reviewer audit surfaced three real issues + several gaps. Fixes: Security: - verifyUCPProfile restricts alg to ['EdDSA','ES256'] before importJWK and passes algorithms[] to compactVerify, closing the alg-confusion attack where a hostile oct JWK in signing_keys[] could mint HS256 signatures that verified. - Enforces JWS protected header `typ='ucp-profile+jws'` (RFC 8725 §3.11). - Rejects duplicate kids in JWKS (prevents ambiguous key resolution). - canonicalizeProfile rejects non-integer Number values at sign-time (cross-language float canonicalization is not stable; use decimal strings for monetary fields). Ergonomics: - New UCPVerificationError with discriminated `code` (no_signature/missing_kid/kid_not_found/duplicate_kid/unsupported_alg/ wrong_typ/signature_invalid/body_mismatch/malformed_jws). Consumers can branch without parsing message strings. Docs: - README documents alg/typ/kid enforcement, error codes, float defense, HSM/KMS integration, key rotation runbook, JWKS-vs-inline trust model. - New examples/signed-ucp-merchant.ts demonstrates the full sign+verify flow with env-var-loaded key + ephemeral fallback + race-safe Promise cache + cache headers. Tests: - Added 12 security regression tests including a real HS256 alg-confusion attack assertion, typ check, dup-kid, malformed JWS, signature segment tampering, EdDSA determinism, ES256 non-determinism, float rejection. - New cross-language fixture corpus (tests/fixtures/cross-lang/) with 6 signed profiles (3 Node + 3 Python) that both SDKs verify, locking in byte parity at CI level. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Second-pass review surfaced a critical Promise-cache regression + several
gap fixes. Net 12 new tests.
- loadSigningKey caches rejected Promise → fixed in 3 places
(martin-estate / core/api / examples/signed-ucp-merchant.ts): on
rejection, clear the cache so next caller retries instead of
permanently poisoning the merchant.
- verifyUCPProfile: wrap JOSENotSupported (RFC 7515 §4.1.11 unrecognized
`crit` header) into UCPVerificationError('unrecognized_critical_header').
RFC 8725 §3.10 says verifiers MUST reject unknown crit; previously we
leaked raw library exception.
- verifyUCPProfile: malformed JWKS shape (null, missing keys, non-array)
now emits UCPVerificationError('malformed_jwks') instead of raw
TypeError. New code added to the discriminated enum.
- verifyUCPProfile: reject JWKs with use != 'sig' as 'unusable_key'
(RFC 7517 §4.2).
- signUCPProfile: enforce that opts.kid matches a JWK in
profile.signing_keys[] at sign-time. Previously a kid-mismatch silently
produced an envelope no verifier could resolve.
- canonicalize: reject NaN/Infinity Numbers in addition to non-integer
Numbers. Lossy JSON serialization avoided.
- Cross-language fixture corpus: 6 → 12 fixtures covering minimal,
ES256-rails, extras-int, capability, unicode, multi-key per language.
Test boundary tightened from `>=6` to specific scenarios.
- Fix vacuous key-order-independence test that was a no-op
(JSON.parse/stringify preserves order); now hand-constructs reverse
insertion-order objects.
- Fix broken JSDoc subpath imports
(`@agent-score/commerce/identity/ucp-jwks` → `@agent-score/commerce`).
- README KMS claim rewritten — `createPrivateKey` does NOT wrap remote
KMS signers; spelled out the actual persistence pattern.
- Tampered-signature test now flips the FIRST char (not last) since the
last base64url char is partial padding bits and may not affect the
decoded signature.
Tests: 712 pass, 4 skipped. Lint + typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- verifyUCPProfile: non-string signature now emits typed
UCPVerificationError(no_signature) instead of jose throwing a
less-typed downstream error. Parity with Python.
- verifyUCPProfile: JWS header kid must be a non-empty string. Strict
type check prevents non-string kid (number/bool/null) from
accidentally matching JWKs with equal-typed kids.
- signUCPProfile: opts.kid must be a non-empty string. Empty string was
silently passing the in-signing_keys check.
- buildUCPProfile: extras-collision filter rejects keys that match
reserved profile fields (signing_keys, services, etc.) so a careless
`extras: { signing_keys: [...] }` can't silently destroy the explicit
field.
- New ucpSigningKeyFromJWK helper (parity with Python's
UCPSigningKey.from_jwk classmethod). Validates kid+kty, rejects oct
symmetric keys.
Tests: 712 pass, 4 skipped. Lint + typecheck clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Defensive guards + test parity with the Python sibling.
ucp-jwks.ts:
- verifyUCPProfile guards profile shape at entry: reject null,
non-object, and arrays with a typed UCPVerificationError(no_signature)
rather than throwing TypeError on `delete stripped.signature`.
- JWKS-entry filter skips null and non-object entries so a malformed
keys[] cannot crash on `.kid` access; surfaces as kid_not_found.
- signUCPProfile validates alg against ALLOWED_ALGS at runtime, matching
the verifier's allowlist; an out-of-band alg now fails at sign time.
- kid_not_found and duplicate_kid messages JSON.stringify the kid value
to avoid log injection from an attacker-controlled string.
Tests:
- ucpSigningKeyFromJWK round-trip coverage for EdDSA + ES256, and
rejection of oct, missing kid, missing kty, and non-object inputs.
- verifyUCPProfile rejects use=enc as unusable_key.
- verifyUCPProfile rejects non-string signature values (42, null, [],
{}) as no_signature.
- JWKS keys[] containing null or string entries surfaces kid_not_found.
- JWS protected header that decodes to a JSON array is rejected as a
typed UCPVerificationError instead of leaking a TypeError.
Docs + example:
- README error-code list now includes unusable_key, malformed_jwks, and
unrecognized_critical_header with one-line explanations.
- signed-ucp-merchant.ts demonstrates ucpSigningKeyFromJWK on the
signing_keys[] entry.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tighten stableStringify rejection set + verifier alg consistency + lock
codepoint-aware key sort with a non-ASCII fixture.
stableStringify now rejects undefined, function, Symbol, and Date values
explicitly so silently-dropped fields or {} from Date#toJSON cannot break
cross-language byte parity. Adds Number.isSafeInteger check after the
existing isInteger guard to reject integers above 2^53 (which Node has
already silently rounded by the time stableStringify sees them).
Object key sort switches from default (UTF-16 code units) to Unicode
codepoint order so Node matches Python json.dumps(sort_keys=True) for
supplementary-plane keys.
verifyUCPProfile additionally rejects when a matched JWK declares an
alg that disagrees with the JWS protected-header alg (RFC 7517 4.4).
Adds node-i18n-keys cross-lang fixture (BMP Latin-1 cafe + BMP CJK +
non-BMP wine glass + ASCII low/high) as a regression lock for the
codepoint sort. Verified the same fixture passes the python-commerce
verifier; matching py-i18n-keys lands on that side in lockstep.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tighten cross-lang fixture corpus + extras-collision defenses + stableStringify
rejection set so cross-language byte parity has airtight regression coverage.
Cross-lang fixture corpus: previous emoji/i18n fixtures used chars (cafe, JP, wine
glass, CJK Compat) where UTF-16 first-unit sort and Unicode codepoint sort produce
identical canonical bodies, so the codepoint-sort fix was silently untested.
Replaces node-i18n-keys.json with node-emoji-keys.json using a key set that
genuinely distinguishes the two: BMP private use (U+E000, codepoint 57344)
alongside supplementary-plane wine glass (U+1F377, codepoint 127863, UTF-16 first
unit 55356). Codepoint sort puts U+E000 BEFORE U+1F377; UTF-16 first-unit sort
reverses that. Both repos now ship both node-emoji-keys.json and py-emoji-keys.json
in their fixture corpus so each repo's verifier validates the OTHER language's
canonical body. A regression in either language's key sort surfaces here.
buildUCPProfile RESERVED set adds __proto__, constructor, prototype so vendor
extras can't slip prototype-pollution payloads into the canonical body and
surprise downstream consumers.
stableStringify gains explicit reject branches for BigInt (raw TypeError from
JSON.stringify), Map / Set / WeakMap / WeakSet (silently produce {}), and typed
arrays (produce numeric-keyed objects). Each emits the same shaped Error as the
existing rejects so callers can branch on a consistent surface.
Drops the redundant Number.MAX_SAFE_INTEGER + 2 case (collapsed to the same float
as MAX_SAFE_INTEGER + 1); replaces with 2**60 which is genuinely distinct.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tell publishers to set Cache-Control: public, max-age=300 on /.well-known/jwks.json and wait at least that long after publishing the new key before removing the old JWK during rotation. Mirrors the python-commerce sibling. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Close cross-language byte-parity hole between node-commerce and python-commerce
on int handling. Node's stableStringify already rejects integers outside
Number.MAX_SAFE_INTEGER (2^53 - 1); Python's _reject_floats only walked for
float, so a Python-signed profile with an oversized int produced a valid
self-verifying envelope that Node could not parse. The Python fix lands in
python-commerce; this commit ships the matching cross-lang fixture
(int-boundary) on the Node side plus a regenerator script for symmetry.
Cross-lang fixture: tests/fixtures/cross-lang/{node,py}-int-boundary.json
exercise max_safe_int / min_safe_int / small_int / neg_small_int / zero. Both
fixtures verify cleanly in both languages. py-int-boundary.json was generated
by the python-commerce companion script and copied here.
Cross-lang corpus meta-test now asserts both int-boundary fixtures exist.
scripts/generate-int-boundary-fixture.ts: one-shot regenerator.
Tests: 757 pass + 4 skipped (was 753 + 4). ESLint + tsc clean.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wrap verifier-side canonicalize call so JCS-incompatible Numbers in a received profile (non-integer, non-finite, oversized) emit a typed UCPVerificationError(body_mismatch) instead of a raw Error. Consumers catching UCPVerificationError no longer have a leak path. Add regression test for error-precedence parity with python-commerce: verify(null, malformedJwks) returns no_signature in both languages (profile-shape check fires first). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…sibling Reorder verifyUCPProfile so JWS header validation (typ/alg/kid via compactVerify callback) runs before canonicalizing the stripped profile body. Matches python-commerce's _peek_jws_header order. A profile carrying both a malformed body (e.g. extras.rate=1.5) and a malformed JWS header (typ="JWT") now surfaces wrong_typ in both SDKs instead of body_mismatch in node and wrong_typ in python. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Reorder the compactVerify resolver to check typ before alg before kid, matching python-commerce's _peek_jws_header. Drop the `algorithms` option since jose enforces it before invoking the resolver, which would short-circuit typ. A profile carrying both alg=HS256 and typ=JWT now emits wrong_typ in both SDKs instead of diverging. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…rity A JWS with both an unrecognized `crit` header AND a wrong `typ` was emitting `unrecognized_critical_header` in Node but `wrong_typ` in python-commerce. jose's compactVerify enforces `crit` before invoking the key-resolver callback, so the typ check inside the resolver never ran. Mirror python-commerce's `_peek_jws_header`: pre-decode the JWS protected header in our code and check typ, alg, kid, crit up-front. compactVerify still runs for signature + body verification. Cross-SDK callers routing on the typed `code` now see the same value for the same input. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Mirrors the rejection in core/api/src/lib/canonicalize.ts so the SDK canonicalizer stays byte-symmetric across the Node and Python siblings on any pre-ES2019 V8 (where JSON.stringify still escapes these codepoints) or browser-side verifier path; today's V8 emits them raw, so the divergence is theoretical but the contract is now consistent with the upstream check. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…hon sibling The API can return account_verification with either null or empty-string for un-set fields depending on the row state. The node builder used `??` (collapse null/undefined only, pass empty-string verbatim); the python sibling used dict.get(k) or DEFAULT for kyc_level/verified_at but dict.get(k, DEFAULT) for age_bracket/jurisdiction (returns None verbatim if key is present-but-null). For the same AssessResult shape, that meant the two SDKs emitted different canonical claims blocks, so a profile signed in one language failed verify in the other. Switch node to `||` for the four account_verification fields (kyc_level, age_bracket, jurisdiction, verified_at) so null AND empty-string both fall through to the schema default, matching the python alignment shipped in the same round. Adds a data-driven-claims cross-lang fixture that, unlike the rest of the corpus, exercises buildUCPProfile's actual data path (constructs a synthetic AgentScoreData with the API "missing" sentinels and lets the builder coalesce). Both languages now emit identical canonical bytes for the input. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
stableStringify now rejects U+2028 / U+2029 in object keys (previously checked only string values); object keys flow through JSON.stringify(k) which on modern V8 emits them raw, while Python's _reject_unsafe_numbers recurses into dict keys and rejects them, so a Node-signed profile with a separator-bearing key would fail Python verify with body_mismatch. Verifier now treats explicit JSON null on JWK.use / JWK.alg as absent (skip-on-null) rather than rejecting; both fields are optional per RFC 7517 and Python ucp_jwks.py uses ``is not None`` semantics, so the Node ``!== undefined`` check was the only side that rejected null and broke language symmetry. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
RFC 7515 §4.1.11 mandates that `crit` be a non-empty array of strings if present. The previous check (`Array.isArray(header.crit) && header.crit.length > 0`) only triggered the unrecognized-header branch on a well-formed crit array and silently accepted `crit: null`, `crit: []`, `crit: "fakething"`, and `crit: [42]`. python-commerce rejects all four shapes with malformed_jws per the same RFC; the parity gap meant a malformed JWS could verify on node but fail on python (or vice versa) for the same input. Shape-check first (gate on `'crit' in header` so explicit null is caught), then the unrecognized-extension check. Adds parametrized vitest cases mirroring the four python-commerce parity inputs; existing `crit: ['unknown']` -> `unrecognized_critical_header` test still passes. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
New `typed-claims` cross-lang fixture exercises the typed-field-only read path (`AgentScoreData.account_verification` populated directly, no raw fallback) that the existing `data-driven-claims` fixture didn't cover (it uses the python `raw=` shape on the python side). `buildUCPProfile` reads the typed fields directly without consulting raw, so both languages must produce byte-identical canonical bytes for the typed path or cross-lang verify silently drifts in production. Pairs with the python sibling commit that fixes the typed-empty-dict read-order bug and preserves empty `UCPPaymentHandler.config` for byte-parity with the TypeScript serializer. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace em-dashes with periods/semicolons/colons/parens across README, CLAUDE, examples/README, CONTRIBUTING. Drop the stale `1.0.0` Stability header reference. Add the `signed-ucp-merchant.ts` example to both the top-level README count (7 -> 8) and the examples + CLAUDE tables. Refresh the test count (~360 -> ~750) to match reality. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds focused tests for fallback paths in a2a card identity, payment headers optional spreads, openapi snippet section toggles, robots_tag custom-paths handling across adapters, signer @solana/kit short-circuit conditions, ucp signature_invalid mapping, and core verifyWalletSignerMatch edges (API-emitted wallet_auth_requires_wallet_signing verdict + fallback resolve catch). Lifts global branch coverage from 89.86% to 91.45%. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
….4.0) UCP §6 does not define a profile-as-JWS typ, and UCP capability names must follow reverse-DNS namespacing (`^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9_]*)+$`). The previous `typ: ucp-profile+jws` and `agentscore-identity` capability name implied UCP-canonical signing/slots they aren't. Renamed to vendor-namespaced honest forms: - JWS protected header `typ`: `ucp-profile+jws` -> `agentscore-profile+jws` (matches the `agentscore-risk-signal+jws` pattern AP2 already uses). - Capability name: `agentscore-identity` -> `sh.agentscore.identity` (passes the UCP regex; namespace authority `agentscore.sh`). - Schema URL path: `agentscore-identity.v1.json` -> `sh-agentscore-identity-v1.json` (path matches the new capability name; URL hosted under namespace authority `agentscore.sh` per UCP convention). All 10 cross-lang fixture scenarios re-signed via the new `scripts/regenerate-cross-lang-fixtures.ts` orchestrator. Hand-crafted `*-capability.json` fixtures bumped to the new name to keep the corpus honest about what callers should publish. 808 tests pass. Cross-lang verify against the python sibling's regenerated `py-*` corpus passes byte-identically for the data-driven and typed-claims fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The capability scenario in the cross-lang orchestrator hand-crafted the old `https://agentscore.sh/schema/identity/1` URL while the SDK's buildUCPProfile auto-injects the new `https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json`. The inconsistency made the corpus dishonest about what callers should publish. Update the orchestrator script to use the canonical URL and regenerate all 20 fixtures so canonical bytes stay byte-identical between Node and Python siblings. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…moji-keys + int-boundary Picks up the py-side regenerator fix that unwrapped doubly-nested `extras` in the emoji-keys and int-boundary scenarios. Node fixtures regenerate with fresh keypairs as a side effect of running the corpus together; structural shapes of the eight unchanged scenarios are identical to the prior corpus. py-emoji-keys.json and py-int-boundary.json now match node-* canonical bodies byte-for-byte (modulo per-run keypair material), so cross-lang verify covers the non-ASCII-keys-interleaved-with-canonical-fields case in both directions. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…s not set it
Cross-lang parity test: Node's `UCPPaymentHandler.config` is a TypeScript
optional property, so `{ name: 'tempo' }` ships a wire profile without the
`config` key. Python's UCPPaymentHandler.to_dict() now matches by omitting
empty configs; this test pins the Node side of the contract so a future
refactor that auto-emits `config: {}` would fail loudly.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…pe parity - Remove standalone generate-*-fixture.ts scripts that duplicate scenarios already covered by regenerate-cross-lang-fixtures.ts; standalones used bare JSON.stringify (no ensureAscii equivalent matters here, but the divergent code path would silently drift if a future unicode field landed). Orchestrator is now the single source of truth. - Add a design note to src/identity/ucp.ts explaining the per-element shape choice: Node interfaces (UCPSigningKey / UCPService / UCPCapability) accept canonical fields plus vendor extras flat in the same object via index signatures, with no separate extras slot. The python sibling models these as dataclasses with an explicit extras dict and to_dict() reserved-key collision guards. Both designs offer equivalent guarantees: in Node the canonical names are positional fields in the interface, so a duplicate is a static type error or a trivial self-overwrite in object-literal syntax; there is no separate map that could shadow a reserved key. The top-level BuildUCPProfileInput.extras DOES carry a runtime guard because that is a dedicated map populatable via spread. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…thon parity) Field name on BuildUCPProfileInput now matches the python sibling's build_ucp_profile(agentscore_schema_url=...) keyword arg. Vendors switching languages get the same param name in both SDKs. Also updates the test that asserted the camelCase form. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…keyed bindings)
The pre-refactor `buildUCPProfile` emitted a flat top-level body with services /
capabilities / payment_handlers as ARRAYS, which doesn't match the UCP §6 spec.
Verified against the live Pura Vida reference profile at
puravidabracelets.com/.well-known/ucp (Shopify's UCP integration): the spec body
nests under `ucp` with services / capabilities / payment_handlers as MAPS keyed
by reverse-DNS service / capability / handler name.
A trust-mode UCP verifier reading the old shape would skip us as malformed
before even checking the signature, so cross-language byte-parity tests were
passing while the published profile was non-parseable.
This is a breaking change to the SDK surface:
Output shape change:
- Top-level: { ucp: { version, services, capabilities, payment_handlers, name?,
supported_versions? }, signing_keys: [...], signature?: "..." }
- services: Record<string, UCPServiceBinding[]> keyed by service name
(e.g., 'dev.ucp.shopping')
- capabilities: Record<string, UCPCapabilityBinding[]> keyed by capability name
- payment_handlers: Record<string, UCPPaymentHandlerBinding[]> keyed by handler
reverse-DNS name
- Each binding carries spec-required fields (id, version, spec, schema for
handlers; version, spec, schema for capabilities)
Type renames:
- UCPService → UCPServiceBinding
- UCPCapability → UCPCapabilityBinding
- UCPPaymentHandler → UCPPaymentHandlerBinding
- New: UCPProfileBody (the inner `ucp` envelope)
Auto-injected sh.agentscore.identity capability now extends both
dev.ucp.shopping.checkout AND dev.ucp.shopping.cart (matching Shopify's
dev.shopify.catalog.storefront multi-parent pattern in the live ecosystem). New
optional `agentscore_spec_url` input, plus `supported_versions` map at profile
root (Pura Vida pattern), plus `ucp_extras` for vendor extras inside the ucp
envelope.
Cross-language byte-parity preserved with python-commerce sibling (mirror
refactor in agentscore-commerce).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ant shape README and the canonical signed-ucp-merchant example now show services / payment_handlers as MAPS keyed by reverse-DNS name (matches the buildUCPProfile output and the live Pura Vida reference profile). README section on profile-body signing reframed: not "UCP §6 trust-mode requires signing" (it doesn't — Pura Vida ships unsigned in production); instead "vendor extension for trust-mode verifiers that opt into auditable profiles". Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…t shape Both regen scripts (Node + Python) were still emitting the OLD flat-array shape; the existing 20 fixtures verified cryptographically (signature is shape-agnostic) but tested the wrong shape. Updated regen script to use the spec-compliant input (services / capabilities / payment_handlers as MAPS keyed by reverse-DNS service / capability / handler name) and regenerated all 10 node-* fixtures + synced the 10 py-* fixtures from the python sibling. Also corrected two stale docstrings in examples/README.md and examples/signed-ucp-merchant.ts that claimed "UCP §6 trust-mode requires JWS signature" — UCP doesn't mandate signing; that's an AgentScore vendor extension for opt-in trust-mode verifiers (Pura Vida and Shopify-backed UCP merchants ship unsigned in production today). Cross-lang.test.ts: 21/21 pass. The corpus now actually tests cross-language byte-parity for the spec-compliant shape, not just signature verification. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…, not UCP-required UCP §6 does not mandate profile-body signing; agentscore-profile+jws is our vendor extension for opt-in trust-mode verifiers. Pura Vida and Shopify-backed UCP merchants ship unsigned in production. Aligns CLAUDE.md table description with the in-file docstring already corrected. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…ling) The Node example forced env UCP_SIGNING_KEY_ALG onto the JWK regardless of what kty/crv the env JWK actually had. If a user set UCP_SIGNING_KEY_JWK_PRIVATE to a P-256 (ES256) JWK but didn't set ALG=ES256, importJWK would fail with a confusing error. Now detect from kty+crv directly (Ed25519→EdDSA, P-256→ES256) and reject unsupported curves with a clear message — matches the python sibling's behavior exactly. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Per UCP §A2A binding (https://ucp.dev/specification/checkout-a2a/): "Businesses supporting UCP must advertise the extension and any optional capabilities in their A2A Agent Card to allow platforms to activate the extension." The agent card MUST carry a top-level `extensions[]` array with an entry whose `uri` matches `https://ucp.dev/2026-04-08/specification/reference`. Without it, spec-strict A2A-first discovery cannot detect UCP support from the card alone. Adds: - `extensions?: A2AAgentCardExtension[]` on `A2AAgentCard` + builder input - `UCP_A2A_EXTENSION_URI` constant pinned to the 2026-04-08 spec snapshot - `ucpA2AExtension(capabilities?)` helper that builds the canonical UCP entry, defaulting to empty capabilities for vendors that serve UCP at the discovery layer but haven't bound formal capabilities yet - Re-exports from package barrel + 5 new tests Generic `extensions[]` field shape matches A2A v1.0 verbatim, so other A2A extensions (not just UCP) compose the same way. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three small parity/defensiveness fixes from the post-spec-audit review: 1. buildA2AAgentCard now skips emitting `extensions` when passed an empty array (matches python-commerce's to_dict behavior). Cross-language profiles canonicalize to identical bytes when both omit. Test added. 2. ucpSigningKeyFromJWK now validates that EC + OKP JWKs carry a non-empty `crv` field. Previously a malformed JWK without crv would silently land in signing_keys and fail at sign-time with an unclear error. Two tests added. 3. README example now shows `extensions: [ucpA2AExtension()]` on the buildA2AAgentCard call so vendors copying the snippet ship spec-compliant A2A cards out of the box. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Patch within the ^0.6.x range. wevm ecosystem upstream tracks the same
versions we already depend on for Tempo/Solana/EVM rails. Tests green
(823 pass, 4 skipped).
Major bumps deferred to dedicated PRs:
- jose 5 -> 6 (type-generic position changes + KeyObject->CryptoKey
runtime change in Node, requires test verification)
- eslint 9 -> 10 + @eslint/js 9 -> 10 (config format changes)
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.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
generateUCPSigningKey,signUCPProfile,verifyUCPProfile,buildJWKSResponse. Profiles are JWS-signed (Compact Serialization) over a JCS-canonicalized body. Both EdDSA (Ed25519, default) and ES256 supported.agentscore-commercePython SDK 1.3.7 — profiles signed by either SDK verify in the other.joseis an optional peer dep — vendors install it only when publishing signed UCP profiles. UnsignedbuildUCPProfilekeeps working./.well-known/jwks.jsonadded to the default discovery-path allowlist.Test plan
kid🤖 Generated with Claude Code