From 1fed2b3f7b8f17d5dd3f23a8cb505bd888de8f2c Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 10:57:28 -0700 Subject: [PATCH 01/35] feat(identity): UCP profile signing helpers (jose peer dep) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 12 +- bun.lock | 3 + package.json | 7 +- src/discovery/robots_tag.ts | 1 + src/identity/ucp-jwks.ts | 244 +++++++++++++++++++++++++++++ src/index.ts | 10 ++ tests/identity/ucp-signing.test.ts | 133 ++++++++++++++++ 7 files changed, 408 insertions(+), 2 deletions(-) create mode 100644 src/identity/ucp-jwks.ts create mode 100644 tests/identity/ucp-signing.test.ts diff --git a/README.md b/README.md index 8e7ff9f..b6c57a9 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ npm install hono mppx @x402/core @x402/evm @solana/mpp @solana/kit stripe # wh |---|---| | `/identity/{hono,express,fastify,nextjs,web}` | Trust gate middleware: KYC, sanctions, age, jurisdiction. `agentscoreGate(...)`, `getAgentScoreData(c)`, `captureWallet(...)`, `verifyWalletSignerMatch(...)`. Plus shared denial helpers: `denialReasonStatus`, `denialReasonToBody`, `buildSignerMismatchBody`, `buildContactSupportNextSteps`, `verificationAgentInstructions`, `isFixableDenial`, `FIXABLE_DENIAL_REASONS`. | | `/payment` | `networks`, `USDC`, `rails` registries; `paymentDirective`, `buildPaymentDirective`, `wwwAuthenticateHeader`, `paymentRequiredHeader`, `aliasAmountFields` (v1↔v2 amount field shim — emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlementOverrideHeader`, `dispatchSettlementByNetwork`, `extractPaymentSigner` (returns `{address, network}`); `createX402Server`, `createMppxServer`; drop-in x402 helpers: `validateX402NetworkConfig` (boot-time guard), `verifyX402Request` (parse + validate inbound X-Payment), `processX402Settle` (verify-then-settle with one call), `classifyX402SettleResult` (maps the tagged settle result to a recommended HTTP status / code / nextSteps so merchants get a controlled envelope without coupling to facilitator-specific error text). | -| `/discovery` | `isDiscoveryProbeRequest`, `buildDiscoveryProbeResponse` (with optional `x402Sample` for x402-aware crawlers — `awal x402 details` etc.), `sampleX402AcceptForNetwork` (USDC sample-accept builder for known CAIP-2 networks), `buildWellKnownMpp`, `buildLlmsTxt` + `llmsTxtIdentitySection` + `llmsTxtPaymentSection` (compact + verbose modes), `buildSkillMd` (Claude-Skill-compatible `/skill.md` agent-discovery manifest — strictly agent-facing data only, no internal posture), `agentscoreOpenApiSnippets`, `createBazaarDiscovery`, `noindexNonDiscoveryPaths` (Hono middleware that emits `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces — defaults cover `/openapi.json`, `/llms.txt`, `/skill.md`, `/.well-known/{mpp.json,agent-card.json,ucp}`, `/favicon.{png,ico}`; pure helpers `isDiscoveryPath` + `defaultDiscoveryPaths` for non-Hono frameworks). | +| `/discovery` | `isDiscoveryProbeRequest`, `buildDiscoveryProbeResponse` (with optional `x402Sample` for x402-aware crawlers — `awal x402 details` etc.), `sampleX402AcceptForNetwork` (USDC sample-accept builder for known CAIP-2 networks), `buildWellKnownMpp`, `buildLlmsTxt` + `llmsTxtIdentitySection` + `llmsTxtPaymentSection` (compact + verbose modes), `buildSkillMd` (Claude-Skill-compatible `/skill.md` agent-discovery manifest — strictly agent-facing data only, no internal posture), `agentscoreOpenApiSnippets`, `createBazaarDiscovery`, `noindexNonDiscoveryPaths` (Hono middleware that emits `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces — defaults cover `/openapi.json`, `/llms.txt`, `/skill.md`, `/.well-known/{mpp.json,agent-card.json,ucp,jwks.json}`, `/favicon.{png,ico}`; pure helpers `isDiscoveryPath` + `defaultDiscoveryPaths` for non-Hono frameworks). | | `/challenge` | `build402Body`, `buildAcceptedMethods`, `buildIdentityMetadata`, `buildHowToPay`, `buildAgentInstructions` (auto-emits per-rail `compatible_clients` — smoke-verified CLIs the agent should use; vendor override supported), `buildPricingBlock`, `firstEncounterAgentMemory`, `OrderReceipt`; `respond402` — drop-in 402 emit that preserves mppx's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `buildValidationError` — structured 4xx body builder (`{error: {code, message}, required_fields?, example_body?, next_steps?, ...extra}`) so vendors compose body shapes by name instead of inlining at every validation site. | | `/stripe-multichain` | `createMultichainPaymentIntent`, `getDepositAddress`, `simulateCryptoDeposit`, `createMppxStripe`; `createPiCache` (TTL'd PI / deposit-address cache, Redis-backed when `redisUrl` set, in-memory otherwise), `simulateDepositIfTestMode` (gates on `sk_test_` and looks up the PI for you), `STRIPE_TEST_TX_HASH_SUCCESS` / `STRIPE_TEST_TX_HASH_FAILED` constants. Peer dep on `stripe`. | | `/api` | Everything from `@agent-score/sdk` re-exported in one place: `AgentScore` + `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` + `isAgentScoreTestAddress`. **Don't add `@agent-score/sdk` as a separate dep** — the two can drift versions and cause subtle type mismatches. | @@ -199,6 +199,16 @@ const card = buildA2AAgentCard({ name, url, capabilities, data: assess }); const profile = buildUCPProfile({ name, services, payment_handlers, signing_keys, data: assess }); ``` +UCP §6 trust-mode requires profiles to carry a JWS signature backed by a JWKS at `/.well-known/jwks.json`. Sign + verify via the optional `jose` peer dep: + +```typescript +import { buildJWKSResponse, generateUCPSigningKey, signUCPProfile, verifyUCPProfile } from "@agent-score/commerce"; + +const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: "merchant-2026-05" }); +const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: publicJWK.kid, alg: "EdDSA" }); +const jwks = buildJWKSResponse([publicJWK]); +``` + ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol with no identity-publishing surface — ACP merchants integrate via the existing `build402Body` + `buildPaymentHeaders` + Stripe SPT rail. ### Stripe multichain (peer dep on `stripe`) diff --git a/bun.lock b/bun.lock index d715358..2620182 100644 --- a/bun.lock +++ b/bun.lock @@ -25,6 +25,7 @@ "express": "^5.2.1", "fastify": "^5.8.5", "hono": "^4.12.18", + "jose": "^5.9.0", "lefthook": "^2.1.6", "mppx": "^0.6.15", "tsup": "^8.5.1", @@ -38,6 +39,7 @@ "express": ">=4.0.0", "fastify": ">=4.0.0", "hono": ">=4.0.0", + "jose": ">=5.9.0", "stripe": ">=17.0.0", }, "optionalPeers": [ @@ -46,6 +48,7 @@ "express", "fastify", "hono", + "jose", "stripe", ], }, diff --git a/package.json b/package.json index d724789..f8b2dd0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agent-score/commerce", - "version": "1.3.3", + "version": "1.3.4", "description": "Agent commerce SDK — identity middleware (Hono, Express, Fastify, Next.js, Web Fetch) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agent commerce.", "main": "./dist/index.js", "module": "./dist/index.mjs", @@ -131,6 +131,7 @@ "express": ">=4.0.0", "fastify": ">=4.0.0", "hono": ">=4.0.0", + "jose": ">=5.9.0", "stripe": ">=17.0.0" }, "peerDependenciesMeta": { @@ -149,6 +150,9 @@ "hono": { "optional": true }, + "jose": { + "optional": true + }, "stripe": { "optional": true } @@ -171,6 +175,7 @@ "express": "^5.2.1", "fastify": "^5.8.5", "hono": "^4.12.18", + "jose": "^5.9.0", "lefthook": "^2.1.6", "mppx": "^0.6.15", "tsup": "^8.5.1", diff --git a/src/discovery/robots_tag.ts b/src/discovery/robots_tag.ts index 02045ed..e93724a 100644 --- a/src/discovery/robots_tag.ts +++ b/src/discovery/robots_tag.ts @@ -18,6 +18,7 @@ export const defaultDiscoveryPaths: ReadonlySet = new Set([ '/.well-known/x402', '/.well-known/agent-card.json', '/.well-known/ucp', + '/.well-known/jwks.json', '/favicon.png', '/favicon.ico', ]); diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts new file mode 100644 index 0000000..adff3f9 --- /dev/null +++ b/src/identity/ucp-jwks.ts @@ -0,0 +1,244 @@ +/** + * UCP profile signing helpers (JWKS + JWS). + * + * UCP §6 (https://ucp.dev/latest/specification/signatures/) requires that profiles + * published at `/.well-known/ucp` carry a JWKS-backed signature for trust-mode clients + * (Google AI Mode, Gemini commerce, future ChatGPT app shells). Without a signature, + * trust-mode clients reject the profile. + * + * This module provides: + * - `generateUCPSigningKey()` — generate an Ed25519 keypair for signing + * - `signUCPProfile()` — sign a UCP profile body, returning a JWS-attached envelope + * - `verifyUCPProfile()` — verify a signed profile against a JWKS + * - `buildJWKSResponse()` — assemble a JWKS document for `/.well-known/jwks.json` + * + * Implementation rides on `jose` (peer-dep, optional). Merchants who don't sign their + * profile (development) skip this module entirely; the unsigned `buildUCPProfile()` + * path still works. + * + * Why Ed25519: smaller signatures (64 bytes vs 256+ for RSA), faster verification, no + * curve-parameter ceremony. UCP also accepts ES256 (P-256 ECDSA) — pass `alg: 'ES256'` + * to `signUCPProfile()` if your existing payment signing key is P-256. + */ + +import type { UCPProfile, UCPSigningKey } from './ucp'; + +/** Output of `generateUCPSigningKey()`. The private key is what you sign with; the + * public JWK is what you publish at `/.well-known/jwks.json` and reference in the + * UCP profile's `signing_keys[]`. + */ +export interface GeneratedUCPKey { + /** Private key (KeyLike, opaque) — pass to `signUCPProfile()`. Never publish. */ + privateKey: unknown; + /** Public key as JWK — publish at `/.well-known/jwks.json` and inline in UCP `signing_keys[]`. */ + publicJWK: UCPSigningKey; +} + +/** A JWKS document — `{ keys: [...] }` per RFC 7517. Serve at `/.well-known/jwks.json`. */ +export interface JWKSResponse { + keys: UCPSigningKey[]; +} + +/** Options for `signUCPProfile()`. */ +export interface SignUCPProfileOptions { + /** Private signing key — opaque KeyLike from `generateUCPSigningKey()` or `importJWK()`. */ + signingKey: unknown; + /** Key ID (must match a `kid` in the profile's `signing_keys[]`). */ + kid: string; + /** Signing algorithm — `EdDSA` (default) or `ES256`. */ + alg?: 'EdDSA' | 'ES256'; +} + +/** A signed UCP profile envelope. Same shape as `UCPProfile` plus the `signature` field + * carrying the JWS Compact Serialization over the canonicalized profile body. */ +export interface SignedUCPProfile extends UCPProfile { + /** JWS Compact Serialization (`
..`) over the profile body + * with `signature` removed and keys sorted. Verifiers reconstruct the canonical body + * and validate against the JWK identified by `kid` in the JWS protected header. */ + signature: string; +} + +const JOSE_INSTALL_HINT = 'Install the optional peer dependency: `npm install jose@^5` (or `bun add jose`).'; + +async function loadJose(): Promise { + try { + return await import('jose'); + } catch (err) { + throw new Error( + `UCP signing requires the \`jose\` library, which is an optional peer dependency. ${JOSE_INSTALL_HINT}\nOriginal error: ${err instanceof Error ? err.message : String(err)}`, + ); + } +} + +/** + * Canonicalize a UCP profile for signing. Removes the `signature` field (if present), + * sorts keys deterministically, and returns the JSON string. Both signer and verifier + * compute the same bytes. + * + * Implementation note: UCP §6.2 specifies "the JSON-serialized profile body, with + * `signature` removed and keys ordered lexicographically at every nesting level." This + * is JCS-style canonicalization without the full RFC 8785 numeric handling — UCP + * profiles don't contain floats so the simpler key-sort is sufficient. + */ +function canonicalizeProfile(profile: UCPProfile): string { + const stripped = { ...profile } as Record; + delete stripped.signature; + return stableStringify(stripped); +} + +/** Deterministic JSON.stringify with lexicographic key ordering at every level. */ +function stableStringify(value: unknown): string { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`; + const obj = value as Record; + const keys = Object.keys(obj).sort(); + const pairs = keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`); + return `{${pairs.join(',')}}`; +} + +/** + * Generate a fresh Ed25519 (default) or ES256 keypair for signing UCP profiles. + * + * The `privateKey` is an opaque KeyLike — store it server-side and pass to + * `signUCPProfile()`. Never log or transmit the private key. + * + * The `publicJWK` is what you publish at `/.well-known/jwks.json` and inline in the + * UCP profile's `signing_keys[]` array. + * + * Example: + * ```ts + * import { generateUCPSigningKey } from '@agent-score/commerce/identity/ucp-jwks'; + * + * const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'merchant-2026-05' }); + * // Persist privateKey securely (env var, KMS, secret manager). + * // Publish publicJWK at /.well-known/jwks.json and reference it in your UCP profile. + * ``` + */ +export async function generateUCPSigningKey(opts: { + /** Key ID (kid). Must be unique per key; you'll reference this in the UCP profile's `signing_keys[]`. */ + kid: string; + /** Signing algorithm. Default `EdDSA`. */ + alg?: 'EdDSA' | 'ES256'; +}): Promise { + const jose = await loadJose(); + const alg = opts.alg ?? 'EdDSA'; + const { privateKey, publicKey } = await jose.generateKeyPair(alg, { extractable: true }); + const exportedJwk = await jose.exportJWK(publicKey); + + const publicJWK: UCPSigningKey = { + kid: opts.kid, + alg, + use: 'sig', + ...exportedJwk, + } as UCPSigningKey; + + return { privateKey, publicJWK }; +} + +/** + * Sign a UCP profile, returning a new envelope with the JWS attached as `signature`. + * + * The signature covers the canonicalized profile body (everything except `signature` + * itself, with keys sorted at every level). Trust-mode UCP verifiers reconstruct the + * canonical body, look up the key referenced by the JWS header's `kid`, and validate. + * + * The profile's `signing_keys[]` MUST already include a JWK with the matching `kid` + * — otherwise verifiers can't find the public key. Add the `publicJWK` from + * `generateUCPSigningKey()` to your `signing_keys[]` before calling this. + * + * Example: + * ```ts + * const profile = buildUCPProfile({ ..., signing_keys: [publicJWK] }); + * const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'merchant-2026-05' }); + * c.json(signed); + * ``` + */ +export async function signUCPProfile( + profile: UCPProfile, + opts: SignUCPProfileOptions, +): Promise { + const jose = await loadJose(); + const alg = opts.alg ?? 'EdDSA'; + + const canonicalBody = canonicalizeProfile(profile); + const payloadBytes = new TextEncoder().encode(canonicalBody); + + const signature = await new jose.CompactSign(payloadBytes) + .setProtectedHeader({ alg, kid: opts.kid, typ: 'ucp-profile+jws' }) + .sign(opts.signingKey as Parameters[0]); + + return { ...profile, signature }; +} + +/** + * Verify a signed UCP profile against a JWKS. Returns `true` when the JWS validates + * against a matching key in `jwks`; throws on signature mismatch, missing key, or + * canonicalization drift. + * + * Round-trip helper for tests and for cross-merchant verification flows. Trust-mode + * UCP clients use the same algorithm. + * + * Example: + * ```ts + * const ok = await verifyUCPProfile(signedProfile, { keys: [publicJWK] }); + * ``` + */ +export async function verifyUCPProfile( + profile: SignedUCPProfile, + jwks: JWKSResponse, +): Promise { + const jose = await loadJose(); + + const stripped = { ...profile } as Partial; + const sig = stripped.signature; + delete stripped.signature; + if (!sig) throw new Error('UCP profile has no `signature` field; expected JWS Compact Serialization.'); + + const canonicalBody = canonicalizeProfile(stripped as UCPProfile); + const expectedPayload = new TextEncoder().encode(canonicalBody); + + const { payload: signedPayload } = await jose.compactVerify(sig, async (header) => { + const kid = header.kid; + if (!kid) throw new Error('UCP signature header missing `kid`.'); + const jwk = jwks.keys.find((k) => (k as Record).kid === kid); + if (!jwk) throw new Error(`No JWK in JWKS matching kid=${kid}.`); + return jose.importJWK(jwk as Parameters[0], header.alg); + }); + + // Compare the bytes that were actually signed against the canonical body of the + // profile we received. `compactVerify` validates the JWS against the bytes embedded + // in the JWS payload segment — but the profile body could have been swapped after + // signing while the JWS stayed unchanged. Body-vs-payload comparison closes that + // gap. + if (!constantTimeEqual(signedPayload, expectedPayload)) { + throw new Error('UCP profile body does not match the signed payload (tampered or non-canonical).'); + } + + return true; +} + +/** Constant-time byte comparison to avoid leaking length / position info on mismatch. */ +function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean { + if (a.length !== b.length) return false; + let diff = 0; + for (let i = 0; i < a.length; i += 1) { + diff |= a[i] ^ b[i]; + } + return diff === 0; +} + +/** + * Build a JWKS document for `/.well-known/jwks.json`. + * + * Example: + * ```ts + * import { buildJWKSResponse } from '@agent-score/commerce/identity/ucp-jwks'; + * + * app.get('/.well-known/jwks.json', (c) => + * c.json(buildJWKSResponse([publicJWK])) + * ); + * ``` + */ +export function buildJWKSResponse(keys: UCPSigningKey[]): JWKSResponse { + return { keys }; +} diff --git a/src/index.ts b/src/index.ts index 78b7698..904bf60 100644 --- a/src/index.ts +++ b/src/index.ts @@ -40,6 +40,16 @@ export { type UCPService, type UCPSigningKey, } from './identity/ucp'; +export { + buildJWKSResponse, + generateUCPSigningKey, + type GeneratedUCPKey, + type JWKSResponse, + type SignUCPProfileOptions, + type SignedUCPProfile, + signUCPProfile, + verifyUCPProfile, +} from './identity/ucp-jwks'; export { type EnforcementMode, type GateResult, diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts new file mode 100644 index 0000000..af1ede7 --- /dev/null +++ b/tests/identity/ucp-signing.test.ts @@ -0,0 +1,133 @@ +import { describe, expect, it } from 'vitest'; +import { buildUCPProfile } from '../../src/identity/ucp'; +import { + buildJWKSResponse, + generateUCPSigningKey, + signUCPProfile, + verifyUCPProfile, +} from '../../src/identity/ucp-jwks'; + +const baseInput = { + name: 'Test Merchant', + services: [{ type: 'rest', url: 'https://agents.example.com' }], + payment_handlers: [ + { name: 'tempo', config: { recipient: '0x1234' } }, + ], +}; + +describe('UCP signing — generateUCPSigningKey', () => { + it('generates an Ed25519 keypair by default', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'test-key-1' }); + expect(privateKey).toBeDefined(); + expect(publicJWK.kid).toBe('test-key-1'); + expect(publicJWK.alg).toBe('EdDSA'); + expect(publicJWK.use).toBe('sig'); + expect(publicJWK.kty).toBe('OKP'); + expect(publicJWK.crv).toBe('Ed25519'); + expect(typeof (publicJWK as Record).x).toBe('string'); + }); + + it('generates an ES256 keypair when alg=ES256', async () => { + const { publicJWK } = await generateUCPSigningKey({ kid: 'test-es256', alg: 'ES256' }); + expect(publicJWK.alg).toBe('ES256'); + expect(publicJWK.kty).toBe('EC'); + expect(publicJWK.crv).toBe('P-256'); + expect(typeof (publicJWK as Record).x).toBe('string'); + expect(typeof (publicJWK as Record).y).toBe('string'); + }); + + it('produces a different kid + key material on each call', async () => { + const a = await generateUCPSigningKey({ kid: 'a' }); + const b = await generateUCPSigningKey({ kid: 'b' }); + expect(a.publicJWK.kid).toBe('a'); + expect(b.publicJWK.kid).toBe('b'); + expect((a.publicJWK as Record).x).not.toBe((b.publicJWK as Record).x); + }); +}); + +describe('UCP signing — signUCPProfile / verifyUCPProfile round-trip', () => { + it('signs an Ed25519-keyed profile and verifies against the matching JWKS', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'merchant-2026-05' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'merchant-2026-05' }); + + expect(signed.signature).toBeDefined(); + expect(typeof signed.signature).toBe('string'); + expect(signed.signature.split('.')).toHaveLength(3); // JWS Compact has 3 segments + + const ok = await verifyUCPProfile(signed, buildJWKSResponse([publicJWK])); + expect(ok).toBe(true); + }); + + it('signs an ES256-keyed profile and verifies', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'es256-key', alg: 'ES256' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'es256-key', alg: 'ES256' }); + + const ok = await verifyUCPProfile(signed, buildJWKSResponse([publicJWK])); + expect(ok).toBe(true); + }); + + it('verifies against a multi-key JWKS (selects by kid)', async () => { + const oldKey = await generateUCPSigningKey({ kid: 'old-key' }); + const newKey = await generateUCPSigningKey({ kid: 'new-key' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [oldKey.publicJWK, newKey.publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: newKey.privateKey, kid: 'new-key' }); + + const ok = await verifyUCPProfile(signed, buildJWKSResponse([oldKey.publicJWK, newKey.publicJWK])); + expect(ok).toBe(true); + }); + + it('rejects a tampered profile body', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + + const tampered = { ...signed, name: 'Different Name' }; + await expect(verifyUCPProfile(tampered, buildJWKSResponse([publicJWK]))).rejects.toThrow(); + }); + + it('rejects when JWKS does not contain the signing key', async () => { + const signer = await generateUCPSigningKey({ kid: 'signer' }); + const other = await generateUCPSigningKey({ kid: 'other' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [signer.publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: signer.privateKey, kid: 'signer' }); + + await expect(verifyUCPProfile(signed, buildJWKSResponse([other.publicJWK]))).rejects.toThrow(/No JWK in JWKS/); + }); + + it('rejects when profile has no signature field', async () => { + const { publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + await expect( + verifyUCPProfile(profile as unknown as Awaited>, buildJWKSResponse([publicJWK])), + ).rejects.toThrow(/no `signature` field/); + }); +}); + +describe('UCP signing — canonicalization', () => { + it('signs the profile such that key-order in the JSON does not affect verification', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profileA = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profileA, { signingKey: privateKey, kid: 'k' }); + + // Re-construct the same profile with keys in different insertion order; should + // still verify because canonicalization sorts keys deterministically. + const reordered = JSON.parse(JSON.stringify(signed)); + const ok = await verifyUCPProfile(reordered, buildJWKSResponse([publicJWK])); + expect(ok).toBe(true); + }); +}); + +describe('UCP signing — buildJWKSResponse', () => { + it('wraps keys in a `{ keys: [...] }` document', () => { + const k1 = { kid: 'a', kty: 'OKP', crv: 'Ed25519', x: 'xxx', use: 'sig', alg: 'EdDSA' }; + const k2 = { kid: 'b', kty: 'EC', crv: 'P-256', x: 'xxx', y: 'yyy', use: 'sig', alg: 'ES256' }; + const jwks = buildJWKSResponse([k1, k2]); + expect(jwks).toEqual({ keys: [k1, k2] }); + }); + + it('handles empty key set', () => { + expect(buildJWKSResponse([])).toEqual({ keys: [] }); + }); +}); From 7748db39008bc32620c27d2d200c0d7baa89ca0e Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 11:12:35 -0700 Subject: [PATCH 02/35] chore(deps): bump deps + override fast-uri to fix GHSA-q3j6-qgpj-74h6 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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) --- bun.lock | 21 ++++++++++++++++----- package.json | 7 ++++--- 2 files changed, 20 insertions(+), 8 deletions(-) diff --git a/bun.lock b/bun.lock index 2620182..2ec7788 100644 --- a/bun.lock +++ b/bun.lock @@ -13,7 +13,7 @@ "@solana/kit": "^6.9.0", "@solana/mpp": "^0.5.2", "@types/express": "^5.0.6", - "@types/node": "^25.6.0", + "@types/node": "^25.6.2", "@vitest/coverage-v8": "^4.1.5", "@x402/core": "^2.11.0", "@x402/evm": "^2.11.0", @@ -27,7 +27,7 @@ "hono": "^4.12.18", "jose": "^5.9.0", "lefthook": "^2.1.6", - "mppx": "^0.6.15", + "mppx": "^0.6.16", "tsup": "^8.5.1", "typescript": "^6.0.3", "typescript-eslint": "^8.59.2", @@ -55,6 +55,7 @@ }, "overrides": { "axios": "^1.15.0", + "fast-uri": "^3.1.1", }, "packages": { "@adraffy/ens-normalize": ["@adraffy/ens-normalize@1.11.1", "", {}, "sha512-nhCBV3quEgesuf7c7KYfperqSS14T8bYuvJ8PcLJp6znkZpFc0AuW4qBtr8eKVyPPe/8RSr7sglCWPU5eaxwKQ=="], @@ -409,7 +410,7 @@ "@types/json5": ["@types/json5@0.0.29", "", {}, "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ=="], - "@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@types/node": ["@types/node@25.6.2", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-sokuT28dxf9JT5Kady1fsXOvI4HVpjZa95NKT5y9PNTIrs2AsobR4GFAA90ZG8M+nxVRLysCXsVj6eGC7Vbrlw=="], "@types/qs": ["@types/qs@6.15.0", "", {}, "sha512-JawvT8iBVWpzTrz3EGw9BTQFg3BQNmwERdKE22vlTxawwtbyUSlMppvZYKLZzB5zgACXdXxbD3m1bXaMqP/9ow=="], @@ -669,7 +670,7 @@ "fast-querystring": ["fast-querystring@1.1.2", "", { "dependencies": { "fast-decode-uri-component": "^1.0.1" } }, "sha512-g6KuKWmFXc0fID8WWH0jit4g0AGBoJhCkJMb1RmbsSEUNvQ+ZC8D6CUZ+GtF8nMzSPXnhiePyyqqipzNNEnHjg=="], - "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + "fast-uri": ["fast-uri@3.1.2", "", {}, "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ=="], "fastify": ["fastify@5.8.5", "", { "dependencies": { "@fastify/ajv-compiler": "^4.0.5", "@fastify/error": "^4.0.0", "@fastify/fast-json-stringify-compiler": "^5.0.0", "@fastify/proxy-addr": "^5.0.0", "abstract-logging": "^2.0.1", "avvio": "^9.0.0", "fast-json-stringify": "^6.0.0", "find-my-way": "^9.0.0", "light-my-request": "^6.0.0", "pino": "^9.14.0 || ^10.1.0", "process-warning": "^5.0.0", "rfdc": "^1.3.1", "secure-json-parse": "^4.0.0", "semver": "^7.6.0", "toad-cache": "^3.7.0" } }, "sha512-Yqptv59pQzPgQUSIm87hMqHJmdkb1+GPxdE6vW6FRyVE9G86mt7rOghitiU4JHRaTyDUk9pfeKmDeu70lAwM4Q=="], @@ -931,7 +932,7 @@ "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], - "mppx": ["mppx@0.6.15", "", { "dependencies": { "incur": "^0.4.5", "ox": "0.14.20", "zod": "^4.3.6" }, "peerDependencies": { "@modelcontextprotocol/sdk": ">=1.25.0", "elysia": ">=1", "express": ">=5", "hono": ">=4.12.14", "viem": ">=2.47.5" }, "optionalPeers": ["@modelcontextprotocol/sdk", "elysia", "express", "hono"], "bin": { "mppx": "dist/bin.js", "mppx.src": "src/bin.ts" } }, "sha512-5p0XtrkKGW158rPAMGIuS8Cmowk4IJVoWEiHdxism7iV+8YjJdxNJG5etMBeEnh52wPRcrs9f18ENDGbwy9/EA=="], + "mppx": ["mppx@0.6.16", "", { "dependencies": { "incur": "^0.4.5", "ox": "0.14.20", "zod": "^4.3.6" }, "peerDependencies": { "@modelcontextprotocol/sdk": ">=1.25.0", "elysia": ">=1", "express": ">=5", "hono": ">=4.12.16", "viem": ">=2.47.5" }, "optionalPeers": ["@modelcontextprotocol/sdk", "elysia", "express", "hono"], "bin": { "mppx": "dist/bin.js", "mppx.src": "src/bin.ts" } }, "sha512-gByr5oM0vfbJqh3S7e3WLLjE3pjniZShv2kyFdzl3aav1ejH8rkjVqKMJ92nuQWkDw+2QZgzYD5PcbnqzUPZjA=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -1251,6 +1252,16 @@ "@solana/rpc-transport-http/undici-types": ["undici-types@8.2.0", "", {}, "sha512-uciYZ5yCmf+QJb18kJw10HjquzM7K0z992vWcI+84KeBpTfXT4hfgfGJ5DQbf/mCBPACofkrjvqgcjZfuujjFA=="], + "@types/body-parser/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "@types/connect/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "@types/express-serve-static-core/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "@types/send/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + + "@types/serve-static/@types/node": ["@types/node@25.6.0", "", { "dependencies": { "undici-types": "~7.19.0" } }, "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ=="], + "@typescript-eslint/eslint-plugin/@typescript-eslint/utils": ["@typescript-eslint/utils@8.59.0", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", "@typescript-eslint/scope-manager": "8.59.0", "@typescript-eslint/types": "8.59.0", "@typescript-eslint/typescript-estree": "8.59.0" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "sha512-I1R/K7V07XsMJ12Oaxg/O9GfrysGTmCRhvZJBv0RE0NcULMzjqVpR5kRRQjHsz3J/bElU7HwCO7zkqL+MSUz+g=="], "@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], diff --git a/package.json b/package.json index f8b2dd0..31f276f 100644 --- a/package.json +++ b/package.json @@ -123,7 +123,8 @@ "@agent-score/sdk": "^2.2.0" }, "overrides": { - "axios": "^1.15.0" + "axios": "^1.15.0", + "fast-uri": "^3.1.1" }, "peerDependencies": { "@solana/kit": ">=6.5.0", @@ -163,7 +164,7 @@ "@solana/kit": "^6.9.0", "@solana/mpp": "^0.5.2", "@types/express": "^5.0.6", - "@types/node": "^25.6.0", + "@types/node": "^25.6.2", "@vitest/coverage-v8": "^4.1.5", "@x402/core": "^2.11.0", "@x402/evm": "^2.11.0", @@ -177,7 +178,7 @@ "hono": "^4.12.18", "jose": "^5.9.0", "lefthook": "^2.1.6", - "mppx": "^0.6.15", + "mppx": "^0.6.16", "tsup": "^8.5.1", "typescript": "^6.0.3", "typescript-eslint": "^8.59.2", From a58fd3acfebde4fbc277ef04d55fddfa2b1d12e6 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 13:54:02 -0700 Subject: [PATCH 03/35] hardening(identity): UCP signing security + ergonomics fixes MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 15 +- examples/signed-ucp-merchant.ts | 119 ++++++++++++++ src/identity/ucp-jwks.ts | 93 +++++++++-- src/index.ts | 1 + .../fixtures/cross-lang/node-es256-rails.json | 63 +++++++ .../fixtures/cross-lang/node-extras-int.json | 49 ++++++ tests/fixtures/cross-lang/node-minimal.json | 41 +++++ tests/fixtures/cross-lang/py-es256-rails.json | 63 +++++++ tests/fixtures/cross-lang/py-extras-int.json | 49 ++++++ tests/fixtures/cross-lang/py-minimal.json | 41 +++++ tests/identity/cross-lang.test.ts | 47 ++++++ tests/identity/ucp-signing.test.ts | 155 ++++++++++++++++++ 12 files changed, 723 insertions(+), 13 deletions(-) create mode 100644 examples/signed-ucp-merchant.ts create mode 100644 tests/fixtures/cross-lang/node-es256-rails.json create mode 100644 tests/fixtures/cross-lang/node-extras-int.json create mode 100644 tests/fixtures/cross-lang/node-minimal.json create mode 100644 tests/fixtures/cross-lang/py-es256-rails.json create mode 100644 tests/fixtures/cross-lang/py-extras-int.json create mode 100644 tests/fixtures/cross-lang/py-minimal.json create mode 100644 tests/identity/cross-lang.test.ts diff --git a/README.md b/README.md index b6c57a9..2ca96a3 100644 --- a/README.md +++ b/README.md @@ -199,16 +199,27 @@ const card = buildA2AAgentCard({ name, url, capabilities, data: assess }); const profile = buildUCPProfile({ name, services, payment_handlers, signing_keys, data: assess }); ``` -UCP §6 trust-mode requires profiles to carry a JWS signature backed by a JWKS at `/.well-known/jwks.json`. Sign + verify via the optional `jose` peer dep: +UCP §6 trust-mode requires profiles to carry a JWS signature backed by a JWKS at `/.well-known/jwks.json`. Sign + verify via the optional `jose` peer dep (tested against jose v5.x; pin `jose@^5`): ```typescript -import { buildJWKSResponse, generateUCPSigningKey, signUCPProfile, verifyUCPProfile } from "@agent-score/commerce"; +import { buildJWKSResponse, generateUCPSigningKey, signUCPProfile, verifyUCPProfile, UCPVerificationError } from "@agent-score/commerce"; const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: "merchant-2026-05" }); +const profile = buildUCPProfile({ name, services, payment_handlers, signing_keys: [publicJWK] }); const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: publicJWK.kid, alg: "EdDSA" }); const jwks = buildJWKSResponse([publicJWK]); ``` +`verifyUCPProfile` enforces the JWS protected header `typ: "ucp-profile+jws"`, restricts `alg` to `EdDSA` / `ES256`, requires a `kid`, rejects duplicate kids in the JWKS, and compares the canonical body bytes against the JWS payload to catch swap-after-sign tampering. Failures throw `UCPVerificationError` with a discriminated `code` (`no_signature` / `missing_kid` / `kid_not_found` / `duplicate_kid` / `unsupported_alg` / `wrong_typ` / `signature_invalid` / `body_mismatch` / `malformed_jws`). + +`signUCPProfile` rejects profiles containing non-integer `Number` values (cross-language float canonicalization is not stable; use decimal strings for monetary or fractional fields). + +**HSM / KMS-backed signing.** `signingKey` accepts any `jose.KeyLike` — including remote signers wrapped via `createPrivateKey` from `node:crypto` for AWS KMS / GCP KMS asymmetric keys. The `signing_key` never has to leave the HSM. + +**Key rotation.** Mint a new key with a new `kid`, add the public JWK to your JWKS endpoint alongside the old one, then sign new profiles with the new key. Verifiers fetching the JWKS pick up both; any in-flight envelopes signed by the old key still verify until you remove that JWK from the JWKS. Drop the old JWK once your verifier-side cache TTL has elapsed. + +**Inline JWK in the profile vs separate JWKS endpoint.** UCP §6 mandates the separate `/.well-known/jwks.json` endpoint as the canonical trust source. The profile's `signing_keys[]` is informational; verifiers MUST resolve the kid against the JWKS (not the embedded copy), to prevent a swap-after-sign attack where a hostile actor replaces the inline key with their own. + ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol with no identity-publishing surface — ACP merchants integrate via the existing `build402Body` + `buildPaymentHeaders` + Stripe SPT rail. ### Stripe multichain (peer dep on `stripe`) diff --git a/examples/signed-ucp-merchant.ts b/examples/signed-ucp-merchant.ts new file mode 100644 index 0000000..2f937cc --- /dev/null +++ b/examples/signed-ucp-merchant.ts @@ -0,0 +1,119 @@ +/** + * Signed UCP profile example — `/.well-known/ucp` + `/.well-known/jwks.json`. + * + * UCP §6 trust-mode verification (Google AI Mode, Gemini commerce) requires the + * profile to carry a JWS signature and the merchant to publish a JWKS endpoint + * verifiers can fetch the public key from. This example wires both routes against + * a persistent signing key (env-loaded for prod, ephemeral for dev). + * + * Run: `bun examples/signed-ucp-merchant.ts` (port 3010). + * + * Production checklist: + * - Set `UCP_SIGNING_KEY_JWK_PRIVATE` to a JSON-encoded private JWK (mint via + * `generateUCPSigningKey()` once, persist in your secret manager). + * - The kid in the env JWK MUST match what verifiers will see in your published + * profile — pick a stable name like `merchant-2026-05`. + * - Configure `Cache-Control: public, max-age=300` (or longer) on /.well-known/jwks.json + * so verifiers don't hammer the endpoint. + * - Rotate by minting a new key + new kid, publishing both in the JWKS, signing + * new profiles with the new key, then dropping the old JWK after your verifier + * cache TTL expires. + */ + +import { + buildJWKSResponse, + buildUCPProfile, + generateUCPSigningKey, + signUCPProfile, + type GeneratedUCPKey, + UCPVerificationError, + verifyUCPProfile, +} from '@agent-score/commerce'; +import { Hono } from 'hono'; +import { exportJWK, importJWK, type CryptoKey, type JWK } from 'jose'; + +const KID = process.env.UCP_SIGNING_KEY_KID ?? 'merchant-2026-05'; +const ALG = (process.env.UCP_SIGNING_KEY_ALG ?? 'EdDSA') as 'EdDSA' | 'ES256'; + +let cached: Promise | null = null; + +function loadSigningKey(): Promise { + // Cache the in-flight Promise (not the resolved value) so two concurrent + // first-callers can't independently generate different keys. + if (cached) return cached; + cached = (async () => { + const envJwk = process.env.UCP_SIGNING_KEY_JWK_PRIVATE; + if (envJwk) { + let jwk: JWK; + try { + jwk = JSON.parse(envJwk) as JWK; + } catch (err) { + throw new Error( + `Failed to parse UCP_SIGNING_KEY_JWK_PRIVATE as JSON: ${err instanceof Error ? err.message : String(err)}`, + ); + } + const privateKey = (await importJWK(jwk, ALG)) as CryptoKey; + // Re-export to derive a clean public-only JWK (drops `d` + any RSA private fields). + const publicJWK = (await exportJWK(privateKey)) as JWK; + publicJWK.kid = jwk.kid ?? KID; + publicJWK.alg = ALG; + publicJWK.use = 'sig'; + delete (publicJWK as Record).d; + return { privateKey, publicJWK } as GeneratedUCPKey; + } + console.warn('[ucp] UCP_SIGNING_KEY_JWK_PRIVATE not set — generating ephemeral key. Verifier caches will break across restarts.'); + return generateUCPSigningKey({ kid: KID, alg: ALG }); + })(); + return cached; +} + +const app = new Hono(); + +app.get('/.well-known/ucp', async (c) => { + const key = await loadSigningKey(); + const profile = buildUCPProfile({ + name: 'My Agent Service', + services: [{ type: 'rest', url: 'https://agents.example.com' }], + payment_handlers: [{ name: 'tempo', config: { recipient: '0xfeedface' } }], + signing_keys: [key.publicJWK], + }); + const signed = await signUCPProfile(profile, { + signingKey: key.privateKey, + kid: key.publicJWK.kid as string, + alg: ALG, + }); + c.header('Cache-Control', 'public, max-age=60'); + return c.json(signed); +}); + +app.get('/.well-known/jwks.json', async (c) => { + const key = await loadSigningKey(); + c.header('Cache-Control', 'public, max-age=300'); + c.header('Content-Type', 'application/jwk-set+json'); + return c.json(buildJWKSResponse([key.publicJWK])); +}); + +// Self-smoke: confirm sign + verify round-trip locally. +app.get('/_selftest/ucp', async (c) => { + const profileRes = await app.request('/.well-known/ucp'); + const jwksRes = await app.request('/.well-known/jwks.json'); + const profile = await profileRes.json() as Awaited>; + const jwks = await jwksRes.json() as Parameters[1]; + try { + await verifyUCPProfile(profile, jwks); + return c.json({ ok: true, kid: (profile.signing_keys?.[0] as { kid?: string } | undefined)?.kid }); + } catch (err) { + if (err instanceof UCPVerificationError) { + return c.json({ ok: false, code: err.code, message: err.message }, 500); + } + throw err; + } +}); + +const port = Number(process.env.PORT ?? 3010); +console.warn(`signed-ucp-merchant listening on :${port}`); +console.warn(' /.well-known/ucp — signed profile'); +console.warn(' /.well-known/jwks.json — public key set'); +console.warn(' /_selftest/ucp — local verify round-trip'); + +Bun.serve({ port, fetch: app.fetch }); diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index adff3f9..6778ebf 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -58,7 +58,39 @@ export interface SignedUCPProfile extends UCPProfile { signature: string; } -const JOSE_INSTALL_HINT = 'Install the optional peer dependency: `npm install jose@^5` (or `bun add jose`).'; +const JOSE_INSTALL_HINT = 'Install the optional peer dependency: `npm install jose@^5` (or `bun add jose`). Tested against jose v5.x.'; + +/** UCP §6 + RFC 8725 §3.1 — restrict accepted JWS algorithms. Anything outside this + * list (HS, RS, none, etc.) is rejected to prevent alg-confusion attacks where a + * hostile JWK published in the profile's signing_keys[] is used with an unintended + * algorithm. */ +const ALLOWED_ALGS = ['EdDSA', 'ES256'] as const; +type AllowedAlg = (typeof ALLOWED_ALGS)[number]; + +/** UCP §6.2 — JWS protected header `typ` value. Verifiers SHOULD enforce this to + * prevent cross-protocol token reuse (RFC 8725 §3.11). */ +const UCP_TYP = 'ucp-profile+jws'; + +/** Discriminated error class so consumers can branch on failure mode without + * parsing message strings or importing jose internals. */ +export class UCPVerificationError extends Error { + constructor( + public readonly code: + | 'no_signature' + | 'missing_kid' + | 'kid_not_found' + | 'duplicate_kid' + | 'unsupported_alg' + | 'wrong_typ' + | 'signature_invalid' + | 'body_mismatch' + | 'malformed_jws', + message: string, + ) { + super(message); + this.name = 'UCPVerificationError'; + } +} async function loadJose(): Promise { try { @@ -86,8 +118,17 @@ function canonicalizeProfile(profile: UCPProfile): string { return stableStringify(stripped); } -/** Deterministic JSON.stringify with lexicographic key ordering at every level. */ +/** Deterministic JSON.stringify with lexicographic key ordering at every level. + * Throws on non-integer Number values — UCP profiles don't carry floats and + * cross-language float canonicalization (RFC 8785 §3.2.2.3) would diverge between + * Node's JSON.stringify and Python's json.dumps. Defensive: catch the + * drift at sign-time rather than at verifier-time in production. */ function stableStringify(value: unknown): string { + if (typeof value === 'number' && !Number.isInteger(value) && Number.isFinite(value)) { + throw new Error( + `UCP profile canonicalization rejects non-integer Number ${value}. Use a decimal string (e.g. "9.99") for monetary or fractional fields to preserve cross-language byte-parity.`, + ); + } if (value === null || typeof value !== 'object') return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`; const obj = value as Record; @@ -192,18 +233,48 @@ export async function verifyUCPProfile( const stripped = { ...profile } as Partial; const sig = stripped.signature; delete stripped.signature; - if (!sig) throw new Error('UCP profile has no `signature` field; expected JWS Compact Serialization.'); + if (!sig) throw new UCPVerificationError('no_signature', 'UCP profile has no `signature` field; expected JWS Compact Serialization.'); const canonicalBody = canonicalizeProfile(stripped as UCPProfile); const expectedPayload = new TextEncoder().encode(canonicalBody); - const { payload: signedPayload } = await jose.compactVerify(sig, async (header) => { - const kid = header.kid; - if (!kid) throw new Error('UCP signature header missing `kid`.'); - const jwk = jwks.keys.find((k) => (k as Record).kid === kid); - if (!jwk) throw new Error(`No JWK in JWKS matching kid=${kid}.`); - return jose.importJWK(jwk as Parameters[0], header.alg); - }); + let signedPayload: Uint8Array; + try { + const verified = await jose.compactVerify( + sig, + async (header) => { + // RFC 8725 §3.1 — restrict to allow-listed algorithms before key resolution + // so a hostile JWK can never be used with HS256/none/RS256/etc. + if (!ALLOWED_ALGS.includes(header.alg as AllowedAlg)) { + throw new UCPVerificationError('unsupported_alg', `UCP signing alg must be one of ${ALLOWED_ALGS.join(', ')}; got ${String(header.alg)}.`); + } + // RFC 8725 §3.11 — enforce expected typ to prevent cross-protocol token reuse. + if (header.typ !== UCP_TYP) { + throw new UCPVerificationError('wrong_typ', `UCP signature typ must be "${UCP_TYP}"; got ${String(header.typ)}.`); + } + const kid = header.kid; + if (!kid) throw new UCPVerificationError('missing_kid', 'UCP signature header missing `kid`.'); + const matches = jwks.keys.filter((k) => (k as Record).kid === kid); + if (matches.length === 0) throw new UCPVerificationError('kid_not_found', `No JWK in JWKS matching kid=${kid}.`); + if (matches.length > 1) throw new UCPVerificationError('duplicate_kid', `JWKS contains ${matches.length} keys with kid=${kid}; expected exactly one.`); + return jose.importJWK(matches[0] as Parameters[0], header.alg); + }, + { algorithms: [...ALLOWED_ALGS] }, + ); + signedPayload = verified.payload; + } catch (err) { + if (err instanceof UCPVerificationError) throw err; + if (err instanceof Error && err.name === 'JOSEAlgNotAllowed') { + throw new UCPVerificationError('unsupported_alg', `UCP signing alg not allowed: ${err.message}`); + } + if (err instanceof Error && err.name === 'JWSSignatureVerificationFailed') { + throw new UCPVerificationError('signature_invalid', `UCP signature verification failed: ${err.message}`); + } + if (err instanceof Error && err.name === 'JWSInvalid') { + throw new UCPVerificationError('malformed_jws', `Malformed JWS: ${err.message}`); + } + throw err; + } // Compare the bytes that were actually signed against the canonical body of the // profile we received. `compactVerify` validates the JWS against the bytes embedded @@ -211,7 +282,7 @@ export async function verifyUCPProfile( // signing while the JWS stayed unchanged. Body-vs-payload comparison closes that // gap. if (!constantTimeEqual(signedPayload, expectedPayload)) { - throw new Error('UCP profile body does not match the signed payload (tampered or non-canonical).'); + throw new UCPVerificationError('body_mismatch', 'UCP profile body does not match the signed payload (tampered or non-canonical).'); } return true; diff --git a/src/index.ts b/src/index.ts index 904bf60..17ba88a 100644 --- a/src/index.ts +++ b/src/index.ts @@ -48,6 +48,7 @@ export { type SignUCPProfileOptions, type SignedUCPProfile, signUCPProfile, + UCPVerificationError, verifyUCPProfile, } from './identity/ucp-jwks'; export { diff --git a/tests/fixtures/cross-lang/node-es256-rails.json b/tests/fixtures/cross-lang/node-es256-rails.json new file mode 100644 index 0000000..7eb0535 --- /dev/null +++ b/tests/fixtures/cross-lang/node-es256-rails.json @@ -0,0 +1,63 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://a.example.com" + }, + { + "type": "a2a", + "url": "https://a.example.com/agent-card.json" + } + ], + "capabilities": [], + "payment_handlers": [ + { + "name": "tempo", + "config": { + "rail": "tempo-mainnet", + "chain_id": 4217 + } + }, + { + "name": "x402", + "config": { + "networks": [ + "base-8453" + ] + } + } + ], + "signing_keys": [ + { + "kid": "node-es256-rails-ES256", + "alg": "ES256", + "use": "sig", + "crv": "P-256", + "kty": "EC", + "x": "xMvwGE1713BNeAABNZZhj00pivlto9FNz1YKqzAUvP0", + "y": "BzgzXRAWbR0VWJNL7F59684mX3_fP-0BDUQSmZAvy38" + } + ], + "name": "ES256 Merchant", + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJrdHkiOiJFQyIsInVzZSI6InNpZyIsIngiOiJ4TXZ3R0UxNzEzQk5lQUFCTlpaaGowMHBpdmx0bzlGTnoxWUtxekFVdlAwIiwieSI6IkJ6Z3pYUkFXYlIwVldKTkw3RjU5Njg0bVgzX2ZQLTBCRFVRU21aQXZ5MzgifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.kdcN5xFTZ3Fd4nA9qXlr04F5CxdIVv04zRggY2U6820Gn4sJ9guvJij-Fne26xTEXLIuLlbulwe1bUIJXWBZuQ" + }, + "jwks": { + "keys": [ + { + "kid": "node-es256-rails-ES256", + "alg": "ES256", + "use": "sig", + "crv": "P-256", + "kty": "EC", + "x": "xMvwGE1713BNeAABNZZhj00pivlto9FNz1YKqzAUvP0", + "y": "BzgzXRAWbR0VWJNL7F59684mX3_fP-0BDUQSmZAvy38" + } + ] + }, + "alg": "ES256", + "kid": "node-es256-rails-ES256", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-extras-int.json b/tests/fixtures/cross-lang/node-extras-int.json new file mode 100644 index 0000000..b5354a3 --- /dev/null +++ b/tests/fixtures/cross-lang/node-extras-int.json @@ -0,0 +1,49 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://e.example.com" + } + ], + "capabilities": [], + "payment_handlers": [ + { + "name": "stripe", + "config": { + "profile_id": "abc", + "count": 7 + } + } + ], + "signing_keys": [ + { + "kid": "node-extras-int-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "QdPh4oYqDA7zIBaNkfW_HJLEGiMS_mgZU98a-_8vLpM" + } + ], + "name": "Extras Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiUWRQaDRvWXFEQTd6SUJhTmtmV19ISkxFR2lNU19tZ1pVOThhLV84dkxwTSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.JglqGMtdQKucptR-w8YtNQ3hG6QLB5McUIGlnTHYsa9vl3SfQ3UaoLqKsVH2DHLmf8lRl4qKzB8EHS9mJ9Z0Bw" + }, + "jwks": { + "keys": [ + { + "kid": "node-extras-int-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "QdPh4oYqDA7zIBaNkfW_HJLEGiMS_mgZU98a-_8vLpM" + } + ] + }, + "alg": "EdDSA", + "kid": "node-extras-int-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-minimal.json b/tests/fixtures/cross-lang/node-minimal.json new file mode 100644 index 0000000..cc3f976 --- /dev/null +++ b/tests/fixtures/cross-lang/node-minimal.json @@ -0,0 +1,41 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://m.example.com" + } + ], + "capabilities": [], + "payment_handlers": [], + "signing_keys": [ + { + "kid": "node-minimal-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "QCSceDALov_XB5V0ACkZlnjhhIxBqpoYpaO5HlAf0aw" + } + ], + "name": "Minimal Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1taW5pbWFsLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IlFDU2NlREFMb3ZfWEI1VjBBQ2tabG5qaGhJeEJxcG9ZcGFPNUhsQWYwYXcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.amoy1Vf2vIzfR_asZp0dxc0ywNo0nc4dvoX1BjnJimE_ClfvtcTuGDfglyBYLvk4aRtaqru1DCpYgCSEnI2NBA" + }, + "jwks": { + "keys": [ + { + "kid": "node-minimal-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "QCSceDALov_XB5V0ACkZlnjhhIxBqpoYpaO5HlAf0aw" + } + ] + }, + "alg": "EdDSA", + "kid": "node-minimal-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/py-es256-rails.json b/tests/fixtures/cross-lang/py-es256-rails.json new file mode 100644 index 0000000..dbe5d70 --- /dev/null +++ b/tests/fixtures/cross-lang/py-es256-rails.json @@ -0,0 +1,63 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://a.example.com" + }, + { + "type": "a2a", + "url": "https://a.example.com/agent-card.json" + } + ], + "capabilities": [], + "payment_handlers": [ + { + "name": "tempo", + "config": { + "rail": "tempo-mainnet", + "chain_id": 4217 + } + }, + { + "name": "x402", + "config": { + "networks": [ + "base-8453" + ] + } + } + ], + "signing_keys": [ + { + "kid": "py-es256-rails-ES256", + "kty": "EC", + "alg": "ES256", + "use": "sig", + "crv": "P-256", + "x": "l45yeK-s3eujIDwIU-rEeiv1l6KQq-1GUm4-0P8gpVk", + "y": "3o_N_dWi26UxSRzIIjvuQCKCgkxN6pO_5xYSrZrHYaQ" + } + ], + "name": "ES256 Merchant", + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoidWNwLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoibDQ1eWVLLXMzZXVqSUR3SVUtckVlaXYxbDZLUXEtMUdVbTQtMFA4Z3BWayIsInkiOiIzb19OX2RXaTI2VXhTUnpJSWp2dVFDS0Nna3hONnBPXzV4WVNyWnJIWWFRIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.Qh5KIH-aP8KVqIFDUnUOwnVC0L8Tii03u6NM6Bt-lePUlgnzLwOogNvMRK7hl5YqwYgzhrkWM2KbHRakv-x_cw" + }, + "jwks": { + "keys": [ + { + "crv": "P-256", + "x": "l45yeK-s3eujIDwIU-rEeiv1l6KQq-1GUm4-0P8gpVk", + "y": "3o_N_dWi26UxSRzIIjvuQCKCgkxN6pO_5xYSrZrHYaQ", + "kid": "py-es256-rails-ES256", + "alg": "ES256", + "use": "sig", + "kty": "EC" + } + ] + }, + "alg": "ES256", + "kid": "py-es256-rails-ES256", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-extras-int.json b/tests/fixtures/cross-lang/py-extras-int.json new file mode 100644 index 0000000..cd9c680 --- /dev/null +++ b/tests/fixtures/cross-lang/py-extras-int.json @@ -0,0 +1,49 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://e.example.com" + } + ], + "capabilities": [], + "payment_handlers": [ + { + "name": "stripe", + "config": { + "profile_id": "abc", + "count": 7 + } + } + ], + "signing_keys": [ + { + "kid": "py-extras-int-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "-ZXadF3IWTfw9_0GOs5imZKusJ5ID8vAZgcN4hH7iWw" + } + ], + "name": "Extras Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1leHRyYXMtaW50LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ii1aWGFkRjNJV1RmdzlfMEdPczVpbVpLdXNKNUlEOHZBWmdjTjRoSDdpV3cifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.71PP5YsYjSIA2PVI0B4HNg5MrRQbn0GrUGjeQ4R6SPNK4-n8AMuACSjKqEF7df9hLVrmfuiwUyAJhSItQuFYCA" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "-ZXadF3IWTfw9_0GOs5imZKusJ5ID8vAZgcN4hH7iWw", + "kid": "py-extras-int-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-extras-int-EdDSA", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-minimal.json b/tests/fixtures/cross-lang/py-minimal.json new file mode 100644 index 0000000..685aa07 --- /dev/null +++ b/tests/fixtures/cross-lang/py-minimal.json @@ -0,0 +1,41 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://m.example.com" + } + ], + "capabilities": [], + "payment_handlers": [], + "signing_keys": [ + { + "kid": "py-minimal-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "DvrugQWOA-k_RSYLM4IbjA_IoO_DiFeDfDXAy6PvQM8" + } + ], + "name": "Minimal Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJEdnJ1Z1FXT0Eta19SU1lMTTRJYmpBX0lvT19EaUZlRGZEWEF5NlB2UU04In1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.Uk1fCmzYJvfxp_6CbmgTzdpuZzziodaroFTEjfKZ_qK_FU2i2HfG-SkYdz8icZLQxWVhMtTaoTtqeV6BvjNHBA" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "DvrugQWOA-k_RSYLM4IbjA_IoO_DiFeDfDXAy6PvQM8", + "kid": "py-minimal-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-minimal-EdDSA", + "generator": "python" +} diff --git a/tests/identity/cross-lang.test.ts b/tests/identity/cross-lang.test.ts new file mode 100644 index 0000000..88f0fa5 --- /dev/null +++ b/tests/identity/cross-lang.test.ts @@ -0,0 +1,47 @@ +/** + * Cross-language UCP signing fixture corpus. + * + * Each fixture file is a `{ profile, jwks, alg, kid, generator }` envelope. + * Both Node and Python check in identical fixtures here so a future + * canonicalization change in either language fails CI loudly. Without this, + * cross-language byte parity drift would silently break verifier-side + * compatibility in production. + */ + +import { readdirSync, readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { describe, expect, it } from 'vitest'; +import { verifyUCPProfile, type JWKSResponse, type SignedUCPProfile } from '../../src/identity/ucp-jwks'; + +interface Fixture { + profile: SignedUCPProfile; + jwks: JWKSResponse; + alg: 'EdDSA' | 'ES256'; + kid: string; + generator: 'node' | 'python'; +} + +const FIXTURE_DIR = join(__dirname, '..', 'fixtures', 'cross-lang'); + +const fixtures: { name: string; data: Fixture }[] = readdirSync(FIXTURE_DIR) + .filter((f) => f.endsWith('.json')) + .map((name) => ({ + name, + data: JSON.parse(readFileSync(join(FIXTURE_DIR, name), 'utf-8')) as Fixture, + })); + +describe('UCP signing — cross-language fixture corpus', () => { + for (const { name, data } of fixtures) { + it(`verifies ${name} (${data.alg}, generated by ${data.generator})`, async () => { + const ok = await verifyUCPProfile(data.profile, data.jwks); + expect(ok).toBe(true); + }); + } + + it('corpus contains both Node and Python fixtures', () => { + const generators = new Set(fixtures.map((f) => f.data.generator)); + expect(generators).toContain('node'); + expect(generators).toContain('python'); + expect(fixtures.length).toBeGreaterThanOrEqual(6); + }); +}); diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index af1ede7..2766c55 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -4,6 +4,7 @@ import { buildJWKSResponse, generateUCPSigningKey, signUCPProfile, + UCPVerificationError, verifyUCPProfile, } from '../../src/identity/ucp-jwks'; @@ -131,3 +132,157 @@ describe('UCP signing — buildJWKSResponse', () => { expect(buildJWKSResponse([])).toEqual({ keys: [] }); }); }); + +describe('UCP signing — security: alg-confusion + typ + dup-kid', () => { + // RFC 8725 §3.1: a verifier MUST restrict accepted JWS algorithms to the + // set the application expects. A naive implementation that calls importJWK(jwk, header.alg) + // can be coerced into using HS256 (symmetric) with the public key as the secret — + // a hostile signing_keys[] entry then mints valid-looking signatures. + it('rejects HS256 signatures even when the JWKS contains an HS256 oct key', async () => { + const jose = await import('jose'); + const sharedSecret = new Uint8Array(32).fill(0xab); + const ocJwk = { + kid: 'attacker', + kty: 'oct', + alg: 'HS256', + use: 'sig', + k: Buffer.from(sharedSecret).toString('base64url'), + }; + const profile = buildUCPProfile({ ...baseInput, signing_keys: [ocJwk as never] }); + const stripped = { ...profile } as Record; + delete stripped.signature; + const sortedJson = (() => { + const sort = (v: unknown): unknown => { + if (v === null || typeof v !== 'object') return v; + if (Array.isArray(v)) return v.map(sort); + return Object.keys(v as Record).sort().reduce>((acc, k) => { + acc[k] = sort((v as Record)[k]); + return acc; + }, {}); + }; + return JSON.stringify(sort(stripped)); + })(); + const evilSig = await new jose.CompactSign(new TextEncoder().encode(sortedJson)) + .setProtectedHeader({ alg: 'HS256', kid: 'attacker', typ: 'ucp-profile+jws' }) + .sign(sharedSecret); + const tampered = { ...profile, signature: evilSig }; + await expect(verifyUCPProfile(tampered as never, buildJWKSResponse([ocJwk as never]))) + .rejects.toThrow(UCPVerificationError); + }); + + it('rejects a JWS with typ != "ucp-profile+jws"', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const jose = await import('jose'); + const stripped = { ...profile } as Record; + delete stripped.signature; + const sortedJson = (() => { + const sort = (v: unknown): unknown => { + if (v === null || typeof v !== 'object') return v; + if (Array.isArray(v)) return v.map(sort); + return Object.keys(v as Record).sort().reduce>((acc, k) => { + acc[k] = sort((v as Record)[k]); + return acc; + }, {}); + }; + return JSON.stringify(sort(stripped)); + })(); + const wrongTypSig = await new jose.CompactSign(new TextEncoder().encode(sortedJson)) + .setProtectedHeader({ alg: 'EdDSA', kid: 'k', typ: 'JWT' }) + .sign(privateKey as Parameters[0]); + await expect( + verifyUCPProfile({ ...profile, signature: wrongTypSig } as never, buildJWKSResponse([publicJWK])), + ).rejects.toThrow(/typ/); + }); + + it('rejects duplicate kids in the JWKS', async () => { + const a = await generateUCPSigningKey({ kid: 'dup' }); + const b = await generateUCPSigningKey({ kid: 'dup' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [a.publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: a.privateKey, kid: 'dup' }); + await expect(verifyUCPProfile(signed, buildJWKSResponse([a.publicJWK, b.publicJWK]))) + .rejects.toThrow(/duplicate|2 keys/); + }); + + it('emits typed UCPVerificationError for body mismatch', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + const tampered = { ...signed, name: 'Different' }; + await expect(verifyUCPProfile(tampered, buildJWKSResponse([publicJWK]))) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'body_mismatch' }); + }); + + it('emits typed UCPVerificationError for missing signature', async () => { + const { publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + await expect(verifyUCPProfile(profile as never, buildJWKSResponse([publicJWK]))) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'no_signature' }); + }); + + it('emits typed UCPVerificationError for kid not in JWKS', async () => { + const signer = await generateUCPSigningKey({ kid: 'signer' }); + const other = await generateUCPSigningKey({ kid: 'other' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [signer.publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: signer.privateKey, kid: 'signer' }); + await expect(verifyUCPProfile(signed, buildJWKSResponse([other.publicJWK]))) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'kid_not_found' }); + }); + + it('rejects malformed JWS (not three segments)', async () => { + const { publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const garbage = { ...profile, signature: 'not.a.jws' }; + await expect(verifyUCPProfile(garbage as never, buildJWKSResponse([publicJWK]))) + .rejects.toThrow(); + }); + + it('rejects a tampered signature segment with valid header+payload', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + const segments = signed.signature.split('.'); + // Flip last char of the signature segment. + const flipped = segments[2]!.slice(0, -1) + (segments[2]!.endsWith('A') ? 'B' : 'A'); + const tampered = { ...signed, signature: `${segments[0]}.${segments[1]}.${flipped}` }; + await expect(verifyUCPProfile(tampered, buildJWKSResponse([publicJWK]))) + .rejects.toThrow(); + }); + + it('signing twice with EdDSA is idempotent (deterministic signature)', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const a = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + const b = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + expect(a.signature).toBe(b.signature); + }); + + it('signing twice with ES256 produces different signatures but both verify', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k', alg: 'ES256' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const a = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k', alg: 'ES256' }); + const b = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k', alg: 'ES256' }); + expect(a.signature).not.toBe(b.signature); + expect(await verifyUCPProfile(a, buildJWKSResponse([publicJWK]))).toBe(true); + expect(await verifyUCPProfile(b, buildJWKSResponse([publicJWK]))).toBe(true); + }); +}); + +describe('UCP signing — float canonicalization defense', () => { + it('throws when signing a profile that contains a non-integer Number anywhere', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + // Inject a float into extras-shaped data so the canonicalizer sees it. + (profile as unknown as Record).extras = { rate: 0.0125 }; + await expect(signUCPProfile(profile, { signingKey: privateKey, kid: 'k' })) + .rejects.toThrow(/non-integer Number/); + }); + + it('signing with integers + strings is fine', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + (profile as unknown as Record).extras = { count: 7, label: 'wine' }; + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + expect(await verifyUCPProfile(signed, buildJWKSResponse([publicJWK]))).toBe(true); + }); +}); From f1fa39bc153a0c3b17098f044d2fbe2f6d7167ee Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 15:13:59 -0700 Subject: [PATCH 04/35] hardening: round-2 reviewer findings (security + test gaps) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 2 +- examples/signed-ucp-merchant.ts | 8 +- src/identity/ucp-jwks.ts | 68 +++++++++++--- src/identity/ucp.ts | 2 +- .../fixtures/cross-lang/node-capability.json | 56 ++++++++++++ tests/fixtures/cross-lang/node-multikey.json | 64 +++++++++++++ tests/fixtures/cross-lang/node-unicode.json | 48 ++++++++++ tests/fixtures/cross-lang/py-capability.json | 56 ++++++++++++ tests/fixtures/cross-lang/py-multikey.json | 64 +++++++++++++ tests/fixtures/cross-lang/py-unicode.json | 48 ++++++++++ tests/identity/cross-lang.test.ts | 12 ++- tests/identity/ucp-signing.test.ts | 90 +++++++++++++++++-- 12 files changed, 493 insertions(+), 25 deletions(-) create mode 100644 tests/fixtures/cross-lang/node-capability.json create mode 100644 tests/fixtures/cross-lang/node-multikey.json create mode 100644 tests/fixtures/cross-lang/node-unicode.json create mode 100644 tests/fixtures/cross-lang/py-capability.json create mode 100644 tests/fixtures/cross-lang/py-multikey.json create mode 100644 tests/fixtures/cross-lang/py-unicode.json diff --git a/README.md b/README.md index 2ca96a3..ee772a9 100644 --- a/README.md +++ b/README.md @@ -214,7 +214,7 @@ const jwks = buildJWKSResponse([publicJWK]); `signUCPProfile` rejects profiles containing non-integer `Number` values (cross-language float canonicalization is not stable; use decimal strings for monetary or fractional fields). -**HSM / KMS-backed signing.** `signingKey` accepts any `jose.KeyLike` — including remote signers wrapped via `createPrivateKey` from `node:crypto` for AWS KMS / GCP KMS asymmetric keys. The `signing_key` never has to leave the HSM. +**Persisting the private JWK.** Mint once via `generateUCPSigningKey()`, export with `jose.exportJWK(privateKey)` to get the JSON-serializable form, store in your secret manager (AWS Secrets Manager, GCP Secret Manager, etc.). On each container start, read the secret, `jose.importJWK(jwk, alg)` to re-hydrate. Remote-signer flows (KMS-backed asymmetric keys) require an adapter layer that exposes a `KeyLike` jose can call; `jose` does not natively wrap KMS endpoints. **Key rotation.** Mint a new key with a new `kid`, add the public JWK to your JWKS endpoint alongside the old one, then sign new profiles with the new key. Verifiers fetching the JWKS pick up both; any in-flight envelopes signed by the old key still verify until you remove that JWK from the JWKS. Drop the old JWK once your verifier-side cache TTL has elapsed. diff --git a/examples/signed-ucp-merchant.ts b/examples/signed-ucp-merchant.ts index 2f937cc..c9a4cd1 100644 --- a/examples/signed-ucp-merchant.ts +++ b/examples/signed-ucp-merchant.ts @@ -39,7 +39,8 @@ let cached: Promise | null = null; function loadSigningKey(): Promise { // Cache the in-flight Promise (not the resolved value) so two concurrent - // first-callers can't independently generate different keys. + // first-callers can't independently generate different keys. On rejection + // the cache clears so the next caller retries. if (cached) return cached; cached = (async () => { const envJwk = process.env.UCP_SIGNING_KEY_JWK_PRIVATE; @@ -63,7 +64,10 @@ function loadSigningKey(): Promise { } console.warn('[ucp] UCP_SIGNING_KEY_JWK_PRIVATE not set — generating ephemeral key. Verifier caches will break across restarts.'); return generateUCPSigningKey({ kid: KID, alg: ALG }); - })(); + })().catch((err) => { + cached = null; + throw err; + }); return cached; } diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index 6778ebf..f03af9d 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -84,7 +84,10 @@ export class UCPVerificationError extends Error { | 'wrong_typ' | 'signature_invalid' | 'body_mismatch' - | 'malformed_jws', + | 'malformed_jws' + | 'malformed_jwks' + | 'unrecognized_critical_header' + | 'unusable_key', message: string, ) { super(message); @@ -119,15 +122,25 @@ function canonicalizeProfile(profile: UCPProfile): string { } /** Deterministic JSON.stringify with lexicographic key ordering at every level. - * Throws on non-integer Number values — UCP profiles don't carry floats and - * cross-language float canonicalization (RFC 8785 §3.2.2.3) would diverge between - * Node's JSON.stringify and Python's json.dumps. Defensive: catch the - * drift at sign-time rather than at verifier-time in production. */ + * Rejects ANY non-finite Number (NaN, Infinity, -Infinity) and any Number + * whose value has a fractional part OR whose JSON representation may diverge + * cross-language. Cross-language float canonicalization (RFC 8785 §3.2.2.3) + * is not stable between Node's JSON.stringify and Python's json.dumps + * (e.g. `1.0` → `1` vs `1.0`, `1e-7` → `1e-7` vs `1e-07`). UCP profiles + * must use decimal strings for monetary or fractional fields to preserve + * byte parity with the Python sibling. */ function stableStringify(value: unknown): string { - if (typeof value === 'number' && !Number.isInteger(value) && Number.isFinite(value)) { - throw new Error( - `UCP profile canonicalization rejects non-integer Number ${value}. Use a decimal string (e.g. "9.99") for monetary or fractional fields to preserve cross-language byte-parity.`, - ); + if (typeof value === 'number') { + if (!Number.isFinite(value)) { + throw new Error( + `UCP profile canonicalization rejects non-finite Number ${value}. Use a decimal string for any value that may be NaN/Infinity.`, + ); + } + if (!Number.isInteger(value)) { + throw new Error( + `UCP profile canonicalization rejects non-integer Number ${value}. Use a decimal string (e.g. "9.99") for monetary or fractional fields to preserve cross-language byte-parity.`, + ); + } } if (value === null || typeof value !== 'object') return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`; @@ -148,7 +161,7 @@ function stableStringify(value: unknown): string { * * Example: * ```ts - * import { generateUCPSigningKey } from '@agent-score/commerce/identity/ucp-jwks'; + * import { generateUCPSigningKey } from '@agent-score/commerce'; * * const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'merchant-2026-05' }); * // Persist privateKey securely (env var, KMS, secret manager). @@ -201,11 +214,22 @@ export async function signUCPProfile( const jose = await loadJose(); const alg = opts.alg ?? 'EdDSA'; + // Sign-time kid sanity check: the profile's `signing_keys[]` MUST contain a + // JWK with the matching kid; otherwise verifiers can't resolve the public + // key and the profile is dead-on-arrival. Catch this at sign-time rather + // than at verifier-time in production. + const kids = (profile.signing_keys ?? []).map((k) => (k as Record).kid); + if (!kids.includes(opts.kid)) { + throw new Error( + `signUCPProfile: kid ${JSON.stringify(opts.kid)} is not present in profile.signing_keys[] (declared kids: ${JSON.stringify(kids)}). Verifiers will not find the key.`, + ); + } + const canonicalBody = canonicalizeProfile(profile); const payloadBytes = new TextEncoder().encode(canonicalBody); const signature = await new jose.CompactSign(payloadBytes) - .setProtectedHeader({ alg, kid: opts.kid, typ: 'ucp-profile+jws' }) + .setProtectedHeader({ alg, kid: opts.kid, typ: UCP_TYP }) .sign(opts.signingKey as Parameters[0]); return { ...profile, signature }; @@ -230,6 +254,15 @@ export async function verifyUCPProfile( ): Promise { const jose = await loadJose(); + // JWKS shape guard so a malformed argument emits a typed UCPVerificationError + // rather than a raw TypeError on `.filter is not a function`. + if (!jwks || typeof jwks !== 'object' || !Array.isArray((jwks as { keys?: unknown }).keys)) { + throw new UCPVerificationError( + 'malformed_jwks', + `UCP verifier expected JWKS shape { keys: [...] }; got ${jwks === null ? 'null' : typeof jwks === 'object' ? 'object without keys[] array' : typeof jwks}.`, + ); + } + const stripped = { ...profile } as Partial; const sig = stripped.signature; delete stripped.signature; @@ -257,6 +290,11 @@ export async function verifyUCPProfile( const matches = jwks.keys.filter((k) => (k as Record).kid === kid); if (matches.length === 0) throw new UCPVerificationError('kid_not_found', `No JWK in JWKS matching kid=${kid}.`); if (matches.length > 1) throw new UCPVerificationError('duplicate_kid', `JWKS contains ${matches.length} keys with kid=${kid}; expected exactly one.`); + // RFC 7517 §4.2: reject keys not intended for signature verification. + const matchedKey = matches[0] as Record; + if (matchedKey.use !== undefined && matchedKey.use !== 'sig') { + throw new UCPVerificationError('unusable_key', `JWK with kid=${kid} has use=${JSON.stringify(matchedKey.use)}; expected "sig".`); + } return jose.importJWK(matches[0] as Parameters[0], header.alg); }, { algorithms: [...ALLOWED_ALGS] }, @@ -273,6 +311,12 @@ export async function verifyUCPProfile( if (err instanceof Error && err.name === 'JWSInvalid') { throw new UCPVerificationError('malformed_jws', `Malformed JWS: ${err.message}`); } + // RFC 7515 §4.1.11 / RFC 8725 §3.10: a verifier MUST reject any JWS whose + // `crit` header carries an extension the implementation doesn't understand. + // jose throws JOSENotSupported; wrap so callers see the typed error. + if (err instanceof Error && err.name === 'JOSENotSupported') { + throw new UCPVerificationError('unrecognized_critical_header', `UCP signing rejected unrecognized critical header: ${err.message}`); + } throw err; } @@ -303,7 +347,7 @@ function constantTimeEqual(a: Uint8Array, b: Uint8Array): boolean { * * Example: * ```ts - * import { buildJWKSResponse } from '@agent-score/commerce/identity/ucp-jwks'; + * import { buildJWKSResponse } from '@agent-score/commerce'; * * app.get('/.well-known/jwks.json', (c) => * c.json(buildJWKSResponse([publicJWK])) diff --git a/src/identity/ucp.ts b/src/identity/ucp.ts index bef7ab1..e7c855f 100644 --- a/src/identity/ucp.ts +++ b/src/identity/ucp.ts @@ -123,7 +123,7 @@ const AGENTSCORE_CAPABILITY_VERSION = '1'; * * Example: * ```ts - * import { buildUCPProfile } from '@agent-score/commerce/identity/hono'; + * import { buildUCPProfile } from '@agent-score/commerce'; * * app.get('/.well-known/ucp', async (c) => { * const data = getAgentScoreData(c); diff --git a/tests/fixtures/cross-lang/node-capability.json b/tests/fixtures/cross-lang/node-capability.json new file mode 100644 index 0000000..b8abade --- /dev/null +++ b/tests/fixtures/cross-lang/node-capability.json @@ -0,0 +1,56 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://c.example.com" + } + ], + "capabilities": [ + { + "name": "agentscore-identity", + "schema": "https://agentscore.sh/schema/identity/1", + "version": "1", + "kyc_required": true + } + ], + "payment_handlers": [ + { + "name": "tempo", + "config": { + "rail": "tempo-mainnet", + "chain_id": 4217 + } + } + ], + "signing_keys": [ + { + "kid": "node-capability-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "8zz-L1N_SZ0EUmciU1IzuxBuGd67MSg-OemKm6ofmgg" + } + ], + "name": "Capability Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6ImFnZW50c2NvcmUtaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hL2lkZW50aXR5LzEiLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkNhcGFiaWxpdHkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9jLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI4enotTDFOX1NaMEVVbWNpVTFJenV4QnVHZDY3TVNnLU9lbUttNm9mbWdnIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.YmiTy87alEbVfAEXYzXYkBrsbO_kHqgTSlv3gKuzy6Oere-pJl0PmZ8zGW2uTyjaGC9OFbjLUIzowY3jnJmGAg" + }, + "jwks": { + "keys": [ + { + "kid": "node-capability-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "8zz-L1N_SZ0EUmciU1IzuxBuGd67MSg-OemKm6ofmgg" + } + ] + }, + "alg": "EdDSA", + "kid": "node-capability-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-multikey.json b/tests/fixtures/cross-lang/node-multikey.json new file mode 100644 index 0000000..8c32881 --- /dev/null +++ b/tests/fixtures/cross-lang/node-multikey.json @@ -0,0 +1,64 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://mk.example.com" + } + ], + "capabilities": [], + "payment_handlers": [ + { + "name": "tempo", + "config": { + "rail": "tempo-mainnet" + } + } + ], + "signing_keys": [ + { + "kid": "node-multikey-old", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "qSF9p7IJIFIKxoFl6Od1G8qj65Prx35EnN44zMxJs6U" + }, + { + "kid": "node-multikey-new", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "9Al1EZLHjgl02MWGtIGaStOPnR9cBc0WXNYuGbU5r-g" + } + ], + "name": "Multi-Key Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3IiwidHlwIjoidWNwLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJxU0Y5cDdJSklGSUt4b0ZsNk9kMUc4cWo2NVByeDM1RW5ONDR6TXhKczZVIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW5ldyIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI5QWwxRVpMSGpnbDAyTVdHdElHYVN0T1BuUjljQmMwV1hOWXVHYlU1ci1nIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.jXm8ZRUWa9_BUYfSv_PCJNqIWbAYf39DUwOdqvExMPTLWDoDzNAwoIleWfyiAGXMOyK0J-0DPeeCFTzmOPMnBQ" + }, + "jwks": { + "keys": [ + { + "kid": "node-multikey-old", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "qSF9p7IJIFIKxoFl6Od1G8qj65Prx35EnN44zMxJs6U" + }, + { + "kid": "node-multikey-new", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "9Al1EZLHjgl02MWGtIGaStOPnR9cBc0WXNYuGbU5r-g" + } + ] + }, + "alg": "EdDSA", + "kid": "node-multikey-new", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-unicode.json b/tests/fixtures/cross-lang/node-unicode.json new file mode 100644 index 0000000..18bf117 --- /dev/null +++ b/tests/fixtures/cross-lang/node-unicode.json @@ -0,0 +1,48 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://日本.example.com" + } + ], + "capabilities": [], + "payment_handlers": [ + { + "name": "tempo", + "config": { + "note": "メモ" + } + } + ], + "signing_keys": [ + { + "kid": "node-unicode-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "mxtclpNy58uer_3ivEk9HfPp5_6zXtYUpc_ItTLz0sA" + } + ], + "name": "Café 日本 🍷 Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJteHRjbHBOeTU4dWVyXzNpdkVrOUhmUHA1XzZ6WHRZVXBjX0l0VEx6MHNBIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.21NUepmkXaXs6cRnPBgheUR0F7EoqysnAkOEqiaqZEG8OEGMkegGsVeEvtaxEjpQZfC4KAeTqvjy6Vc-FSDzBg" + }, + "jwks": { + "keys": [ + { + "kid": "node-unicode-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "mxtclpNy58uer_3ivEk9HfPp5_6zXtYUpc_ItTLz0sA" + } + ] + }, + "alg": "EdDSA", + "kid": "node-unicode-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/py-capability.json b/tests/fixtures/cross-lang/py-capability.json new file mode 100644 index 0000000..e7db70a --- /dev/null +++ b/tests/fixtures/cross-lang/py-capability.json @@ -0,0 +1,56 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://c.example.com" + } + ], + "capabilities": [ + { + "name": "agentscore-identity", + "schema": "https://agentscore.sh/schema/identity/1", + "version": "1", + "kyc_required": true + } + ], + "payment_handlers": [ + { + "name": "tempo", + "config": { + "rail": "tempo-mainnet", + "chain_id": 4217 + } + } + ], + "signing_keys": [ + { + "kid": "py-capability-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "gqL1GB3M3r0MBCjHc7ORpjfaLgZHY-PhyJwcg8V1y1c" + } + ], + "name": "Capability Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6ImFnZW50c2NvcmUtaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hL2lkZW50aXR5LzEiLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkNhcGFiaWxpdHkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9jLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiZ3FMMUdCM00zcjBNQkNqSGM3T1JwamZhTGdaSFktUGh5SndjZzhWMXkxYyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.4e8OMTQExso6-qIa4p2US1ViX7FBgfjX8Ey8iuaQPgJl2SkjjQs7PTBFa6h57W3Pk8JJgYWFCbFYvm7mJp4nBQ" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "gqL1GB3M3r0MBCjHc7ORpjfaLgZHY-PhyJwcg8V1y1c", + "kid": "py-capability-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-capability-EdDSA", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-multikey.json b/tests/fixtures/cross-lang/py-multikey.json new file mode 100644 index 0000000..3b6f990 --- /dev/null +++ b/tests/fixtures/cross-lang/py-multikey.json @@ -0,0 +1,64 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://mk.example.com" + } + ], + "capabilities": [], + "payment_handlers": [ + { + "name": "tempo", + "config": { + "rail": "tempo-mainnet" + } + } + ], + "signing_keys": [ + { + "kid": "py-multikey-old", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "CCNBoeaXWgTni7QcDtNohjUmhVEGHelxV3qLYHXZovk" + }, + { + "kid": "py-multikey-new", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "m-pu9Un4958pSkTHuM5laNjzrFxh8VyBh4cOguNyuMY" + } + ], + "name": "Multi-Key Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1vbGQiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiQ0NOQm9lYVhXZ1RuaTdRY0R0Tm9oalVtaFZFR0hlbHhWM3FMWUhYWm92ayJ9LHsiYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Im0tcHU5VW40OTU4cFNrVEh1TTVsYU5qenJGeGg4VnlCaDRjT2d1Tnl1TVkifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.QElsGEGovjoZtyMQX20MZwZ9JjmVUzTxYZ_V5z5z-Co-5uhNi49BAyV1QiBzbP54kZwm_WbwZ_x-9OVYw9rtCg" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "CCNBoeaXWgTni7QcDtNohjUmhVEGHelxV3qLYHXZovk", + "kid": "py-multikey-old", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + }, + { + "crv": "Ed25519", + "x": "m-pu9Un4958pSkTHuM5laNjzrFxh8VyBh4cOguNyuMY", + "kid": "py-multikey-new", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-multikey-new", + "generator": "python" +} diff --git a/tests/fixtures/cross-lang/py-unicode.json b/tests/fixtures/cross-lang/py-unicode.json new file mode 100644 index 0000000..71484d0 --- /dev/null +++ b/tests/fixtures/cross-lang/py-unicode.json @@ -0,0 +1,48 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://日本.example.com" + } + ], + "capabilities": [], + "payment_handlers": [ + { + "name": "tempo", + "config": { + "note": "メモ" + } + } + ], + "signing_keys": [ + { + "kid": "py-unicode-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "Sfrt68PbX5aBkynHGPHclnr0eKFfynzCIC0urH8-o9s" + } + ], + "name": "Café 日本 🍷 Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiU2ZydDY4UGJYNWFCa3luSEdQSGNsbnIwZUtGZnluekNJQzB1ckg4LW85cyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.GO42vYTPp0B8y1MjgN3iqMzby1vJmCzkOEWnV2O0C5ckUePV-QtTyRchmyAEttjr66HOSVEMyU8CgtfVirhCBg" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "Sfrt68PbX5aBkynHGPHclnr0eKFfynzCIC0urH8-o9s", + "kid": "py-unicode-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-unicode-EdDSA", + "generator": "python" +} diff --git a/tests/identity/cross-lang.test.ts b/tests/identity/cross-lang.test.ts index 88f0fa5..3c33fb8 100644 --- a/tests/identity/cross-lang.test.ts +++ b/tests/identity/cross-lang.test.ts @@ -38,10 +38,18 @@ describe('UCP signing — cross-language fixture corpus', () => { }); } - it('corpus contains both Node and Python fixtures', () => { + it('corpus covers the canonical scenarios from both languages', () => { + const names = fixtures.map((f) => f.name); const generators = new Set(fixtures.map((f) => f.data.generator)); expect(generators).toContain('node'); expect(generators).toContain('python'); - expect(fixtures.length).toBeGreaterThanOrEqual(6); + + // Each language ships 6 scenarios so cross-lang verify exercises all of them. + for (const lang of ['node', 'py'] as const) { + for (const scenario of ['minimal', 'es256-rails', 'extras-int', 'capability', 'unicode', 'multikey'] as const) { + expect(names).toContain(`${lang}-${scenario}.json`); + } + } + expect(fixtures.length).toBe(12); }); }); diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index 2766c55..4de851e 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -112,10 +112,15 @@ describe('UCP signing — canonicalization', () => { const profileA = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); const signed = await signUCPProfile(profileA, { signingKey: privateKey, kid: 'k' }); - // Re-construct the same profile with keys in different insertion order; should - // still verify because canonicalization sorts keys deterministically. - const reordered = JSON.parse(JSON.stringify(signed)); - const ok = await verifyUCPProfile(reordered, buildJWKSResponse([publicJWK])); + // Hand-construct the same profile with keys in REVERSE insertion order so + // canonicalization actually has work to do. JSON.parse(JSON.stringify(x)) + // preserves the source order, which is a vacuous round-trip — this version + // genuinely re-orders. + const reordered: Record = {}; + const sortedKeys = Object.keys(signed).sort().reverse(); + for (const k of sortedKeys) reordered[k] = (signed as Record)[k]; + expect(Object.keys(reordered)[0]).not.toBe(Object.keys(signed).sort()[0]); // sanity: order really differs + const ok = await verifyUCPProfile(reordered as never, buildJWKSResponse([publicJWK])); expect(ok).toBe(true); }); }); @@ -242,8 +247,11 @@ describe('UCP signing — security: alg-confusion + typ + dup-kid', () => { const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); const segments = signed.signature.split('.'); - // Flip last char of the signature segment. - const flipped = segments[2]!.slice(0, -1) + (segments[2]!.endsWith('A') ? 'B' : 'A'); + // Flip a char near the start of the signature segment (NOT the last char, + // which is partial padding bits and may not affect the decoded signature). + const sig = segments[2]!; + const flippedChar = sig[0] === 'A' ? 'B' : 'A'; + const flipped = flippedChar + sig.slice(1); const tampered = { ...signed, signature: `${segments[0]}.${segments[1]}.${flipped}` }; await expect(verifyUCPProfile(tampered, buildJWKSResponse([publicJWK]))) .rejects.toThrow(); @@ -272,12 +280,19 @@ describe('UCP signing — float canonicalization defense', () => { it('throws when signing a profile that contains a non-integer Number anywhere', async () => { const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); - // Inject a float into extras-shaped data so the canonicalizer sees it. (profile as unknown as Record).extras = { rate: 0.0125 }; await expect(signUCPProfile(profile, { signingKey: privateKey, kid: 'k' })) .rejects.toThrow(/non-integer Number/); }); + it('throws on NaN / Infinity', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + (profile as unknown as Record).extras = { value: Number.POSITIVE_INFINITY }; + await expect(signUCPProfile(profile, { signingKey: privateKey, kid: 'k' })) + .rejects.toThrow(/non-finite Number/); + }); + it('signing with integers + strings is fine', async () => { const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); @@ -286,3 +301,64 @@ describe('UCP signing — float canonicalization defense', () => { expect(await verifyUCPProfile(signed, buildJWKSResponse([publicJWK]))).toBe(true); }); }); + +describe('UCP signing — additional hardening', () => { + it('signUCPProfile throws when kid is not in profile.signing_keys[]', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'real' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + await expect(signUCPProfile(profile, { signingKey: privateKey, kid: 'wrong' })) + .rejects.toThrow(/not present in profile.signing_keys/); + }); + + it('verifyUCPProfile rejects malformed JWKS shape (missing keys array)', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + await expect(verifyUCPProfile(signed, {} as never)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'malformed_jwks' }); + }); + + it('verifyUCPProfile rejects null JWKS', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + await expect(verifyUCPProfile(signed, null as never)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'malformed_jwks' }); + }); + + it('verifyUCPProfile rejects JWKS where keys is not an array', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + await expect(verifyUCPProfile(signed, { keys: 'not-an-array' } as never)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'malformed_jwks' }); + }); + + it('verifyUCPProfile wraps unrecognized critical header into typed error', async () => { + const { generateUCPSigningKey, buildJWKSResponse, verifyUCPProfile } = await import('../../src/identity/ucp-jwks'); + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + + // Hand-craft a JWS with a critical header that the verifier doesn't recognize. + const { base64url } = await import('jose'); + const { sign } = await import('node:crypto'); + function ss(v: unknown): string { + if (v === null || typeof v !== 'object') return JSON.stringify(v); + if (Array.isArray(v)) return `[${v.map(ss).join(',')}]`; + const o = v as Record; + return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${ss(o[k])}`).join(',')}}`; + } + const canonical = ss(profile); + const headerJson = JSON.stringify({ alg: 'EdDSA', kid: 'k', typ: 'ucp-profile+jws', crit: ['fakething'], fakething: 'x' }); + const headerB64 = base64url.encode(new TextEncoder().encode(headerJson)); + const payloadB64 = base64url.encode(new TextEncoder().encode(canonical)); + const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`); + const sigBytes = sign(null, data, privateKey as Parameters[2]); + const sigB64 = base64url.encode(sigBytes); + const jws = `${headerB64}.${payloadB64}.${sigB64}`; + const signed = { ...profile, signature: jws }; + + await expect(verifyUCPProfile(signed as never, buildJWKSResponse([publicJWK]))) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'unrecognized_critical_header' }); + }); +}); From c9634a2d02978e09bb413862ed8bc0fefe1374c6 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 15:57:33 -0700 Subject: [PATCH 05/35] hardening: round-3 reviewer findings (parity + ergonomics) - 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) --- src/identity/ucp-jwks.ts | 19 +++++++++++-- src/identity/ucp.ts | 44 +++++++++++++++++++++++++++++- src/index.ts | 1 + tests/identity/ucp-signing.test.ts | 2 +- 4 files changed, 62 insertions(+), 4 deletions(-) diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index f03af9d..451164e 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -218,6 +218,9 @@ export async function signUCPProfile( // JWK with the matching kid; otherwise verifiers can't resolve the public // key and the profile is dead-on-arrival. Catch this at sign-time rather // than at verifier-time in production. + if (typeof opts.kid !== 'string' || !opts.kid) { + throw new Error('signUCPProfile: opts.kid must be a non-empty string.'); + } const kids = (profile.signing_keys ?? []).map((k) => (k as Record).kid); if (!kids.includes(opts.kid)) { throw new Error( @@ -266,7 +269,12 @@ export async function verifyUCPProfile( const stripped = { ...profile } as Partial; const sig = stripped.signature; delete stripped.signature; - if (!sig) throw new UCPVerificationError('no_signature', 'UCP profile has no `signature` field; expected JWS Compact Serialization.'); + if (typeof sig !== 'string' || !sig) { + throw new UCPVerificationError( + 'no_signature', + `UCP profile signature must be a non-empty string; got ${sig === undefined ? 'undefined' : typeof sig}.`, + ); + } const canonicalBody = canonicalizeProfile(stripped as UCPProfile); const expectedPayload = new TextEncoder().encode(canonicalBody); @@ -286,7 +294,14 @@ export async function verifyUCPProfile( throw new UCPVerificationError('wrong_typ', `UCP signature typ must be "${UCP_TYP}"; got ${String(header.typ)}.`); } const kid = header.kid; - if (!kid) throw new UCPVerificationError('missing_kid', 'UCP signature header missing `kid`.'); + // Strict string check — a non-string kid (number/bool/null) could + // accidentally match a JWK with an equal-typed kid and mask attacks. + if (typeof kid !== 'string' || !kid) { + throw new UCPVerificationError( + 'missing_kid', + `UCP signature header kid must be a non-empty string; got ${kid === undefined ? 'undefined' : typeof kid}.`, + ); + } const matches = jwks.keys.filter((k) => (k as Record).kid === kid); if (matches.length === 0) throw new UCPVerificationError('kid_not_found', `No JWK in JWKS matching kid=${kid}.`); if (matches.length > 1) throw new UCPVerificationError('duplicate_kid', `JWKS contains ${matches.length} keys with kid=${kid}; expected exactly one.`); diff --git a/src/identity/ucp.ts b/src/identity/ucp.ts index e7c855f..69e0429 100644 --- a/src/identity/ucp.ts +++ b/src/identity/ucp.ts @@ -37,6 +37,38 @@ export interface UCPSigningKey { [k: string]: unknown; } +/** + * Construct a UCPSigningKey from a public JWK dict (e.g. the `publicJWK` + * returned by `generateUCPSigningKey()`). Validates the JWK has required + * fields (`kid`, `kty`) and rejects symmetric (`oct`) keys, which can't + * publicly verify a JWS in trust-mode UCP. + * + * Symmetric to Python's `UCPSigningKey.from_jwk(public_jwk)` classmethod. + * + * Example: + * ```ts + * const { publicJWK } = await generateUCPSigningKey({ kid: 'merchant-2026-05' }); + * const profile = buildUCPProfile({ ..., signing_keys: [ucpSigningKeyFromJWK(publicJWK)] }); + * ``` + */ +export function ucpSigningKeyFromJWK(jwk: Record): UCPSigningKey { + if (!jwk || typeof jwk !== 'object') { + throw new Error(`ucpSigningKeyFromJWK expected a non-null object; got ${typeof jwk}.`); + } + if (typeof jwk.kid !== 'string' || !jwk.kid) { + throw new Error('ucpSigningKeyFromJWK: JWK missing required field `kid` (or non-string).'); + } + if (typeof jwk.kty !== 'string' || !jwk.kty) { + throw new Error('ucpSigningKeyFromJWK: JWK missing required field `kty` (or non-string).'); + } + if (jwk.kty !== 'OKP' && jwk.kty !== 'EC' && jwk.kty !== 'RSA') { + throw new Error( + `ucpSigningKeyFromJWK: kty=${JSON.stringify(jwk.kty)} is not a supported asymmetric key type (expected OKP, EC, or RSA). Symmetric \`oct\` keys are rejected because they cannot publicly verify a JWS in the trust-mode UCP flow.`, + ); + } + return jwk as unknown as UCPSigningKey; +} + export interface UCPService { /** Transport binding — `rest` / `mcp` / `a2a` / `embedded`. */ type: string; @@ -177,7 +209,17 @@ export function buildUCPProfile(input: BuildUCPProfileInput): UCPProfile { }; if (input.name !== undefined) profile.name = input.name; - if (input.extras) Object.assign(profile, input.extras); + if (input.extras) { + // Reserved-field collisions are rejected so a careless `extras: { signing_keys: [...] }` + // can't silently destroy the explicit field. + const RESERVED = new Set(['version', 'spec', 'services', 'capabilities', 'payment_handlers', 'signing_keys', 'name', 'signature']); + for (const k of Object.keys(input.extras)) { + if (RESERVED.has(k)) { + throw new Error(`buildUCPProfile: extras key "${k}" collides with a reserved profile field; rejected.`); + } + } + Object.assign(profile, input.extras); + } return profile; } diff --git a/src/index.ts b/src/index.ts index 17ba88a..084ac95 100644 --- a/src/index.ts +++ b/src/index.ts @@ -39,6 +39,7 @@ export { type UCPProfile, type UCPService, type UCPSigningKey, + ucpSigningKeyFromJWK, } from './identity/ucp'; export { buildJWKSResponse, diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index 4de851e..c5691fd 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -102,7 +102,7 @@ describe('UCP signing — signUCPProfile / verifyUCPProfile round-trip', () => { const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); await expect( verifyUCPProfile(profile as unknown as Awaited>, buildJWKSResponse([publicJWK])), - ).rejects.toThrow(/no `signature` field/); + ).rejects.toMatchObject({ name: 'UCPVerificationError', code: 'no_signature' }); }); }); From 2534df02c8616ff0e0b7a23d4d65247cfdf6e339 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 16:23:23 -0700 Subject: [PATCH 06/35] hardening(identity): round-4 UCP signing reviewer findings 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) --- README.md | 2 +- examples/signed-ucp-merchant.ts | 3 +- src/identity/ucp-jwks.ts | 21 +++++- tests/identity/ucp-signing.test.ts | 115 ++++++++++++++++++++++++++++- 4 files changed, 135 insertions(+), 6 deletions(-) diff --git a/README.md b/README.md index ee772a9..1dfc77b 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: publ const jwks = buildJWKSResponse([publicJWK]); ``` -`verifyUCPProfile` enforces the JWS protected header `typ: "ucp-profile+jws"`, restricts `alg` to `EdDSA` / `ES256`, requires a `kid`, rejects duplicate kids in the JWKS, and compares the canonical body bytes against the JWS payload to catch swap-after-sign tampering. Failures throw `UCPVerificationError` with a discriminated `code` (`no_signature` / `missing_kid` / `kid_not_found` / `duplicate_kid` / `unsupported_alg` / `wrong_typ` / `signature_invalid` / `body_mismatch` / `malformed_jws`). +`verifyUCPProfile` enforces the JWS protected header `typ: "ucp-profile+jws"`, restricts `alg` to `EdDSA` / `ES256`, requires a `kid`, rejects duplicate kids in the JWKS, and compares the canonical body bytes against the JWS payload to catch swap-after-sign tampering. Failures throw `UCPVerificationError` with a discriminated `code` (`no_signature` / `missing_kid` / `kid_not_found` / `duplicate_kid` / `unsupported_alg` / `wrong_typ` / `signature_invalid` / `body_mismatch` / `malformed_jws` / `malformed_jwks` / `unusable_key` / `unrecognized_critical_header`). `malformed_jwks` covers a JWKS argument that isn't a `{ keys: [...] }` document. `unusable_key` covers a matched JWK whose `use` is not `sig` (e.g. `enc`). `unrecognized_critical_header` covers a JWS whose `crit` header lists an extension the verifier doesn't understand (RFC 7515 §4.1.11). `signUCPProfile` rejects profiles containing non-integer `Number` values (cross-language float canonicalization is not stable; use decimal strings for monetary or fractional fields). diff --git a/examples/signed-ucp-merchant.ts b/examples/signed-ucp-merchant.ts index c9a4cd1..d67d9d9 100644 --- a/examples/signed-ucp-merchant.ts +++ b/examples/signed-ucp-merchant.ts @@ -26,6 +26,7 @@ import { generateUCPSigningKey, signUCPProfile, type GeneratedUCPKey, + ucpSigningKeyFromJWK, UCPVerificationError, verifyUCPProfile, } from '@agent-score/commerce'; @@ -79,7 +80,7 @@ app.get('/.well-known/ucp', async (c) => { name: 'My Agent Service', services: [{ type: 'rest', url: 'https://agents.example.com' }], payment_handlers: [{ name: 'tempo', config: { recipient: '0xfeedface' } }], - signing_keys: [key.publicJWK], + signing_keys: [ucpSigningKeyFromJWK(key.publicJWK as Record)], }); const signed = await signUCPProfile(profile, { signingKey: key.privateKey, diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index 451164e..225fc7b 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -214,6 +214,12 @@ export async function signUCPProfile( const jose = await loadJose(); const alg = opts.alg ?? 'EdDSA'; + if (!ALLOWED_ALGS.includes(alg as AllowedAlg)) { + throw new Error( + `signUCPProfile: alg ${JSON.stringify(opts.alg)} is not in the supported set [${ALLOWED_ALGS.join(', ')}].`, + ); + } + // Sign-time kid sanity check: the profile's `signing_keys[]` MUST contain a // JWK with the matching kid; otherwise verifiers can't resolve the public // key and the profile is dead-on-arrival. Catch this at sign-time rather @@ -255,6 +261,13 @@ export async function verifyUCPProfile( profile: SignedUCPProfile, jwks: JWKSResponse, ): Promise { + if (profile === null || typeof profile !== 'object' || Array.isArray(profile)) { + throw new UCPVerificationError( + 'no_signature', + `UCP profile must be a JSON object; got ${profile === null ? 'null' : Array.isArray(profile) ? 'array' : typeof profile}.`, + ); + } + const jose = await loadJose(); // JWKS shape guard so a malformed argument emits a typed UCPVerificationError @@ -302,9 +315,11 @@ export async function verifyUCPProfile( `UCP signature header kid must be a non-empty string; got ${kid === undefined ? 'undefined' : typeof kid}.`, ); } - const matches = jwks.keys.filter((k) => (k as Record).kid === kid); - if (matches.length === 0) throw new UCPVerificationError('kid_not_found', `No JWK in JWKS matching kid=${kid}.`); - if (matches.length > 1) throw new UCPVerificationError('duplicate_kid', `JWKS contains ${matches.length} keys with kid=${kid}; expected exactly one.`); + const matches = jwks.keys.filter( + (k) => k != null && typeof k === 'object' && (k as Record).kid === kid, + ); + if (matches.length === 0) throw new UCPVerificationError('kid_not_found', `No JWK in JWKS matching kid=${JSON.stringify(kid)}.`); + if (matches.length > 1) throw new UCPVerificationError('duplicate_kid', `JWKS contains ${matches.length} keys with kid=${JSON.stringify(kid)}; expected exactly one.`); // RFC 7517 §4.2: reject keys not intended for signature verification. const matchedKey = matches[0] as Record; if (matchedKey.use !== undefined && matchedKey.use !== 'sig') { diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index c5691fd..ac0a28c 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { buildUCPProfile } from '../../src/identity/ucp'; +import { buildUCPProfile, ucpSigningKeyFromJWK } from '../../src/identity/ucp'; import { buildJWKSResponse, generateUCPSigningKey, @@ -362,3 +362,116 @@ describe('UCP signing — additional hardening', () => { .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'unrecognized_critical_header' }); }); }); + +describe('ucpSigningKeyFromJWK', () => { + it('round-trips an EdDSA public JWK from generateUCPSigningKey', async () => { + const { publicJWK } = await generateUCPSigningKey({ kid: 'rt-eddsa', alg: 'EdDSA' }); + const result = ucpSigningKeyFromJWK(publicJWK as Record); + const r = result as Record; + expect(r.kid).toBe('rt-eddsa'); + expect(r.kty).toBe('OKP'); + expect(r.crv).toBe('Ed25519'); + expect(r.alg).toBe('EdDSA'); + expect(r.use).toBe('sig'); + expect(typeof r.x).toBe('string'); + }); + + it('round-trips an ES256 public JWK from generateUCPSigningKey', async () => { + const { publicJWK } = await generateUCPSigningKey({ kid: 'rt-es256', alg: 'ES256' }); + const result = ucpSigningKeyFromJWK(publicJWK as Record); + const r = result as Record; + expect(r.kid).toBe('rt-es256'); + expect(r.kty).toBe('EC'); + expect(r.crv).toBe('P-256'); + expect(r.alg).toBe('ES256'); + expect(r.use).toBe('sig'); + expect(typeof r.x).toBe('string'); + expect(typeof r.y).toBe('string'); + }); + + it('rejects symmetric oct keys', () => { + expect(() => + ucpSigningKeyFromJWK({ kid: 'k', kty: 'oct', k: 'AAAA' }), + ).toThrow(/asymmetric/i); + }); + + it('rejects JWK missing kid', () => { + expect(() => ucpSigningKeyFromJWK({ kty: 'OKP' })).toThrow(/kid/); + }); + + it('rejects JWK missing kty', () => { + expect(() => ucpSigningKeyFromJWK({ kid: 'k' })).toThrow(/kty/); + }); + + it('rejects non-object inputs', () => { + expect(() => ucpSigningKeyFromJWK(null as never)).toThrow(); + expect(() => ucpSigningKeyFromJWK('string' as never)).toThrow(); + expect(() => ucpSigningKeyFromJWK(42 as never)).toThrow(); + }); +}); + +describe('UCP signing — round-4 hardening', () => { + it('rejects a JWK with use=enc as unusable_key', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'enc-key', alg: 'EdDSA' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'enc-key', alg: 'EdDSA' }); + const badJWKS = { keys: [{ ...(publicJWK as Record), use: 'enc' }] }; + await expect(verifyUCPProfile(signed, badJWKS as never)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'unusable_key' }); + }); + + it('rejects non-string signature values with no_signature', async () => { + const { publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + for (const badSig of [42, null, [], {}]) { + const tampered = { ...profile, signature: badSig as unknown as string }; + await expect(verifyUCPProfile(tampered as never, buildJWKSResponse([publicJWK]))) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'no_signature' }); + } + }); + + it('returns kid_not_found when JWKS contains a null entry', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'real' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'real' }); + const badJWKS = { keys: [null] }; + await expect(verifyUCPProfile(signed, badJWKS as never)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'kid_not_found' }); + }); + + it('returns kid_not_found when JWKS contains a string entry', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'real' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'real' }); + const badJWKS = { keys: ['string-not-jwk'] }; + await expect(verifyUCPProfile(signed, badJWKS as never)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'kid_not_found' }); + }); + + it('rejects a JWS whose protected header decodes to a JSON array', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const { base64url } = await import('jose'); + const { sign } = await import('node:crypto'); + + function ss(v: unknown): string { + if (v === null || typeof v !== 'object') return JSON.stringify(v); + if (Array.isArray(v)) return `[${v.map(ss).join(',')}]`; + const o = v as Record; + return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${ss(o[k])}`).join(',')}}`; + } + const canonical = ss(profile); + + const headerJson = JSON.stringify(['EdDSA', 'kid-x']); + const headerB64 = base64url.encode(new TextEncoder().encode(headerJson)); + const payloadB64 = base64url.encode(new TextEncoder().encode(canonical)); + const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`); + const sigBytes = sign(null, data, privateKey as Parameters[2]); + const sigB64 = base64url.encode(sigBytes); + const jws = `${headerB64}.${payloadB64}.${sigB64}`; + const signed = { ...profile, signature: jws }; + + await expect(verifyUCPProfile(signed as never, buildJWKSResponse([publicJWK]))) + .rejects.toBeInstanceOf(UCPVerificationError); + }); +}); From 34d5e2746b043c14bfc9a6a99b7a88e50e39bd7c Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 16:48:33 -0700 Subject: [PATCH 07/35] hardening(identity): round-5 UCP signing reviewer findings 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) --- src/identity/ucp-jwks.ts | 37 ++++++++- tests/fixtures/cross-lang/node-i18n-keys.json | 48 ++++++++++++ tests/identity/cross-lang.test.ts | 7 +- tests/identity/ucp-signing.test.ts | 77 +++++++++++++++++++ 4 files changed, 166 insertions(+), 3 deletions(-) create mode 100644 tests/fixtures/cross-lang/node-i18n-keys.json diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index 225fc7b..5b05e02 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -130,6 +130,20 @@ function canonicalizeProfile(profile: UCPProfile): string { * must use decimal strings for monetary or fractional fields to preserve * byte parity with the Python sibling. */ function stableStringify(value: unknown): string { + if (value === undefined) { + throw new Error( + 'stableStringify: undefined values are not allowed in canonicalized JSON. ' + + 'Object fields with no value must be omitted.', + ); + } + if (typeof value === 'function' || typeof value === 'symbol') { + throw new Error(`stableStringify: ${typeof value} values are not allowed in canonicalized JSON.`); + } + if (value instanceof Date) { + throw new Error( + 'stableStringify: Date instances are not allowed; serialize to an ISO string before passing.', + ); + } if (typeof value === 'number') { if (!Number.isFinite(value)) { throw new Error( @@ -141,11 +155,25 @@ function stableStringify(value: unknown): string { `UCP profile canonicalization rejects non-integer Number ${value}. Use a decimal string (e.g. "9.99") for monetary or fractional fields to preserve cross-language byte-parity.`, ); } + if (!Number.isSafeInteger(value)) { + throw new Error( + `stableStringify: integer ${value} exceeds Number.MAX_SAFE_INTEGER. ` + + 'For values >2^53, use a decimal string to preserve cross-language byte parity.', + ); + } } if (value === null || typeof value !== 'object') return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`; const obj = value as Record; - const keys = Object.keys(obj).sort(); + const keys = Object.keys(obj).sort((a, b) => { + const aPoints = [...a].map((c) => c.codePointAt(0)!); + const bPoints = [...b].map((c) => c.codePointAt(0)!); + const len = Math.min(aPoints.length, bPoints.length); + for (let i = 0; i < len; i += 1) { + if (aPoints[i] !== bPoints[i]) return aPoints[i] - bPoints[i]; + } + return aPoints.length - bPoints.length; + }); const pairs = keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`); return `{${pairs.join(',')}}`; } @@ -325,6 +353,13 @@ export async function verifyUCPProfile( if (matchedKey.use !== undefined && matchedKey.use !== 'sig') { throw new UCPVerificationError('unusable_key', `JWK with kid=${kid} has use=${JSON.stringify(matchedKey.use)}; expected "sig".`); } + // RFC 7517 §4.4: a JWK with a declared `alg` field constrains its use to that algorithm. + if (matchedKey.alg !== undefined && matchedKey.alg !== header.alg) { + throw new UCPVerificationError( + 'unusable_key', + `JWK alg ${JSON.stringify(matchedKey.alg)} does not match JWS header alg ${JSON.stringify(header.alg)}.`, + ); + } return jose.importJWK(matches[0] as Parameters[0], header.alg); }, { algorithms: [...ALLOWED_ALGS] }, diff --git a/tests/fixtures/cross-lang/node-i18n-keys.json b/tests/fixtures/cross-lang/node-i18n-keys.json new file mode 100644 index 0000000..a20f9f3 --- /dev/null +++ b/tests/fixtures/cross-lang/node-i18n-keys.json @@ -0,0 +1,48 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://m.example.com" + } + ], + "capabilities": [], + "payment_handlers": [], + "signing_keys": [ + { + "kid": "node-i18n-keys-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "DzzfpM4ogPIGqyvOa-oHOpYuHK71xzg6YLmhN49xgQM" + } + ], + "name": "i18n Keys Merchant", + "extras": { + "café": "bmp-latin1", + "日本": "bmp-cjk", + "🍷": "non-bmp-supplementary", + "a": "ascii-low", + "z": "ascii-high" + }, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaTE4bi1rZXlzLUVkRFNBIiwidHlwIjoidWNwLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6ImFzY2lpLWxvdyIsImNhZsOpIjoiYm1wLWxhdGluMSIsInoiOiJhc2NpaS1oaWdoIiwi5pel5pysIjoiYm1wLWNqayIsIvCfjbciOiJub24tYm1wLXN1cHBsZW1lbnRhcnkifSwibmFtZSI6ImkxOG4gS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1pMThuLWtleXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiRHp6ZnBNNG9nUElHcXl2T2Etb0hPcFl1SEs3MXh6ZzZZTG1oTjQ5eGdRTSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.0SEYacPOx48si5D6ua4JbN51NNSeYZy2SDEnX2c7qN6b_xncr4kJlpdrEVgRH4cbMjWZq4CMD9DY0rHJ1QMPAQ" + }, + "jwks": { + "keys": [ + { + "kid": "node-i18n-keys-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "DzzfpM4ogPIGqyvOa-oHOpYuHK71xzg6YLmhN49xgQM" + } + ] + }, + "alg": "EdDSA", + "kid": "node-i18n-keys-EdDSA", + "generator": "node" +} diff --git a/tests/identity/cross-lang.test.ts b/tests/identity/cross-lang.test.ts index 3c33fb8..63d5f4c 100644 --- a/tests/identity/cross-lang.test.ts +++ b/tests/identity/cross-lang.test.ts @@ -44,12 +44,15 @@ describe('UCP signing — cross-language fixture corpus', () => { expect(generators).toContain('node'); expect(generators).toContain('python'); - // Each language ships 6 scenarios so cross-lang verify exercises all of them. + // Each language ships its base scenarios so cross-lang verify exercises all of them. + // `i18n-keys` exercises non-ASCII object keys (BMP + non-BMP); both languages must + // sort by Unicode codepoint to maintain byte parity. Node's lang prefix is `node`, + // python's is `py`. for (const lang of ['node', 'py'] as const) { for (const scenario of ['minimal', 'es256-rails', 'extras-int', 'capability', 'unicode', 'multikey'] as const) { expect(names).toContain(`${lang}-${scenario}.json`); } } - expect(fixtures.length).toBe(12); + expect(names).toContain('node-i18n-keys.json'); }); }); diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index ac0a28c..2db3c54 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -410,6 +410,83 @@ describe('ucpSigningKeyFromJWK', () => { }); }); +describe('UCP signing — JCS-incompatible value rejection', () => { + // Probe the internal stableStringify by signing a profile that holds the + // offending value. The signer canonicalizes via stableStringify, so any + // rejection there bubbles up through signUCPProfile. + async function signWith(extras: unknown): Promise { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + (profile as unknown as Record).extras = extras; + await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + } + + it('rejects undefined values in objects', async () => { + await expect(signWith({ a: undefined })).rejects.toThrow(/undefined values are not allowed/); + }); + + it('rejects undefined values inside arrays', async () => { + await expect(signWith([1, undefined, 3])).rejects.toThrow(/undefined values are not allowed/); + }); + + it('rejects function values', async () => { + await expect(signWith({ a: () => {} })).rejects.toThrow(/function values are not allowed/); + }); + + it('rejects Symbol values', async () => { + await expect(signWith({ a: Symbol('x') })).rejects.toThrow(/symbol values are not allowed/); + }); + + it('rejects Date instances', async () => { + await expect(signWith({ a: new Date() })).rejects.toThrow(/Date instances are not allowed/); + }); +}); + +describe('UCP signing — integer overflow defense', () => { + async function signWith(extras: unknown): Promise>> { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + (profile as unknown as Record).extras = extras; + return signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + } + + it('accepts Number.MAX_SAFE_INTEGER (2^53 - 1)', async () => { + await expect(signWith({ n: 9007199254740991 })).resolves.toBeDefined(); + }); + + it('rejects 2^53 (boundary, ambiguous in IEEE 754)', async () => { + await expect(signWith({ n: 9007199254740992 })).rejects.toThrow(/MAX_SAFE_INTEGER/); + }); + + it('rejects 2^53 + 1 as lossy', async () => { + await expect(signWith({ n: Number.MAX_SAFE_INTEGER + 2 })).rejects.toThrow(/MAX_SAFE_INTEGER/); + }); + + it('rejects -(2^53 + 1) as lossy', async () => { + await expect(signWith({ n: -(Number.MAX_SAFE_INTEGER + 2) })).rejects.toThrow(/MAX_SAFE_INTEGER/); + }); + + it('accepts Number.MAX_SAFE_INTEGER', async () => { + await expect(signWith({ n: Number.MAX_SAFE_INTEGER })).resolves.toBeDefined(); + }); + + it('rejects Number.MAX_SAFE_INTEGER + 1', async () => { + await expect(signWith({ n: Number.MAX_SAFE_INTEGER + 1 })).rejects.toThrow(/MAX_SAFE_INTEGER/); + }); +}); + +describe('UCP signing — JWK alg / header alg consistency', () => { + it('rejects when matched JWK alg does not match JWS header alg', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'mismatch', alg: 'EdDSA' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'mismatch', alg: 'EdDSA' }); + const lyingJWK = { ...(publicJWK as Record), alg: 'ES256' }; + const badJWKS = { keys: [lyingJWK] }; + await expect(verifyUCPProfile(signed, badJWKS as never)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'unusable_key' }); + }); +}); + describe('UCP signing — round-4 hardening', () => { it('rejects a JWK with use=enc as unusable_key', async () => { const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'enc-key', alg: 'EdDSA' }); From d8bb5b467003163715d4ac7c6134bf9ea6c07c89 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 17:11:37 -0700 Subject: [PATCH 08/35] hardening(identity): round-6 UCP signing reviewer findings 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) --- src/identity/ucp-jwks.ts | 11 ++++ src/identity/ucp.ts | 18 ++++++- .../fixtures/cross-lang/node-emoji-keys.json | 52 +++++++++++++++++++ tests/fixtures/cross-lang/node-i18n-keys.json | 48 ----------------- tests/fixtures/cross-lang/py-emoji-keys.json | 52 +++++++++++++++++++ tests/identity/cross-lang.test.ts | 20 +++++-- tests/identity/ucp-signing.test.ts | 36 +++++++++++-- tests/identity/ucp.test.ts | 21 ++++++++ 8 files changed, 199 insertions(+), 59 deletions(-) create mode 100644 tests/fixtures/cross-lang/node-emoji-keys.json delete mode 100644 tests/fixtures/cross-lang/node-i18n-keys.json create mode 100644 tests/fixtures/cross-lang/py-emoji-keys.json diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index 5b05e02..58f0fda 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -139,11 +139,22 @@ function stableStringify(value: unknown): string { if (typeof value === 'function' || typeof value === 'symbol') { throw new Error(`stableStringify: ${typeof value} values are not allowed in canonicalized JSON.`); } + if (typeof value === 'bigint') { + throw new Error('stableStringify: BigInt values are not allowed; use a decimal string.'); + } if (value instanceof Date) { throw new Error( 'stableStringify: Date instances are not allowed; serialize to an ISO string before passing.', ); } + if (value instanceof Map || value instanceof Set || value instanceof WeakMap || value instanceof WeakSet) { + throw new Error( + `stableStringify: ${value.constructor.name} values are not allowed; convert to a plain object/array first.`, + ); + } + if (ArrayBuffer.isView(value)) { + throw new Error('stableStringify: typed arrays are not allowed; convert to a plain array first.'); + } if (typeof value === 'number') { if (!Number.isFinite(value)) { throw new Error( diff --git a/src/identity/ucp.ts b/src/identity/ucp.ts index 69e0429..fc47fc8 100644 --- a/src/identity/ucp.ts +++ b/src/identity/ucp.ts @@ -211,8 +211,22 @@ export function buildUCPProfile(input: BuildUCPProfileInput): UCPProfile { if (input.name !== undefined) profile.name = input.name; if (input.extras) { // Reserved-field collisions are rejected so a careless `extras: { signing_keys: [...] }` - // can't silently destroy the explicit field. - const RESERVED = new Set(['version', 'spec', 'services', 'capabilities', 'payment_handlers', 'signing_keys', 'name', 'signature']); + // can't silently destroy the explicit field. `__proto__`, `constructor`, and `prototype` + // are reserved so vendor extras can't slip prototype-pollution payloads into the canonical + // body and surprise downstream consumers. + const RESERVED = new Set([ + 'version', + 'spec', + 'services', + 'capabilities', + 'payment_handlers', + 'signing_keys', + 'name', + 'signature', + '__proto__', + 'constructor', + 'prototype', + ]); for (const k of Object.keys(input.extras)) { if (RESERVED.has(k)) { throw new Error(`buildUCPProfile: extras key "${k}" collides with a reserved profile field; rejected.`); diff --git a/tests/fixtures/cross-lang/node-emoji-keys.json b/tests/fixtures/cross-lang/node-emoji-keys.json new file mode 100644 index 0000000..700e4f3 --- /dev/null +++ b/tests/fixtures/cross-lang/node-emoji-keys.json @@ -0,0 +1,52 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://emoji.example.com" + } + ], + "capabilities": [], + "payment_handlers": [ + { + "name": "tempo", + "config": {} + } + ], + "signing_keys": [ + { + "kid": "node-emoji-keys-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "SEqAXr_hDfmdqLqepK--97NMkVlYF_A1ByPa2xycou8" + } + ], + "name": "Emoji Keys Merchant", + "extras": { + "a": 1, + "豈": 2, + "": 3, + "🍷": 4 + }, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZW1vamkta2V5cy1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6MSwi6LGIIjoyLCLugIAiOjMsIvCfjbciOjR9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWVtb2ppLWtleXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiU0VxQVhyX2hEZm1kcUxxZXBLLS05N05Na1ZsWUZfQTFCeVBhMnh5Y291OCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.QD_zQMZ4UkUkuZQ-rNNEDrEalu2eYrI280Migljdk67UqHWMMOcB4nsBR9mj4E3RJ5M7sgAZ9CWWptdrcTqXCQ" + }, + "jwks": { + "keys": [ + { + "kid": "node-emoji-keys-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "SEqAXr_hDfmdqLqepK--97NMkVlYF_A1ByPa2xycou8" + } + ] + }, + "alg": "EdDSA", + "kid": "node-emoji-keys-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/node-i18n-keys.json b/tests/fixtures/cross-lang/node-i18n-keys.json deleted file mode 100644 index a20f9f3..0000000 --- a/tests/fixtures/cross-lang/node-i18n-keys.json +++ /dev/null @@ -1,48 +0,0 @@ -{ - "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://m.example.com" - } - ], - "capabilities": [], - "payment_handlers": [], - "signing_keys": [ - { - "kid": "node-i18n-keys-EdDSA", - "alg": "EdDSA", - "use": "sig", - "crv": "Ed25519", - "kty": "OKP", - "x": "DzzfpM4ogPIGqyvOa-oHOpYuHK71xzg6YLmhN49xgQM" - } - ], - "name": "i18n Keys Merchant", - "extras": { - "café": "bmp-latin1", - "日本": "bmp-cjk", - "🍷": "non-bmp-supplementary", - "a": "ascii-low", - "z": "ascii-high" - }, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaTE4bi1rZXlzLUVkRFNBIiwidHlwIjoidWNwLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6ImFzY2lpLWxvdyIsImNhZsOpIjoiYm1wLWxhdGluMSIsInoiOiJhc2NpaS1oaWdoIiwi5pel5pysIjoiYm1wLWNqayIsIvCfjbciOiJub24tYm1wLXN1cHBsZW1lbnRhcnkifSwibmFtZSI6ImkxOG4gS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1pMThuLWtleXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiRHp6ZnBNNG9nUElHcXl2T2Etb0hPcFl1SEs3MXh6ZzZZTG1oTjQ5eGdRTSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.0SEYacPOx48si5D6ua4JbN51NNSeYZy2SDEnX2c7qN6b_xncr4kJlpdrEVgRH4cbMjWZq4CMD9DY0rHJ1QMPAQ" - }, - "jwks": { - "keys": [ - { - "kid": "node-i18n-keys-EdDSA", - "alg": "EdDSA", - "use": "sig", - "crv": "Ed25519", - "kty": "OKP", - "x": "DzzfpM4ogPIGqyvOa-oHOpYuHK71xzg6YLmhN49xgQM" - } - ] - }, - "alg": "EdDSA", - "kid": "node-i18n-keys-EdDSA", - "generator": "node" -} diff --git a/tests/fixtures/cross-lang/py-emoji-keys.json b/tests/fixtures/cross-lang/py-emoji-keys.json new file mode 100644 index 0000000..aa3b589 --- /dev/null +++ b/tests/fixtures/cross-lang/py-emoji-keys.json @@ -0,0 +1,52 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "name": "Emoji Keys Merchant", + "services": [ + { + "type": "rest", + "url": "https://emoji.example.com" + } + ], + "capabilities": [], + "payment_handlers": [ + { + "name": "tempo", + "config": {} + } + ], + "signing_keys": [ + { + "crv": "Ed25519", + "x": "xrTm5ZIZUbFC1_S2Yw5KZkf-9m8--CmwP6-bkttx-ik", + "kid": "py-emoji-keys-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ], + "extras": { + "a": 1, + "豈": 2, + "": 3, + "🍷": 4 + }, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6MSwi6LGIIjoyLCLugIAiOjMsIvCfjbciOjR9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InhyVG01WklaVWJGQzFfUzJZdzVLWmtmLTltOC0tQ213UDYtYmt0dHgtaWsifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.O2ENDO4OJreRSvRZqbyMzbQlaG3SKy_zsfMFqqV6HUkwvIzmpH2bot_XtJzyz23RTsBdwvZtLxQJOSnBFkIfBQ" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "xrTm5ZIZUbFC1_S2Yw5KZkf-9m8--CmwP6-bkttx-ik", + "kid": "py-emoji-keys-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-emoji-keys-EdDSA", + "generator": "python" +} diff --git a/tests/identity/cross-lang.test.ts b/tests/identity/cross-lang.test.ts index 63d5f4c..1706808 100644 --- a/tests/identity/cross-lang.test.ts +++ b/tests/identity/cross-lang.test.ts @@ -45,14 +45,24 @@ describe('UCP signing — cross-language fixture corpus', () => { expect(generators).toContain('python'); // Each language ships its base scenarios so cross-lang verify exercises all of them. - // `i18n-keys` exercises non-ASCII object keys (BMP + non-BMP); both languages must - // sort by Unicode codepoint to maintain byte parity. Node's lang prefix is `node`, - // python's is `py`. + // `emoji-keys` exercises non-ASCII object keys with codepoints that genuinely + // distinguish UTF-16 first-unit sort from Unicode codepoint sort: BMP private use + // (U+E000) ranks BEFORE supplementary plane (U+1F377) by codepoint but AFTER it by + // UTF-16 first unit (because the high surrogate 55356 < 57344). Both repos ship the + // node and python emoji-keys fixtures so a regression in either language's key sort + // surfaces here. for (const lang of ['node', 'py'] as const) { - for (const scenario of ['minimal', 'es256-rails', 'extras-int', 'capability', 'unicode', 'multikey'] as const) { + for (const scenario of [ + 'minimal', + 'es256-rails', + 'extras-int', + 'capability', + 'unicode', + 'multikey', + 'emoji-keys', + ] as const) { expect(names).toContain(`${lang}-${scenario}.json`); } } - expect(names).toContain('node-i18n-keys.json'); }); }); diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index 2db3c54..eb1253c 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -440,6 +440,34 @@ describe('UCP signing — JCS-incompatible value rejection', () => { it('rejects Date instances', async () => { await expect(signWith({ a: new Date() })).rejects.toThrow(/Date instances are not allowed/); }); + + it('rejects BigInt values', async () => { + await expect(signWith({ a: 1n })).rejects.toThrow(/BigInt values are not allowed/); + }); + + it('rejects Map values', async () => { + await expect(signWith({ a: new Map([['x', 1]]) })).rejects.toThrow(/Map values are not allowed/); + }); + + it('rejects Set values', async () => { + await expect(signWith({ a: new Set([1, 2]) })).rejects.toThrow(/Set values are not allowed/); + }); + + it('rejects WeakMap values', async () => { + await expect(signWith({ a: new WeakMap() })).rejects.toThrow(/WeakMap values are not allowed/); + }); + + it('rejects WeakSet values', async () => { + await expect(signWith({ a: new WeakSet() })).rejects.toThrow(/WeakSet values are not allowed/); + }); + + it('rejects typed arrays (Uint8Array)', async () => { + await expect(signWith({ a: new Uint8Array([1, 2, 3]) })).rejects.toThrow(/typed arrays are not allowed/); + }); + + it('rejects typed arrays (Int32Array)', async () => { + await expect(signWith({ a: new Int32Array([1, 2, 3]) })).rejects.toThrow(/typed arrays are not allowed/); + }); }); describe('UCP signing — integer overflow defense', () => { @@ -458,12 +486,12 @@ describe('UCP signing — integer overflow defense', () => { await expect(signWith({ n: 9007199254740992 })).rejects.toThrow(/MAX_SAFE_INTEGER/); }); - it('rejects 2^53 + 1 as lossy', async () => { - await expect(signWith({ n: Number.MAX_SAFE_INTEGER + 2 })).rejects.toThrow(/MAX_SAFE_INTEGER/); + it('rejects 2^60 as lossy (well above MAX_SAFE_INTEGER, distinct float from 2^53)', async () => { + await expect(signWith({ n: 2 ** 60 })).rejects.toThrow(/MAX_SAFE_INTEGER/); }); - it('rejects -(2^53 + 1) as lossy', async () => { - await expect(signWith({ n: -(Number.MAX_SAFE_INTEGER + 2) })).rejects.toThrow(/MAX_SAFE_INTEGER/); + it('rejects -(2^60) as lossy', async () => { + await expect(signWith({ n: -(2 ** 60) })).rejects.toThrow(/MAX_SAFE_INTEGER/); }); it('accepts Number.MAX_SAFE_INTEGER', async () => { diff --git a/tests/identity/ucp.test.ts b/tests/identity/ucp.test.ts index e759f05..3464776 100644 --- a/tests/identity/ucp.test.ts +++ b/tests/identity/ucp.test.ts @@ -95,4 +95,25 @@ describe('buildUCPProfile', () => { const cap = profile.capabilities.find((c) => c.name === AGENTSCORE_UCP_CAPABILITY); expect(cap?.schema).toBe('https://custom.example/schema.json'); }); + + it.each([ + ['version'], + ['spec'], + ['services'], + ['capabilities'], + ['payment_handlers'], + ['signing_keys'], + ['name'], + ['signature'], + ['__proto__'], + ['constructor'], + ['prototype'], + ])('rejects extras key "%s" as a reserved-field collision', (k) => { + expect(() => + buildUCPProfile({ + ...baseInput, + extras: { [k]: 'attacker' }, + }), + ).toThrow(/collides with a reserved profile field/); + }); }); From debc41f0cfd8dc346d3f8ab301b24253ed7226e6 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 17:38:10 -0700 Subject: [PATCH 09/35] docs(ucp): round-7 README rotation Cache-Control note 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) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 1dfc77b..22de4b7 100644 --- a/README.md +++ b/README.md @@ -216,7 +216,7 @@ const jwks = buildJWKSResponse([publicJWK]); **Persisting the private JWK.** Mint once via `generateUCPSigningKey()`, export with `jose.exportJWK(privateKey)` to get the JSON-serializable form, store in your secret manager (AWS Secrets Manager, GCP Secret Manager, etc.). On each container start, read the secret, `jose.importJWK(jwk, alg)` to re-hydrate. Remote-signer flows (KMS-backed asymmetric keys) require an adapter layer that exposes a `KeyLike` jose can call; `jose` does not natively wrap KMS endpoints. -**Key rotation.** Mint a new key with a new `kid`, add the public JWK to your JWKS endpoint alongside the old one, then sign new profiles with the new key. Verifiers fetching the JWKS pick up both; any in-flight envelopes signed by the old key still verify until you remove that JWK from the JWKS. Drop the old JWK once your verifier-side cache TTL has elapsed. +**Key rotation.** Mint a new key with a new `kid`, add the public JWK to your JWKS endpoint alongside the old one, then sign new profiles with the new key. Verifiers fetching the JWKS pick up both; any in-flight envelopes signed by the old key still verify until you remove that JWK from the JWKS. 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. **Inline JWK in the profile vs separate JWKS endpoint.** UCP §6 mandates the separate `/.well-known/jwks.json` endpoint as the canonical trust source. The profile's `signing_keys[]` is informational; verifiers MUST resolve the kid against the JWKS (not the embedded copy), to prevent a swap-after-sign attack where a hostile actor replaces the inline key with their own. From a3f5e056b011345e898e4a5f202ec01a6bd3d745 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 18:19:35 -0700 Subject: [PATCH 10/35] hardening(identity): round-9 UCP signing reviewer findings 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) --- README.md | 2 +- scripts/generate-int-boundary-fixture.ts | 57 +++++++++++++++++++ .../cross-lang/node-int-boundary.json | 46 +++++++++++++++ .../fixtures/cross-lang/py-int-boundary.json | 48 ++++++++++++++++ tests/identity/cross-lang.test.ts | 1 + 5 files changed, 153 insertions(+), 1 deletion(-) create mode 100644 scripts/generate-int-boundary-fixture.ts create mode 100644 tests/fixtures/cross-lang/node-int-boundary.json create mode 100644 tests/fixtures/cross-lang/py-int-boundary.json diff --git a/README.md b/README.md index 22de4b7..ec54d2f 100644 --- a/README.md +++ b/README.md @@ -212,7 +212,7 @@ const jwks = buildJWKSResponse([publicJWK]); `verifyUCPProfile` enforces the JWS protected header `typ: "ucp-profile+jws"`, restricts `alg` to `EdDSA` / `ES256`, requires a `kid`, rejects duplicate kids in the JWKS, and compares the canonical body bytes against the JWS payload to catch swap-after-sign tampering. Failures throw `UCPVerificationError` with a discriminated `code` (`no_signature` / `missing_kid` / `kid_not_found` / `duplicate_kid` / `unsupported_alg` / `wrong_typ` / `signature_invalid` / `body_mismatch` / `malformed_jws` / `malformed_jwks` / `unusable_key` / `unrecognized_critical_header`). `malformed_jwks` covers a JWKS argument that isn't a `{ keys: [...] }` document. `unusable_key` covers a matched JWK whose `use` is not `sig` (e.g. `enc`). `unrecognized_critical_header` covers a JWS whose `crit` header lists an extension the verifier doesn't understand (RFC 7515 §4.1.11). -`signUCPProfile` rejects profiles containing non-integer `Number` values (cross-language float canonicalization is not stable; use decimal strings for monetary or fractional fields). +`signUCPProfile` rejects profiles containing non-integer `Number` values and integers outside `Number.MAX_SAFE_INTEGER` (cross-language float canonicalization is not stable; values past 2^53 lose precision when JS verifiers reparse the canonical body). Use decimal strings for monetary or fractional fields and for any integer that may exceed the safe range. **Persisting the private JWK.** Mint once via `generateUCPSigningKey()`, export with `jose.exportJWK(privateKey)` to get the JSON-serializable form, store in your secret manager (AWS Secrets Manager, GCP Secret Manager, etc.). On each container start, read the secret, `jose.importJWK(jwk, alg)` to re-hydrate. Remote-signer flows (KMS-backed asymmetric keys) require an adapter layer that exposes a `KeyLike` jose can call; `jose` does not natively wrap KMS endpoints. diff --git a/scripts/generate-int-boundary-fixture.ts b/scripts/generate-int-boundary-fixture.ts new file mode 100644 index 0000000..f327c57 --- /dev/null +++ b/scripts/generate-int-boundary-fixture.ts @@ -0,0 +1,57 @@ +/** + * One-shot generator for the int-boundary cross-lang fixture (Node side). + * + * Writes `tests/fixtures/cross-lang/node-int-boundary.json`. The fixture + * exercises the safe-integer boundary that BOTH languages must round-trip + * identically: `Number.MAX_SAFE_INTEGER` (2^53 - 1), its negative, zero, and + * small ints. Lossy values (>2^53) are NOT in the fixture (they're rejected + * at sign time); they're unit-tested in each language's signing path. + */ + +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { buildUCPProfile, type UCPSigningKey } from '../src/identity/ucp'; +import { + buildJWKSResponse, + generateUCPSigningKey, + signUCPProfile, +} from '../src/identity/ucp-jwks'; + +const OUT = join(__dirname, '..', 'tests', 'fixtures', 'cross-lang', 'node-int-boundary.json'); +const KID = 'node-int-boundary-EdDSA'; + +async function main(): Promise { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); + + const profile = buildUCPProfile({ + name: 'Int Boundary Merchant', + services: [{ type: 'rest', url: 'https://i.example.com' }], + payment_handlers: [], + signing_keys: [publicJWK as UCPSigningKey], + extras: { + max_safe_int: 9007199254740991, + min_safe_int: -9007199254740991, + small_int: 42, + neg_small_int: -42, + zero: 0, + }, + }); + + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); + + const fixture = { + profile: signed, + jwks: buildJWKSResponse([publicJWK]), + alg: 'EdDSA', + kid: KID, + generator: 'node', + }; + + writeFileSync(OUT, `${JSON.stringify(fixture, null, 2)}\n`); + console.warn(`wrote ${OUT}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/tests/fixtures/cross-lang/node-int-boundary.json b/tests/fixtures/cross-lang/node-int-boundary.json new file mode 100644 index 0000000..bf60b31 --- /dev/null +++ b/tests/fixtures/cross-lang/node-int-boundary.json @@ -0,0 +1,46 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://i.example.com" + } + ], + "capabilities": [], + "payment_handlers": [], + "signing_keys": [ + { + "kid": "node-int-boundary-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "uCH2zVsMZjpjmCGrrBSSmvWMftXFFCYDAUC5YG54XKw" + } + ], + "name": "Int Boundary Merchant", + "max_safe_int": 9007199254740991, + "min_safe_int": -9007199254740991, + "small_int": 42, + "neg_small_int": -42, + "zero": 0, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaW50LWJvdW5kYXJ5LUVkRFNBIiwidHlwIjoidWNwLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoidUNIMnpWc01aanBqbUNHcnJCU1NtdldNZnRYRkZDWURBVUM1WUc1NFhLdyJ9XSwic21hbGxfaW50Ijo0Miwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsInplcm8iOjB9.MABQW9Af3K1ThGkncreJJk-Pv2JdRssGkhO0-UHcZpQmnlriPCJJskL91sgaANfBfNMFRvq6v0xqWeAiMWPqDg" + }, + "jwks": { + "keys": [ + { + "kid": "node-int-boundary-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "uCH2zVsMZjpjmCGrrBSSmvWMftXFFCYDAUC5YG54XKw" + } + ] + }, + "alg": "EdDSA", + "kid": "node-int-boundary-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/py-int-boundary.json b/tests/fixtures/cross-lang/py-int-boundary.json new file mode 100644 index 0000000..b8b4481 --- /dev/null +++ b/tests/fixtures/cross-lang/py-int-boundary.json @@ -0,0 +1,48 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "name": "Int Boundary Merchant", + "services": [ + { + "type": "rest", + "url": "https://i.example.com" + } + ], + "capabilities": [], + "payment_handlers": [], + "signing_keys": [ + { + "crv": "Ed25519", + "x": "orncEOVmokkWyFRnJFYk1TeRC9nrMQG1Ip9kloaOd98", + "kid": "py-int-boundary-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ], + "extras": { + "max_safe_int": 9007199254740991, + "min_safe_int": -9007199254740991, + "small_int": 42, + "neg_small_int": -42, + "zero": 0 + }, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsibWF4X3NhZmVfaW50Ijo5MDA3MTk5MjU0NzQwOTkxLCJtaW5fc2FmZV9pbnQiOi05MDA3MTk5MjU0NzQwOTkxLCJuZWdfc21hbGxfaW50IjotNDIsInNtYWxsX2ludCI6NDIsInplcm8iOjB9LCJuYW1lIjoiSW50IEJvdW5kYXJ5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W10sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4Ijoib3JuY0VPVm1va2tXeUZSbkpGWWsxVGVSQzluck1RRzFJcDlrbG9hT2Q5OCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.p4tNJUnyRRHUtEBN3_y4DtuKk4CLBQnMfmGHz76wYYaxiAYa0oN251EC4PrkAHrZ6OlgKagTS027yisUf3qeDA" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "orncEOVmokkWyFRnJFYk1TeRC9nrMQG1Ip9kloaOd98", + "kid": "py-int-boundary-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-int-boundary-EdDSA", + "generator": "python" +} diff --git a/tests/identity/cross-lang.test.ts b/tests/identity/cross-lang.test.ts index 1706808..17f82e3 100644 --- a/tests/identity/cross-lang.test.ts +++ b/tests/identity/cross-lang.test.ts @@ -60,6 +60,7 @@ describe('UCP signing — cross-language fixture corpus', () => { 'unicode', 'multikey', 'emoji-keys', + 'int-boundary', ] as const) { expect(names).toContain(`${lang}-${scenario}.json`); } From 7f4db231cf9b006d94864514186c7b1ef62301c3 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 19:22:09 -0700 Subject: [PATCH 11/35] hardening(identity): round-11 UCP signing reviewer findings 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) --- src/identity/ucp-jwks.ts | 10 +++++- tests/identity/ucp-signing.test.ts | 54 ++++++++++++++++++++++++++++++ 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index 58f0fda..7027f23 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -328,7 +328,15 @@ export async function verifyUCPProfile( ); } - const canonicalBody = canonicalizeProfile(stripped as UCPProfile); + let canonicalBody: string; + try { + canonicalBody = canonicalizeProfile(stripped as UCPProfile); + } catch (err) { + throw new UCPVerificationError( + 'body_mismatch', + `Failed to canonicalize received profile for verification: ${err instanceof Error ? err.message : String(err)}`, + ); + } const expectedPayload = new TextEncoder().encode(canonicalBody); let signedPayload: Uint8Array; diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index eb1253c..6b92288 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -580,3 +580,57 @@ describe('UCP signing — round-4 hardening', () => { .rejects.toBeInstanceOf(UCPVerificationError); }); }); + +describe('UCP signing — verifier-side canonicalize must not leak raw Error', () => { + async function makeSigned(): Promise<{ + signed: Awaited>; + jwks: ReturnType; + }> { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + return { signed, jwks: buildJWKSResponse([publicJWK]) }; + } + + it('emits typed body_mismatch when received profile carries a non-integer Number', async () => { + const { signed, jwks } = await makeSigned(); + const tampered = { ...signed, extras: { n: 1.5 } } as unknown as typeof signed; + await expect(verifyUCPProfile(tampered, jwks)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'body_mismatch' }); + }); + + it('emits typed body_mismatch when received profile carries an unsafe-large Number', async () => { + const { signed, jwks } = await makeSigned(); + const tampered = { ...signed, extras: { n: Number.MAX_SAFE_INTEGER + 1 } } as unknown as typeof signed; + await expect(verifyUCPProfile(tampered, jwks)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'body_mismatch' }); + }); + + it('emits typed body_mismatch when received profile carries NaN', async () => { + const { signed, jwks } = await makeSigned(); + const tampered = { ...signed, extras: { n: NaN } } as unknown as typeof signed; + await expect(verifyUCPProfile(tampered, jwks)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'body_mismatch' }); + }); + + it('emits typed body_mismatch when received profile carries Infinity', async () => { + const { signed, jwks } = await makeSigned(); + const tampered = { ...signed, extras: { n: Infinity } } as unknown as typeof signed; + await expect(verifyUCPProfile(tampered, jwks)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'body_mismatch' }); + }); + + it('emits typed body_mismatch when received profile carries a BigInt', async () => { + const { signed, jwks } = await makeSigned(); + const tampered = { ...signed, extras: { n: 1n } } as unknown as typeof signed; + await expect(verifyUCPProfile(tampered, jwks)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'body_mismatch' }); + }); +}); + +describe('UCP signing — error precedence parity (profile-first)', () => { + it('null profile + malformed JWKS returns no_signature (profile-first)', async () => { + await expect(verifyUCPProfile(null as never, 'not a jwks' as never)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'no_signature' }); + }); +}); From 95cd95a02248ba28e997ad6473afaa847b311714 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 02:49:36 -0700 Subject: [PATCH 12/35] hardening(identity): align UCP verifier error precedence with python 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) --- src/identity/ucp-jwks.ts | 30 ++++++++++++++++++------------ tests/identity/ucp-signing.test.ts | 30 ++++++++++++++++++++++++++++++ 2 files changed, 48 insertions(+), 12 deletions(-) diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index 7027f23..495d77d 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -328,17 +328,12 @@ export async function verifyUCPProfile( ); } - let canonicalBody: string; - try { - canonicalBody = canonicalizeProfile(stripped as UCPProfile); - } catch (err) { - throw new UCPVerificationError( - 'body_mismatch', - `Failed to canonicalize received profile for verification: ${err instanceof Error ? err.message : String(err)}`, - ); - } - const expectedPayload = new TextEncoder().encode(canonicalBody); - + // Run compactVerify (which fires header validation via the key-resolver + // callback: typ → alg → kid → JWK lookup) BEFORE canonicalizing the stripped + // profile body. Header-level violations therefore take precedence over body + // canonicalization errors, matching the Python sibling's _peek_jws_header + // ordering. Cross-language parity means a profile with both a malformed body + // AND a malformed JWS header surfaces the same `code` in both SDKs. let signedPayload: Uint8Array; try { const verified = await jose.compactVerify( @@ -404,9 +399,20 @@ export async function verifyUCPProfile( throw err; } + let canonicalBody: string; + try { + canonicalBody = canonicalizeProfile(stripped as UCPProfile); + } catch (err) { + throw new UCPVerificationError( + 'body_mismatch', + `Failed to canonicalize received profile for verification: ${err instanceof Error ? err.message : String(err)}`, + ); + } + const expectedPayload = new TextEncoder().encode(canonicalBody); + // Compare the bytes that were actually signed against the canonical body of the // profile we received. `compactVerify` validates the JWS against the bytes embedded - // in the JWS payload segment — but the profile body could have been swapped after + // in the JWS payload segment, but the profile body could have been swapped after // signing while the JWS stayed unchanged. Body-vs-payload comparison closes that // gap. if (!constantTimeEqual(signedPayload, expectedPayload)) { diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index 6b92288..c8c4a8d 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -633,4 +633,34 @@ describe('UCP signing — error precedence parity (profile-first)', () => { await expect(verifyUCPProfile(null as never, 'not a jwks' as never)) .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'no_signature' }); }); + + it('mixed body-malformed + wrong-typ JWS emits wrong_typ (header-first like Python)', async () => { + // Build a profile, sign it with the wrong typ (typ="JWT" instead of + // ucp-profile+jws) so header validation rejects, then mutate the body + // to also carry a non-integer Number that would fail canonicalize. The + // verifier must surface `wrong_typ` (header-first), matching the Python + // sibling's _peek_jws_header order. + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const jose = await import('jose'); + const stripped = { ...profile } as Record; + delete stripped.signature; + const sortedJson = (() => { + const sort = (v: unknown): unknown => { + if (v === null || typeof v !== 'object') return v; + if (Array.isArray(v)) return v.map(sort); + return Object.keys(v as Record).sort().reduce>((acc, k) => { + acc[k] = sort((v as Record)[k]); + return acc; + }, {}); + }; + return JSON.stringify(sort(stripped)); + })(); + const wrongTypSig = await new jose.CompactSign(new TextEncoder().encode(sortedJson)) + .setProtectedHeader({ alg: 'EdDSA', kid: 'k', typ: 'JWT' }) + .sign(privateKey as Parameters[0]); + const tampered = { ...profile, signature: wrongTypSig, extras: { rate: 1.5 } } as never; + await expect(verifyUCPProfile(tampered, buildJWKSResponse([publicJWK]))) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'wrong_typ' }); + }); }); From e25b6483aef59ff505e21d4b5dea6bde7bfe66bd Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 02:58:53 -0700 Subject: [PATCH 13/35] hardening(identity): align UCP header check order with python sibling 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) --- src/identity/ucp-jwks.ts | 16 ++++++++++----- tests/identity/ucp-signing.test.ts | 31 ++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 5 deletions(-) diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index 495d77d..85cf73a 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -339,15 +339,22 @@ export async function verifyUCPProfile( const verified = await jose.compactVerify( sig, async (header) => { + // Header check order is typ → alg → kid to match the Python sibling's + // _peek_jws_header. A profile with multiple header faults (e.g. typ=JWT + // AND alg=HS256) must surface the same `code` from both SDKs; the + // `algorithms` option on compactVerify is intentionally omitted because + // jose enforces it BEFORE invoking this resolver, which would short-circuit + // typ before we could check it. The callback covers the same RFC 8725 §3.1 + // restriction below. + // RFC 8725 §3.11 — enforce expected typ to prevent cross-protocol token reuse. + if (header.typ !== UCP_TYP) { + throw new UCPVerificationError('wrong_typ', `UCP signature typ must be "${UCP_TYP}"; got ${String(header.typ)}.`); + } // RFC 8725 §3.1 — restrict to allow-listed algorithms before key resolution // so a hostile JWK can never be used with HS256/none/RS256/etc. if (!ALLOWED_ALGS.includes(header.alg as AllowedAlg)) { throw new UCPVerificationError('unsupported_alg', `UCP signing alg must be one of ${ALLOWED_ALGS.join(', ')}; got ${String(header.alg)}.`); } - // RFC 8725 §3.11 — enforce expected typ to prevent cross-protocol token reuse. - if (header.typ !== UCP_TYP) { - throw new UCPVerificationError('wrong_typ', `UCP signature typ must be "${UCP_TYP}"; got ${String(header.typ)}.`); - } const kid = header.kid; // Strict string check — a non-string kid (number/bool/null) could // accidentally match a JWK with an equal-typed kid and mask attacks. @@ -376,7 +383,6 @@ export async function verifyUCPProfile( } return jose.importJWK(matches[0] as Parameters[0], header.alg); }, - { algorithms: [...ALLOWED_ALGS] }, ); signedPayload = verified.payload; } catch (err) { diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index c8c4a8d..ce507fb 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -663,4 +663,35 @@ describe('UCP signing — error precedence parity (profile-first)', () => { await expect(verifyUCPProfile(tampered, buildJWKSResponse([publicJWK]))) .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'wrong_typ' }); }); + + it('mixed alg=HS256 + typ=JWT JWS emits wrong_typ (typ-first like Python)', async () => { + // Hand-craft a JWS whose protected header carries BOTH a wrong typ + // ("JWT") AND a disallowed alg ("HS256"). Python's _peek_jws_header + // checks typ before alg, so it surfaces wrong_typ; Node must do the + // same so cross-SDK error codes stay aligned for any caller routing + // on `code`. + const jose = await import('jose'); + const { publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const stripped = { ...profile } as Record; + delete stripped.signature; + const sortedJson = (() => { + const sort = (v: unknown): unknown => { + if (v === null || typeof v !== 'object') return v; + if (Array.isArray(v)) return v.map(sort); + return Object.keys(v as Record).sort().reduce>((acc, k) => { + acc[k] = sort((v as Record)[k]); + return acc; + }, {}); + }; + return JSON.stringify(sort(stripped)); + })(); + const sharedSecret = new Uint8Array(32).fill(0xab); + const mixedSig = await new jose.CompactSign(new TextEncoder().encode(sortedJson)) + .setProtectedHeader({ alg: 'HS256', kid: 'k', typ: 'JWT' }) + .sign(sharedSecret); + await expect( + verifyUCPProfile({ ...profile, signature: mixedSig } as never, buildJWKSResponse([publicJWK])), + ).rejects.toMatchObject({ name: 'UCPVerificationError', code: 'wrong_typ' }); + }); }); From d5897c0a3c52f0f7e1aa78a04ad80123d7a0fbb8 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 03:11:23 -0700 Subject: [PATCH 14/35] hardening(identity): pre-decode JWS header for crit+typ precedence parity 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) --- src/identity/ucp-jwks.ts | 90 ++++++++++++++++++++---------- tests/identity/ucp-signing.test.ts | 32 +++++++++++ 2 files changed, 93 insertions(+), 29 deletions(-) diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index 85cf73a..82b884e 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -328,36 +328,68 @@ export async function verifyUCPProfile( ); } - // Run compactVerify (which fires header validation via the key-resolver - // callback: typ → alg → kid → JWK lookup) BEFORE canonicalizing the stripped - // profile body. Header-level violations therefore take precedence over body - // canonicalization errors, matching the Python sibling's _peek_jws_header - // ordering. Cross-language parity means a profile with both a malformed body - // AND a malformed JWS header surfaces the same `code` in both SDKs. + // Pre-decode the protected header so typ → alg → kid → crit checks run BEFORE + // jose's compactVerify. jose enforces `crit` internally ahead of the key-resolver + // callback, which would surface `unrecognized_critical_header` on a JWS that + // also has a wrong typ; the python-commerce sibling's `_peek_jws_header` decodes + // the header manually and checks typ first. Mirroring that ordering here means + // a JWS with multiple header faults emits the same `code` in both SDKs. + let header: { alg?: unknown; kid?: unknown; typ?: unknown; crit?: unknown }; + try { + const protectedB64 = sig.split('.')[0]; + if (!protectedB64) throw new Error('JWS protected header segment is empty.'); + const headerJson = new TextDecoder().decode(jose.base64url.decode(protectedB64)); + const parsed = JSON.parse(headerJson); + if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) { + throw new Error('JWS protected header is not a JSON object.'); + } + header = parsed as { alg?: unknown; kid?: unknown; typ?: unknown; crit?: unknown }; + } catch (err) { + throw new UCPVerificationError( + 'malformed_jws', + `JWS protected header is not valid base64url-encoded JSON: ${err instanceof Error ? err.message : String(err)}`, + ); + } + + // Header check order is typ → alg → kid → crit to match the Python sibling's + // _peek_jws_header. RFC 8725 §3.11: enforce expected typ to prevent + // cross-protocol token reuse. + if (header.typ !== UCP_TYP) { + throw new UCPVerificationError('wrong_typ', `UCP signature typ must be "${UCP_TYP}"; got ${String(header.typ)}.`); + } + // RFC 8725 §3.1: restrict to allow-listed algorithms before key resolution + // so a hostile JWK can never be used with HS256/none/RS256/etc. + if (!ALLOWED_ALGS.includes(header.alg as AllowedAlg)) { + throw new UCPVerificationError('unsupported_alg', `UCP signing alg must be one of ${ALLOWED_ALGS.join(', ')}; got ${String(header.alg)}.`); + } + // Strict string check: a non-string kid (number/bool/null) could accidentally + // match a JWK with an equal-typed kid and mask attacks. + if (typeof header.kid !== 'string' || !header.kid) { + throw new UCPVerificationError( + 'missing_kid', + `UCP signature header kid must be a non-empty string; got ${header.kid === undefined ? 'undefined' : typeof header.kid}.`, + ); + } + // RFC 7515 §4.1.11 / RFC 8725 §3.10: reject any JWS whose `crit` header + // advertises an extension we don't understand. UCP defines no `crit` headers, + // so any non-empty `crit` array is unrecognized by definition. + if (Array.isArray(header.crit) && header.crit.length > 0) { + throw new UCPVerificationError( + 'unrecognized_critical_header', + `JWS protected header advertises unrecognized crit headers: ${JSON.stringify(header.crit)}.`, + ); + } + let signedPayload: Uint8Array; try { const verified = await jose.compactVerify( sig, - async (header) => { - // Header check order is typ → alg → kid to match the Python sibling's - // _peek_jws_header. A profile with multiple header faults (e.g. typ=JWT - // AND alg=HS256) must surface the same `code` from both SDKs; the - // `algorithms` option on compactVerify is intentionally omitted because - // jose enforces it BEFORE invoking this resolver, which would short-circuit - // typ before we could check it. The callback covers the same RFC 8725 §3.1 - // restriction below. - // RFC 8725 §3.11 — enforce expected typ to prevent cross-protocol token reuse. - if (header.typ !== UCP_TYP) { - throw new UCPVerificationError('wrong_typ', `UCP signature typ must be "${UCP_TYP}"; got ${String(header.typ)}.`); - } - // RFC 8725 §3.1 — restrict to allow-listed algorithms before key resolution - // so a hostile JWK can never be used with HS256/none/RS256/etc. - if (!ALLOWED_ALGS.includes(header.alg as AllowedAlg)) { - throw new UCPVerificationError('unsupported_alg', `UCP signing alg must be one of ${ALLOWED_ALGS.join(', ')}; got ${String(header.alg)}.`); - } - const kid = header.kid; - // Strict string check — a non-string kid (number/bool/null) could - // accidentally match a JWK with an equal-typed kid and mask attacks. + async (h) => { + // typ/alg/kid/crit were validated up-front against the pre-decoded header; + // this resolver only handles JWK lookup. Re-checking kid here keeps the + // jose API satisfied and provides defense-in-depth against any header + // re-parse divergence between this code path and jose's internals. + const kid = h.kid; if (typeof kid !== 'string' || !kid) { throw new UCPVerificationError( 'missing_kid', @@ -375,13 +407,13 @@ export async function verifyUCPProfile( throw new UCPVerificationError('unusable_key', `JWK with kid=${kid} has use=${JSON.stringify(matchedKey.use)}; expected "sig".`); } // RFC 7517 §4.4: a JWK with a declared `alg` field constrains its use to that algorithm. - if (matchedKey.alg !== undefined && matchedKey.alg !== header.alg) { + if (matchedKey.alg !== undefined && matchedKey.alg !== h.alg) { throw new UCPVerificationError( 'unusable_key', - `JWK alg ${JSON.stringify(matchedKey.alg)} does not match JWS header alg ${JSON.stringify(header.alg)}.`, + `JWK alg ${JSON.stringify(matchedKey.alg)} does not match JWS header alg ${JSON.stringify(h.alg)}.`, ); } - return jose.importJWK(matches[0] as Parameters[0], header.alg); + return jose.importJWK(matches[0] as Parameters[0], h.alg); }, ); signedPayload = verified.payload; diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index ce507fb..4c8c02e 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -694,4 +694,36 @@ describe('UCP signing — error precedence parity (profile-first)', () => { verifyUCPProfile({ ...profile, signature: mixedSig } as never, buildJWKSResponse([publicJWK])), ).rejects.toMatchObject({ name: 'UCPVerificationError', code: 'wrong_typ' }); }); + + it('mixed crit + wrong typ JWS emits wrong_typ (typ-first like Python)', async () => { + // Hand-craft a JWS whose protected header carries BOTH a wrong typ ("JWT") + // AND an unrecognized crit header ("fakething"). jose's compactVerify + // enforces `crit` BEFORE invoking the key-resolver callback, so without + // the pre-decode pass this would surface `unrecognized_critical_header`. + // Python's _peek_jws_header decodes manually and checks typ first; Node + // mirrors that ordering so the same input emits the same `code` in both + // SDKs. + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'real' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const { base64url } = await import('jose'); + const { sign } = await import('node:crypto'); + function ss(v: unknown): string { + if (v === null || typeof v !== 'object') return JSON.stringify(v); + if (Array.isArray(v)) return `[${v.map(ss).join(',')}]`; + const o = v as Record; + return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${ss(o[k])}`).join(',')}}`; + } + const canonical = ss(profile); + const headerJson = JSON.stringify({ alg: 'EdDSA', typ: 'JWT', kid: 'real', crit: ['fakething'], fakething: 'x' }); + const headerB64 = base64url.encode(new TextEncoder().encode(headerJson)); + const payloadB64 = base64url.encode(new TextEncoder().encode(canonical)); + const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`); + const sigBytes = sign(null, data, privateKey as Parameters[2]); + const sigB64 = base64url.encode(sigBytes); + const jws = `${headerB64}.${payloadB64}.${sigB64}`; + const signed = { ...profile, signature: jws }; + + await expect(verifyUCPProfile(signed as never, buildJWKSResponse([publicJWK]))) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'wrong_typ' }); + }); }); From e14057f884fcf69f1bbc28924244b494b76f21e0 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 03:54:56 -0700 Subject: [PATCH 15/35] hardening(identity): reject U+2028/U+2029 in stableStringify 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) --- src/identity/ucp-jwks.ts | 15 +++++++++ tests/identity/ucp-signing.test.ts | 52 ++++++++++++++++++++++++++++++ 2 files changed, 67 insertions(+) diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index 82b884e..abe2060 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -173,6 +173,21 @@ function stableStringify(value: unknown): string { ); } } + if (typeof value === 'string') { + // Cross-language byte parity: pre-ES2019 V8 (and any environment whose + // JSON.stringify still escapes U+2028 / U+2029) emits \u2028 / \u2029 + // for these codepoints, while Python's json.dumps with ensure_ascii=False + // emits them raw. A string carrying either would canonicalize to different + // bytes across the Node and Python siblings and break signature + // verification at the language boundary. Mirror the rejection in + // core/api/src/lib/canonicalize.ts so the contract stays symmetric. + if (value.includes('\u2028') || value.includes('\u2029')) { + throw new Error( + 'stableStringify: strings containing U+2028 (LINE SEPARATOR) or U+2029 (PARAGRAPH SEPARATOR) are not allowed; cross-language byte parity requires neither be present (Node JSON.stringify on older V8 escapes them; Python json.dumps with ensure_ascii=False does not).', + ); + } + return JSON.stringify(value); + } if (value === null || typeof value !== 'object') return JSON.stringify(value); if (Array.isArray(value)) return `[${value.map(stableStringify).join(',')}]`; const obj = value as Record; diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index 4c8c02e..c06ca59 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -727,3 +727,55 @@ describe('UCP signing — error precedence parity (profile-first)', () => { .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'wrong_typ' }); }); }); + +describe('UCP signing — U+2028 / U+2029 rejection', () => { + // Modern V8 emits U+2028 / U+2029 raw from JSON.stringify, so on today's Node + // the divergence with Python json.dumps(ensure_ascii=False) is theoretical. + // The rejection mirrors core/api/src/lib/canonicalize.ts so the contract + // stays symmetric for any pre-ES2019 verifier (older V8, browser-side + // verifier code) where JSON.stringify still escapes these codepoints. + async function signWith(extras: unknown): Promise { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + (profile as unknown as Record).extras = extras; + await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + } + + it('rejects strings containing U+2028 (LINE SEPARATOR) at top level', async () => { + await expect(signWith({ note: 'before
after' })) + .rejects.toThrow(/U\+2028/); + }); + + it('rejects strings containing U+2029 (PARAGRAPH SEPARATOR) at top level', async () => { + await expect(signWith({ note: 'before
after' })) + .rejects.toThrow(/U\+2029/); + }); + + it('rejects U+2028 nested inside an array', async () => { + await expect(signWith({ items: ['ok', 'bad
tail'] })) + .rejects.toThrow(/U\+2028/); + }); + + it('rejects U+2029 nested inside an array', async () => { + await expect(signWith({ items: ['ok', 'bad
tail'] })) + .rejects.toThrow(/U\+2029/); + }); + + it('rejects U+2028 nested inside an object value', async () => { + await expect(signWith({ deep: { inner: 'before
after' } })) + .rejects.toThrow(/U\+2028/); + }); + + it('rejects U+2029 nested inside an object value', async () => { + await expect(signWith({ deep: { inner: 'before
after' } })) + .rejects.toThrow(/U\+2029/); + }); + + it('accepts U+2027 (HYPHENATION POINT) as sanity case — different codepoint, not a target', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + (profile as unknown as Record).extras = { note: 'before‧after' }; + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + expect(await verifyUCPProfile(signed, buildJWKSResponse([publicJWK]))).toBe(true); + }); +}); From 9627a21f45458946373581d76ed71cc81a204591 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 04:50:01 -0700 Subject: [PATCH 16/35] hardening(identity): align buildUCPProfile claims coalescing with python 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) --- .../generate-data-driven-claims-fixture.ts | 71 +++++++++++++++++++ src/identity/ucp.ts | 12 ++-- .../cross-lang/node-data-driven-claims.json | 57 +++++++++++++++ .../cross-lang/py-data-driven-claims.json | 57 +++++++++++++++ tests/identity/cross-lang.test.ts | 4 ++ tests/identity/ucp.test.ts | 52 ++++++++++++++ 6 files changed, 249 insertions(+), 4 deletions(-) create mode 100644 scripts/generate-data-driven-claims-fixture.ts create mode 100644 tests/fixtures/cross-lang/node-data-driven-claims.json create mode 100644 tests/fixtures/cross-lang/py-data-driven-claims.json diff --git a/scripts/generate-data-driven-claims-fixture.ts b/scripts/generate-data-driven-claims-fixture.ts new file mode 100644 index 0000000..8dcbf03 --- /dev/null +++ b/scripts/generate-data-driven-claims-fixture.ts @@ -0,0 +1,71 @@ +/** + * One-shot generator for the data-driven-claims cross-lang fixture (Node side). + * + * Writes `tests/fixtures/cross-lang/node-data-driven-claims.json`. Unlike the + * other cross-lang fixtures (which hand-craft the `agentscore-identity` + * capability), this one EXERCISES `buildUCPProfile`'s data path: it constructs + * a synthetic `AgentScoreData` with the API-shape "missing" sentinels (empty + * string for kyc_level, null for age_bracket / jurisdiction / verified_at) and + * lets the builder coalesce them. Both languages MUST emit identical canonical + * bytes for this input or cross-lang verify drifts silently in production. + */ + +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { buildUCPProfile, type UCPSigningKey } from '../src/identity/ucp'; +import { + buildJWKSResponse, + generateUCPSigningKey, + signUCPProfile, +} from '../src/identity/ucp-jwks'; +import type { AgentScoreData } from '../src/core'; + +const OUT = join(__dirname, '..', 'tests', 'fixtures', 'cross-lang', 'node-data-driven-claims.json'); +const KID = 'node-data-driven-claims-EdDSA'; + +async function main(): Promise { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); + + const data: AgentScoreData = { + decision: 'allow', + decision_reasons: [], + resolved_operator: 'op_data_driven', + verify_url: 'https://agentscore.sh/verify/op_data_driven', + account_verification: { + // Empty string is the API's "set but unknown" shape for some columns; + // null is the shape for others. The builder must coerce both to the + // schema default identically across node and python. + kyc_level: '', + sanctions_clear: false, + age_bracket: null as unknown as string, + jurisdiction: null as unknown as string, + verified_at: null, + }, + }; + + const profile = buildUCPProfile({ + name: 'Data Driven Claims Merchant', + services: [{ type: 'rest', url: 'https://d.example.com' }], + payment_handlers: [], + signing_keys: [publicJWK as UCPSigningKey], + data, + }); + + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); + + const fixture = { + profile: signed, + jwks: buildJWKSResponse([publicJWK]), + alg: 'EdDSA', + kid: KID, + generator: 'node', + }; + + writeFileSync(OUT, `${JSON.stringify(fixture, null, 2)}\n`); + console.warn(`wrote ${OUT}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/identity/ucp.ts b/src/identity/ucp.ts index fc47fc8..68c0f93 100644 --- a/src/identity/ucp.ts +++ b/src/identity/ucp.ts @@ -180,13 +180,17 @@ export function buildUCPProfile(input: BuildUCPProfileInput): UCPProfile { if (operatorId) { const operatorVerification = input.data.operator_verification; const accountVerification = input.data.account_verification; + // `||` (not `??`) coerces both null/undefined AND empty string to the default, + // matching the python sibling. The API can return `account_verification` with + // either null or `""` for un-set fields depending on the row state, and a + // profile signed in one language must verify in the other across both shapes. const claims: Record = { operator_id: operatorId, - kyc_level: accountVerification?.kyc_level ?? operatorVerification?.level ?? 'none', + kyc_level: accountVerification?.kyc_level || operatorVerification?.level || 'none', sanctions_clear: accountVerification?.sanctions_clear === true, - age_bracket: accountVerification?.age_bracket ?? 'unknown', - jurisdiction: accountVerification?.jurisdiction ?? '', - verified_at: accountVerification?.verified_at ?? operatorVerification?.verified_at ?? null, + age_bracket: accountVerification?.age_bracket || 'unknown', + jurisdiction: accountVerification?.jurisdiction || '', + verified_at: accountVerification?.verified_at || operatorVerification?.verified_at || null, verify_url: input.data.verify_url ?? null, issuer: 'https://agentscore.sh', }; diff --git a/tests/fixtures/cross-lang/node-data-driven-claims.json b/tests/fixtures/cross-lang/node-data-driven-claims.json new file mode 100644 index 0000000..65a77b6 --- /dev/null +++ b/tests/fixtures/cross-lang/node-data-driven-claims.json @@ -0,0 +1,57 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://d.example.com" + } + ], + "capabilities": [ + { + "name": "agentscore-identity", + "version": "1", + "schema": "https://agentscore.sh/schemas/ucp/agentscore-identity.v1.json", + "claims": { + "operator_id": "op_data_driven", + "kyc_level": "none", + "sanctions_clear": false, + "age_bracket": "unknown", + "jurisdiction": "", + "verified_at": null, + "verify_url": "https://agentscore.sh/verify/op_data_driven", + "issuer": "https://agentscore.sh" + } + } + ], + "payment_handlers": [], + "signing_keys": [ + { + "kid": "node-data-driven-claims-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "1GQBzacuSLmz5l6LPHluSWLNI1xgcriiRdqs9sO22hY" + } + ], + "name": "Data Driven Claims Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwidHlwIjoidWNwLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoiYWdlbnRzY29yZS1pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9hZ2VudHNjb3JlLWlkZW50aXR5LnYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1kYXRhLWRyaXZlbi1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiMUdRQnphY3VTTG16NWw2TFBIbHVTV0xOSTF4Z2NyaWlSZHFzOXNPMjJoWSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.yBVx0_My6D8OAF-g6866FiM24IChFrfQqE5IPhhoxHiNO8qjgBRlE0MCGhUdW0i-3mF8TroUsnsaVv0NV_vbDw" + }, + "jwks": { + "keys": [ + { + "kid": "node-data-driven-claims-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "1GQBzacuSLmz5l6LPHluSWLNI1xgcriiRdqs9sO22hY" + } + ] + }, + "alg": "EdDSA", + "kid": "node-data-driven-claims-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/py-data-driven-claims.json b/tests/fixtures/cross-lang/py-data-driven-claims.json new file mode 100644 index 0000000..8e31df0 --- /dev/null +++ b/tests/fixtures/cross-lang/py-data-driven-claims.json @@ -0,0 +1,57 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://d.example.com" + } + ], + "capabilities": [ + { + "name": "agentscore-identity", + "schema": "https://agentscore.sh/schemas/ucp/agentscore-identity.v1.json", + "version": "1", + "claims": { + "operator_id": "op_data_driven", + "kyc_level": "none", + "sanctions_clear": false, + "age_bracket": "unknown", + "jurisdiction": "", + "verified_at": null, + "verify_url": "https://agentscore.sh/verify/op_data_driven", + "issuer": "https://agentscore.sh" + } + } + ], + "payment_handlers": [], + "signing_keys": [ + { + "kid": "py-data-driven-claims-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "e0tM2PG2SrWLVh2twzUQqc4wVi5isQJTWZLWe9Jceqg" + } + ], + "name": "Data Driven Claims Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoiYWdlbnRzY29yZS1pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9hZ2VudHNjb3JlLWlkZW50aXR5LnYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImUwdE0yUEcyU3JXTFZoMnR3elVRcWM0d1ZpNWlzUUpUV1pMV2U5SmNlcWcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.IRSaAW3aI_uT0YBakBlQ_DalJNlvmiID89pmeK2avjS1rZ1FWTjTnYv4fHYbkolTYKYSW4PNC8rV4hTYtPOzDg" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "e0tM2PG2SrWLVh2twzUQqc4wVi5isQJTWZLWe9Jceqg", + "kid": "py-data-driven-claims-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-data-driven-claims-EdDSA", + "generator": "python" +} diff --git a/tests/identity/cross-lang.test.ts b/tests/identity/cross-lang.test.ts index 17f82e3..d7c3472 100644 --- a/tests/identity/cross-lang.test.ts +++ b/tests/identity/cross-lang.test.ts @@ -61,6 +61,10 @@ describe('UCP signing — cross-language fixture corpus', () => { 'multikey', 'emoji-keys', 'int-boundary', + // `data-driven-claims` is the only fixture in the corpus that exercises + // `buildUCPProfile` / `build_ucp_profile`'s data path (vs. hand-crafted + // capabilities). Catches drift in `account_verification` coalescing. + 'data-driven-claims', ] as const) { expect(names).toContain(`${lang}-${scenario}.json`); } diff --git a/tests/identity/ucp.test.ts b/tests/identity/ucp.test.ts index 3464776..bac166d 100644 --- a/tests/identity/ucp.test.ts +++ b/tests/identity/ucp.test.ts @@ -116,4 +116,56 @@ describe('buildUCPProfile', () => { }), ).toThrow(/collides with a reserved profile field/); }); + + // Empty-string and null normalization: the API can emit + // `account_verification` with either null or `""` for un-set fields, and the + // node + python siblings must produce the SAME canonical claims block for + // either shape so a profile signed in one language verifies in the other. + describe('account_verification missing-value normalization (cross-lang parity)', () => { + const baseDataWithOp = { + decision: 'allow', + decision_reasons: [], + resolved_operator: 'op_abc', + }; + + const claimsOf = (av: AgentScoreData['account_verification']) => { + const profile = buildUCPProfile({ + ...baseInput, + data: { ...baseDataWithOp, account_verification: av } as AgentScoreData, + }); + const cap = profile.capabilities.find((c) => c.name === AGENTSCORE_UCP_CAPABILITY) as Record< + string, + unknown + >; + return cap.claims as Record; + }; + + it('coerces empty-string kyc_level to "none"', () => { + expect(claimsOf({ kyc_level: '' }).kyc_level).toBe('none'); + }); + + it('coerces null age_bracket to "unknown"', () => { + expect(claimsOf({ age_bracket: null as unknown as string }).age_bracket).toBe('unknown'); + }); + + it('coerces empty-string age_bracket to "unknown"', () => { + expect(claimsOf({ age_bracket: '' }).age_bracket).toBe('unknown'); + }); + + it('coerces null jurisdiction to ""', () => { + expect(claimsOf({ jurisdiction: null as unknown as string }).jurisdiction).toBe(''); + }); + + it('coerces empty-string jurisdiction to ""', () => { + expect(claimsOf({ jurisdiction: '' }).jurisdiction).toBe(''); + }); + + it('coerces null verified_at to null', () => { + expect(claimsOf({ verified_at: null }).verified_at).toBeNull(); + }); + + it('coerces empty-string verified_at to null', () => { + expect(claimsOf({ verified_at: '' }).verified_at).toBeNull(); + }); + }); }); From 4293500ed821d51b9f10db9bc8634fb6467441f2 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 05:55:57 -0700 Subject: [PATCH 17/35] hardening(identity): round-26 UCP signing reviewer findings 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) --- src/identity/ucp-jwks.ts | 20 +++++++++-- tests/identity/ucp-signing.test.ts | 53 ++++++++++++++++++++++++++++++ 2 files changed, 71 insertions(+), 2 deletions(-) diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index abe2060..2818e99 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -200,6 +200,18 @@ function stableStringify(value: unknown): string { } return aPoints.length - bPoints.length; }); + // Cross-language byte parity: same rejection rationale as the string-value + // branch above. Object keys flow through JSON.stringify(k) at the pairs line + // below, so without this check a key carrying U+2028 / U+2029 would pass on + // modern V8 but Python's _reject_unsafe_numbers (which recurses into dict + // keys) would throw at verify time. + for (const k of keys) { + if (k.includes('
') || k.includes('
')) { + throw new Error( + 'stableStringify: object keys containing U+2028 (LINE SEPARATOR) or U+2029 (PARAGRAPH SEPARATOR) are not allowed; cross-language byte parity (Node JSON.stringify on older V8 escapes them; Python json.dumps with ensure_ascii=False does not).', + ); + } + } const pairs = keys.map((k) => `${JSON.stringify(k)}:${stableStringify(obj[k])}`); return `{${pairs.join(',')}}`; } @@ -417,12 +429,16 @@ export async function verifyUCPProfile( if (matches.length === 0) throw new UCPVerificationError('kid_not_found', `No JWK in JWKS matching kid=${JSON.stringify(kid)}.`); if (matches.length > 1) throw new UCPVerificationError('duplicate_kid', `JWKS contains ${matches.length} keys with kid=${JSON.stringify(kid)}; expected exactly one.`); // RFC 7517 §4.2: reject keys not intended for signature verification. + // `use` and `alg` are optional per RFC 7517; an explicit JSON null is + // out-of-spec but treat it as absent (skip-on-null) so a JWK with + // `"use": null` matches Python's `is not None` semantics in + // ucp_jwks.py and the two languages stay symmetric. const matchedKey = matches[0] as Record; - if (matchedKey.use !== undefined && matchedKey.use !== 'sig') { + if (matchedKey.use != null && matchedKey.use !== 'sig') { throw new UCPVerificationError('unusable_key', `JWK with kid=${kid} has use=${JSON.stringify(matchedKey.use)}; expected "sig".`); } // RFC 7517 §4.4: a JWK with a declared `alg` field constrains its use to that algorithm. - if (matchedKey.alg !== undefined && matchedKey.alg !== h.alg) { + if (matchedKey.alg != null && matchedKey.alg !== h.alg) { throw new UCPVerificationError( 'unusable_key', `JWK alg ${JSON.stringify(matchedKey.alg)} does not match JWS header alg ${JSON.stringify(h.alg)}.`, diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index c06ca59..63bad74 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -778,4 +778,57 @@ describe('UCP signing — U+2028 / U+2029 rejection', () => { const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); expect(await verifyUCPProfile(signed, buildJWKSResponse([publicJWK]))).toBe(true); }); + + // Object-key rejection: same cross-language byte-parity rationale as the + // value-side rejection above. Python's _reject_unsafe_numbers recurses into + // dict keys, so a Node-signed profile with U+2028 / U+2029 in an object key + // would canonicalize cleanly here but throw body_mismatch on Python verify. + it('rejects an object key containing U+2028 (LINE SEPARATOR)', async () => { + await expect(signWith({ 'bad
key': 'value' })) + .rejects.toThrow(/U\+2028/); + }); + + it('rejects a nested object key containing U+2029 (PARAGRAPH SEPARATOR)', async () => { + await expect(signWith({ outer: { 'bad
key': 'value' } })) + .rejects.toThrow(/U\+2029/); + }); + + it('accepts an object key containing U+2027 (HYPHENATION POINT) as sanity case', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + (profile as unknown as Record).extras = { 'fine‧key': 'value' }; + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'k' }); + expect(await verifyUCPProfile(signed, buildJWKSResponse([publicJWK]))).toBe(true); + }); +}); + +describe('UCP signing — JWK use/alg null treated as absent', () => { + // RFC 7517 lists `use` and `alg` as optional. JSON null for these fields is + // out-of-spec but harmless; treat null as absent so the Node verifier + // matches Python's `is not None` semantics and a JWK with explicit nulls + // doesn't reject in one language and pass in the other. + it('verifies successfully when matched JWK has use=null', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'null-use' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'null-use' }); + const jwksWithNullUse = { keys: [{ ...(publicJWK as Record), use: null }] }; + expect(await verifyUCPProfile(signed, jwksWithNullUse as never)).toBe(true); + }); + + it('verifies successfully when matched JWK has alg=null', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'null-alg', alg: 'EdDSA' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'null-alg', alg: 'EdDSA' }); + const jwksWithNullAlg = { keys: [{ ...(publicJWK as Record), alg: null }] }; + expect(await verifyUCPProfile(signed, jwksWithNullAlg as never)).toBe(true); + }); + + it('still rejects use=enc with unusable_key (sanity: non-null wrong values still fail)', async () => { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'enc-sanity', alg: 'EdDSA' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: 'enc-sanity', alg: 'EdDSA' }); + const badJWKS = { keys: [{ ...(publicJWK as Record), use: 'enc' }] }; + await expect(verifyUCPProfile(signed, badJWKS as never)) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'unusable_key' }); + }); }); From b9510a696f890927b2ae54499c570990d837b9ef Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 06:01:29 -0700 Subject: [PATCH 18/35] hardening(identity): align crit shape validation with python sibling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/identity/ucp-jwks.ts | 19 +++++++++++----- tests/identity/ucp-signing.test.ts | 36 ++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 5 deletions(-) diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index 2818e99..fb2eafa 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -397,13 +397,22 @@ export async function verifyUCPProfile( `UCP signature header kid must be a non-empty string; got ${header.kid === undefined ? 'undefined' : typeof header.kid}.`, ); } - // RFC 7515 §4.1.11 / RFC 8725 §3.10: reject any JWS whose `crit` header - // advertises an extension we don't understand. UCP defines no `crit` headers, - // so any non-empty `crit` array is unrecognized by definition. - if (Array.isArray(header.crit) && header.crit.length > 0) { + // RFC 7515 §4.1.11: `crit` MUST be a non-empty array of strings if present. + // Shape-check first (matches python-commerce's malformed_jws split) so that + // explicit `crit: null` / `crit: []` / `crit: "foo"` / `crit: [42]` aren't + // silently accepted; only well-formed crit arrays fall through to the + // unrecognized-extension check (RFC 8725 §3.10 — UCP defines no crit headers). + if ('crit' in header) { + const crit = (header as { crit?: unknown }).crit; + if (!Array.isArray(crit) || crit.length === 0 || !crit.every((c) => typeof c === 'string')) { + throw new UCPVerificationError( + 'malformed_jws', + `JWS protected header crit must be a non-empty array of strings; got ${JSON.stringify(crit)}.`, + ); + } throw new UCPVerificationError( 'unrecognized_critical_header', - `JWS protected header advertises unrecognized crit headers: ${JSON.stringify(header.crit)}.`, + `JWS protected header advertises unrecognized crit headers: ${JSON.stringify(crit)}.`, ); } diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index 63bad74..2edff55 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -361,6 +361,42 @@ describe('UCP signing — additional hardening', () => { await expect(verifyUCPProfile(signed as never, buildJWKSResponse([publicJWK]))) .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'unrecognized_critical_header' }); }); + + // RFC 7515 §4.1.11: crit MUST be a non-empty array of strings if present. + // The four cases below mirror python-commerce's malformed_jws parity tests so + // a malformed crit shape never silently falls through to the unrecognized + // branch (or worse, gets accepted) on either SDK. + it.each([ + { label: 'null', crit: null as unknown }, + { label: 'empty array', crit: [] as unknown }, + { label: 'string (not array)', crit: 'fakething' as unknown }, + { label: 'array with non-string element', crit: [42] as unknown }, + ])('verifyUCPProfile rejects malformed crit ($label) with malformed_jws', async ({ crit }) => { + const { generateUCPSigningKey, buildJWKSResponse, verifyUCPProfile } = await import('../../src/identity/ucp-jwks'); + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); + const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); + + const { base64url } = await import('jose'); + const { sign } = await import('node:crypto'); + function ss(v: unknown): string { + if (v === null || typeof v !== 'object') return JSON.stringify(v); + if (Array.isArray(v)) return `[${v.map(ss).join(',')}]`; + const o = v as Record; + return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${ss(o[k])}`).join(',')}}`; + } + const canonical = ss(profile); + const headerJson = JSON.stringify({ alg: 'EdDSA', kid: 'k', typ: 'ucp-profile+jws', crit }); + const headerB64 = base64url.encode(new TextEncoder().encode(headerJson)); + const payloadB64 = base64url.encode(new TextEncoder().encode(canonical)); + const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`); + const sigBytes = sign(null, data, privateKey as Parameters[2]); + const sigB64 = base64url.encode(sigBytes); + const jws = `${headerB64}.${payloadB64}.${sigB64}`; + const signed = { ...profile, signature: jws }; + + await expect(verifyUCPProfile(signed as never, buildJWKSResponse([publicJWK]))) + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'malformed_jws' }); + }); }); describe('ucpSigningKeyFromJWK', () => { From 18aabccac142a6acaf945c272ad1a88959dc4bbd Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 06:53:35 -0700 Subject: [PATCH 19/35] hardening(identity): add typed-claims cross-lang fixture 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) --- scripts/generate-typed-claims-fixture.ts | 74 +++++++++++++++++++ .../cross-lang/node-typed-claims.json | 57 ++++++++++++++ .../fixtures/cross-lang/py-typed-claims.json | 57 ++++++++++++++ tests/identity/cross-lang.test.ts | 12 ++- 4 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 scripts/generate-typed-claims-fixture.ts create mode 100644 tests/fixtures/cross-lang/node-typed-claims.json create mode 100644 tests/fixtures/cross-lang/py-typed-claims.json diff --git a/scripts/generate-typed-claims-fixture.ts b/scripts/generate-typed-claims-fixture.ts new file mode 100644 index 0000000..68b6bbf --- /dev/null +++ b/scripts/generate-typed-claims-fixture.ts @@ -0,0 +1,74 @@ +/** + * One-shot generator for the typed-claims cross-lang fixture (Node side). + * + * Writes `tests/fixtures/cross-lang/node-typed-claims.json`. Sibling to + * `generate-data-driven-claims-fixture.ts` but exercises the **typed** + * `AgentScoreData.account_verification` / `AgentScoreData.operator_verification` + * read path (no raw fallback) so cross-lang verify catches drift on the + * typed-field-only call site. Python's `build_ucp_profile` reads the typed + * fields first without consulting raw when they are present, so both + * languages must emit the identical canonical bytes for this hand-constructed + * input shape. + */ + +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { buildUCPProfile, type UCPSigningKey } from '../src/identity/ucp'; +import { + buildJWKSResponse, + generateUCPSigningKey, + signUCPProfile, +} from '../src/identity/ucp-jwks'; +import type { AgentScoreData } from '../src/core'; + +const OUT = join(__dirname, '..', 'tests', 'fixtures', 'cross-lang', 'node-typed-claims.json'); +const KID = 'node-typed-claims-EdDSA'; + +async function main(): Promise { + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); + + const data: AgentScoreData = { + decision: 'allow', + decision_reasons: [], + resolved_operator: 'op_typed_claims', + verify_url: 'https://agentscore.sh/verify/op_typed_claims', + operator_verification: { + level: 'enhanced', + operator_type: 'api', + verified_at: '2026-04-01T00:00:00Z', + }, + account_verification: { + kyc_level: 'enhanced', + sanctions_clear: true, + age_bracket: '21+', + jurisdiction: 'US', + verified_at: '2026-04-01T00:00:00Z', + }, + }; + + const profile = buildUCPProfile({ + name: 'Typed Claims Merchant', + services: [{ type: 'rest', url: 'https://t.example.com' }], + payment_handlers: [], + signing_keys: [publicJWK as UCPSigningKey], + data, + }); + + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); + + const fixture = { + profile: signed, + jwks: buildJWKSResponse([publicJWK]), + alg: 'EdDSA', + kid: KID, + generator: 'node', + }; + + writeFileSync(OUT, `${JSON.stringify(fixture, null, 2)}\n`); + console.warn(`wrote ${OUT}`); +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/tests/fixtures/cross-lang/node-typed-claims.json b/tests/fixtures/cross-lang/node-typed-claims.json new file mode 100644 index 0000000..aebf243 --- /dev/null +++ b/tests/fixtures/cross-lang/node-typed-claims.json @@ -0,0 +1,57 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://t.example.com" + } + ], + "capabilities": [ + { + "name": "agentscore-identity", + "version": "1", + "schema": "https://agentscore.sh/schemas/ucp/agentscore-identity.v1.json", + "claims": { + "operator_id": "op_typed_claims", + "kyc_level": "enhanced", + "sanctions_clear": true, + "age_bracket": "21+", + "jurisdiction": "US", + "verified_at": "2026-04-01T00:00:00Z", + "verify_url": "https://agentscore.sh/verify/op_typed_claims", + "issuer": "https://agentscore.sh" + } + } + ], + "payment_handlers": [], + "signing_keys": [ + { + "kid": "node-typed-claims-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "hkhmYJSOPyC7tC2baujBsjvTdDs0M2gnmiTGEm_H9y0" + } + ], + "name": "Typed Claims Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwidHlwIjoidWNwLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoiYWdlbnRzY29yZS1pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9hZ2VudHNjb3JlLWlkZW50aXR5LnYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS10eXBlZC1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiaGtobVlKU09QeUM3dEMyYmF1akJzanZUZERzME0yZ25taVRHRW1fSDl5MCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.GJZcFBMvdIPmELSrUGzu--PmKwjItbpV74peSvcJcXRk6DRHgivYZOaTOPjFgZgOqnvhAEeG-gvy4O6jP5NrCA" + }, + "jwks": { + "keys": [ + { + "kid": "node-typed-claims-EdDSA", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "kty": "OKP", + "x": "hkhmYJSOPyC7tC2baujBsjvTdDs0M2gnmiTGEm_H9y0" + } + ] + }, + "alg": "EdDSA", + "kid": "node-typed-claims-EdDSA", + "generator": "node" +} diff --git a/tests/fixtures/cross-lang/py-typed-claims.json b/tests/fixtures/cross-lang/py-typed-claims.json new file mode 100644 index 0000000..af21d35 --- /dev/null +++ b/tests/fixtures/cross-lang/py-typed-claims.json @@ -0,0 +1,57 @@ +{ + "profile": { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "services": [ + { + "type": "rest", + "url": "https://t.example.com" + } + ], + "capabilities": [ + { + "name": "agentscore-identity", + "schema": "https://agentscore.sh/schemas/ucp/agentscore-identity.v1.json", + "version": "1", + "claims": { + "operator_id": "op_typed_claims", + "kyc_level": "enhanced", + "sanctions_clear": true, + "age_bracket": "21+", + "jurisdiction": "US", + "verified_at": "2026-04-01T00:00:00Z", + "verify_url": "https://agentscore.sh/verify/op_typed_claims", + "issuer": "https://agentscore.sh" + } + } + ], + "payment_handlers": [], + "signing_keys": [ + { + "kid": "py-typed-claims-EdDSA", + "kty": "OKP", + "alg": "EdDSA", + "use": "sig", + "crv": "Ed25519", + "x": "Qu9H2p75WjLc0DCdYY7MTaTkDZ0YPBFKHH3jsZMjFiA" + } + ], + "name": "Typed Claims Merchant", + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoiYWdlbnRzY29yZS1pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9hZ2VudHNjb3JlLWlkZW50aXR5LnYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IlF1OUgycDc1V2pMYzBEQ2RZWTdNVGFUa0RaMFlQQkZLSEgzanNaTWpGaUEifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.Awkp_QIMwjiiBE4CSiZQBkxXNdxwGBIPW36sAFIngbax_otu5N5S2kBlnt4xUhvRCJ-_CHieGCPJseIXa0i9Dg" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "Qu9H2p75WjLc0DCdYY7MTaTkDZ0YPBFKHH3jsZMjFiA", + "kid": "py-typed-claims-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-typed-claims-EdDSA", + "generator": "python" +} diff --git a/tests/identity/cross-lang.test.ts b/tests/identity/cross-lang.test.ts index d7c3472..101ba7e 100644 --- a/tests/identity/cross-lang.test.ts +++ b/tests/identity/cross-lang.test.ts @@ -61,10 +61,16 @@ describe('UCP signing — cross-language fixture corpus', () => { 'multikey', 'emoji-keys', 'int-boundary', - // `data-driven-claims` is the only fixture in the corpus that exercises - // `buildUCPProfile` / `build_ucp_profile`'s data path (vs. hand-crafted - // capabilities). Catches drift in `account_verification` coalescing. + // `data-driven-claims` exercises the raw-dict fallback read path + // (`AssessResult(raw={"account_verification": {...}})`) that production + // callers populate. `typed-claims` exercises the typed field path + // (`AssessResult(account_verification={...}, raw=None)`) that + // hand-constructed callers use — Node's `buildUCPProfile` reads typed + // fields directly without consulting raw, so both paths must produce + // byte-identical canonical bytes across languages or cross-lang verify + // silently drifts. 'data-driven-claims', + 'typed-claims', ] as const) { expect(names).toContain(`${lang}-${scenario}.json`); } From 27f3a3cfb72ecb1e5c0719eb737a8e8e82618255 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 08:24:28 -0700 Subject: [PATCH 20/35] docs: drop em dashes, update example count, add signed-ucp-merchant 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) --- CLAUDE.md | 45 +++++++++++++++++++-------------------- CONTRIBUTING.md | 2 +- README.md | 52 +++++++++++++++++++++++----------------------- examples/README.md | 9 ++++---- 4 files changed, 55 insertions(+), 53 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index be153ec..829de41 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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) | @@ -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 | |---|---| @@ -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 | |---|---| @@ -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`) for UCP §6 trust-mode verifiers. 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 */ }); @@ -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 @@ -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 diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b59db24..d0fa70c 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -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 diff --git a/README.md b/README.md index ec54d2f..76a0bd5 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![npm version](https://img.shields.io/npm/v/@agent-score/commerce.svg)](https://www.npmjs.com/package/@agent-score/commerce) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -The full merchant-side SDK for [AgentScore](https://agentscore.sh) — agent commerce in one install. Ships identity gating, payment rail helpers, 402 challenge builders, MPP discovery, and Stripe multichain support. Built and maintained by AgentScore; works with any 402/MPP merchant in the ecosystem, AgentScore-gated or not. +The full merchant-side SDK for [AgentScore](https://agentscore.sh): agent commerce in one install. Ships identity gating, payment rail helpers, 402 challenge builders, MPP discovery, and Stripe multichain support. Built and maintained by AgentScore; works with any 402/MPP merchant in the ecosystem, AgentScore-gated or not. ## Install @@ -13,7 +13,7 @@ npm install @agent-score/commerce bun add @agent-score/commerce ``` -Framework + protocol packages are optional peer deps — install only what you use: +Framework + protocol packages are optional peer deps; install only what you use: ```bash npm install hono mppx @x402/core @x402/evm @solana/mpp @solana/kit stripe # whatever your stack needs @@ -24,11 +24,11 @@ npm install hono mppx @x402/core @x402/evm @solana/mpp @solana/kit stripe # wh | Subpath | What it provides | |---|---| | `/identity/{hono,express,fastify,nextjs,web}` | Trust gate middleware: KYC, sanctions, age, jurisdiction. `agentscoreGate(...)`, `getAgentScoreData(c)`, `captureWallet(...)`, `verifyWalletSignerMatch(...)`. Plus shared denial helpers: `denialReasonStatus`, `denialReasonToBody`, `buildSignerMismatchBody`, `buildContactSupportNextSteps`, `verificationAgentInstructions`, `isFixableDenial`, `FIXABLE_DENIAL_REASONS`. | -| `/payment` | `networks`, `USDC`, `rails` registries; `paymentDirective`, `buildPaymentDirective`, `wwwAuthenticateHeader`, `paymentRequiredHeader`, `aliasAmountFields` (v1↔v2 amount field shim — emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlementOverrideHeader`, `dispatchSettlementByNetwork`, `extractPaymentSigner` (returns `{address, network}`); `createX402Server`, `createMppxServer`; drop-in x402 helpers: `validateX402NetworkConfig` (boot-time guard), `verifyX402Request` (parse + validate inbound X-Payment), `processX402Settle` (verify-then-settle with one call), `classifyX402SettleResult` (maps the tagged settle result to a recommended HTTP status / code / nextSteps so merchants get a controlled envelope without coupling to facilitator-specific error text). | -| `/discovery` | `isDiscoveryProbeRequest`, `buildDiscoveryProbeResponse` (with optional `x402Sample` for x402-aware crawlers — `awal x402 details` etc.), `sampleX402AcceptForNetwork` (USDC sample-accept builder for known CAIP-2 networks), `buildWellKnownMpp`, `buildLlmsTxt` + `llmsTxtIdentitySection` + `llmsTxtPaymentSection` (compact + verbose modes), `buildSkillMd` (Claude-Skill-compatible `/skill.md` agent-discovery manifest — strictly agent-facing data only, no internal posture), `agentscoreOpenApiSnippets`, `createBazaarDiscovery`, `noindexNonDiscoveryPaths` (Hono middleware that emits `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces — defaults cover `/openapi.json`, `/llms.txt`, `/skill.md`, `/.well-known/{mpp.json,agent-card.json,ucp,jwks.json}`, `/favicon.{png,ico}`; pure helpers `isDiscoveryPath` + `defaultDiscoveryPaths` for non-Hono frameworks). | -| `/challenge` | `build402Body`, `buildAcceptedMethods`, `buildIdentityMetadata`, `buildHowToPay`, `buildAgentInstructions` (auto-emits per-rail `compatible_clients` — smoke-verified CLIs the agent should use; vendor override supported), `buildPricingBlock`, `firstEncounterAgentMemory`, `OrderReceipt`; `respond402` — drop-in 402 emit that preserves mppx's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `buildValidationError` — structured 4xx body builder (`{error: {code, message}, required_fields?, example_body?, next_steps?, ...extra}`) so vendors compose body shapes by name instead of inlining at every validation site. | +| `/payment` | `networks`, `USDC`, `rails` registries; `paymentDirective`, `buildPaymentDirective`, `wwwAuthenticateHeader`, `paymentRequiredHeader`, `aliasAmountFields` (v1↔v2 amount field shim: emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlementOverrideHeader`, `dispatchSettlementByNetwork`, `extractPaymentSigner` (returns `{address, network}`); `createX402Server`, `createMppxServer`; drop-in x402 helpers: `validateX402NetworkConfig` (boot-time guard), `verifyX402Request` (parse + validate inbound X-Payment), `processX402Settle` (verify-then-settle with one call), `classifyX402SettleResult` (maps the tagged settle result to a recommended HTTP status / code / nextSteps so merchants get a controlled envelope without coupling to facilitator-specific error text). | +| `/discovery` | `isDiscoveryProbeRequest`, `buildDiscoveryProbeResponse` (with optional `x402Sample` for x402-aware crawlers, e.g. `awal x402 details`), `sampleX402AcceptForNetwork` (USDC sample-accept builder for known CAIP-2 networks), `buildWellKnownMpp`, `buildLlmsTxt` + `llmsTxtIdentitySection` + `llmsTxtPaymentSection` (compact + verbose modes), `buildSkillMd` (Claude-Skill-compatible `/skill.md` agent-discovery manifest; strictly agent-facing data only, no internal posture), `agentscoreOpenApiSnippets`, `createBazaarDiscovery`, `noindexNonDiscoveryPaths` (Hono middleware that emits `X-Robots-Tag: noindex` on every path except the agent-discovery surfaces; defaults cover `/openapi.json`, `/llms.txt`, `/skill.md`, `/.well-known/{mpp.json,agent-card.json,ucp,jwks.json}`, `/favicon.{png,ico}`; pure helpers `isDiscoveryPath` + `defaultDiscoveryPaths` for non-Hono frameworks). | +| `/challenge` | `build402Body`, `buildAcceptedMethods`, `buildIdentityMetadata`, `buildHowToPay`, `buildAgentInstructions` (auto-emits per-rail `compatible_clients`: smoke-verified CLIs the agent should use; vendor override supported), `buildPricingBlock`, `firstEncounterAgentMemory`, `OrderReceipt`; `respond402`, a drop-in 402 emit that preserves mppx's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `buildValidationError`: structured 4xx body builder (`{error: {code, message}, required_fields?, example_body?, next_steps?, ...extra}`) so vendors compose body shapes by name instead of inlining at every validation site. | | `/stripe-multichain` | `createMultichainPaymentIntent`, `getDepositAddress`, `simulateCryptoDeposit`, `createMppxStripe`; `createPiCache` (TTL'd PI / deposit-address cache, Redis-backed when `redisUrl` set, in-memory otherwise), `simulateDepositIfTestMode` (gates on `sk_test_` and looks up the PI for you), `STRIPE_TEST_TX_HASH_SUCCESS` / `STRIPE_TEST_TX_HASH_FAILED` constants. Peer dep on `stripe`. | -| `/api` | Everything from `@agent-score/sdk` re-exported in one place: `AgentScore` + `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` + `isAgentScoreTestAddress`. **Don't add `@agent-score/sdk` as a separate dep** — the two can drift versions and cause subtle type mismatches. | +| `/api` | Everything from `@agent-score/sdk` re-exported in one place: `AgentScore` + `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` + `isAgentScoreTestAddress`. **Don't add `@agent-score/sdk` as a separate dep**; the two can drift versions and cause subtle type mismatches. | ## Quick start @@ -53,7 +53,7 @@ const _gate = agentscoreGate({ createSessionOnMissing: { apiKey: process.env.AGENTSCORE_API_KEY!, context: "wine-purchase" }, }); -// Run the gate CONDITIONALLY — only when a payment credential is already attached. +// Run the gate CONDITIONALLY: only when a payment credential is already attached. // Anonymous discovery (no payment header) flows through to the handler so any spec- // compliant x402 wallet can read the 402 challenge with rails + pricing without first // proving identity. Identity is verified at settle time on the retry leg. @@ -95,10 +95,10 @@ const directives = [ ]; const wwwAuth = wwwAuthenticateHeader(directives); -// Recover the on-chain signer from the inbound credential — returns {address, network}. +// Recover the on-chain signer from the inbound credential; returns {address, network}. // 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, else by -// decoding the credential's signed-tx payload — `@solana/kit` optional peer). +// decoding the credential's signed-tx payload; `@solana/kit` optional peer). const signer = await extractPaymentSigner(req, req.headers.get("x-payment") ?? undefined); ``` @@ -117,7 +117,7 @@ const mppx = await createMppxServer({ tempo: { recipient: process.env.TEMPO_RECIPIENT! }, solana: { recipient: process.env.SOLANA_RECIPIENT!, - // Optional fee sponsor — pass any `TransactionPartialSigner` from `@solana/kit`. + // Optional fee sponsor: pass any `TransactionPartialSigner` from `@solana/kit`. // signer: solanaFeePayerSigner, }, stripe: { profileId: process.env.STRIPE_PROFILE_ID!, secretKey: process.env.STRIPE_SECRET_KEY! }, @@ -170,7 +170,7 @@ const responseBody = build402Body({ ```typescript import { buildIdempotencyKey, buildPaymentHeaders } from "@agent-score/commerce/payment"; -// Stable per-payment key — Stripe PI id wins, falls back to pi-{orderId}-{amountCents}. +// Stable per-payment key: Stripe PI id wins, falls back to pi-{orderId}-{amountCents}. const idempotencyKey = buildIdempotencyKey({ paymentIntentId, orderId, amountCents }); // One-call WWW-Authenticate + PAYMENT-REQUIRED bundle from a single rails declaration. @@ -192,10 +192,10 @@ return new Response(JSON.stringify(responseBody), { status: 402, headers }); ```typescript import { buildA2AAgentCard, buildUCPProfile } from "@agent-score/commerce"; -// Google A2A v1.0 Signed Agent Card — publish at /.well-known/agent-card.json +// Google A2A v1.0 Signed Agent Card; publish at /.well-known/agent-card.json const card = buildA2AAgentCard({ name, url, capabilities, data: assess }); -// Google Universal Commerce Protocol — publish at /.well-known/ucp +// Google Universal Commerce Protocol; publish at /.well-known/ucp const profile = buildUCPProfile({ name, services, payment_handlers, signing_keys, data: assess }); ``` @@ -220,7 +220,7 @@ const jwks = buildJWKSResponse([publicJWK]); **Inline JWK in the profile vs separate JWKS endpoint.** UCP §6 mandates the separate `/.well-known/jwks.json` endpoint as the canonical trust source. The profile's `signing_keys[]` is informational; verifiers MUST resolve the kid against the JWKS (not the embedded copy), to prevent a swap-after-sign attack where a hostile actor replaces the inline key with their own. -ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol with no identity-publishing surface — ACP merchants integrate via the existing `build402Body` + `buildPaymentHeaders` + Stripe SPT rail. +ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol with no identity-publishing surface; ACP merchants integrate via the existing `build402Body` + `buildPaymentHeaders` + Stripe SPT rail. ### Stripe multichain (peer dep on `stripe`) @@ -243,8 +243,8 @@ const result = await createMultichainPaymentIntent({ const baseAddress = getDepositAddress(result, "base"); const solanaAddress = getDepositAddress(result, "solana"); -// PI / deposit-address cache. Redis-backed when REDIS_URL is set, in-memory otherwise — -// multi-task deployments need Redis so a deposit lands on whichever task settles it. +// PI / deposit-address cache. Redis-backed when REDIS_URL is set, in-memory otherwise. +// Multi-task deployments need Redis so a deposit lands on whichever task settles it. const piCache = createPiCache({ redisUrl: process.env.REDIS_URL }); for (const addr of Object.values(result.depositAddresses)) { await piCache.cacheAddress(addr); @@ -252,7 +252,7 @@ for (const addr of Object.values(result.depositAddresses)) { } piCache.cacheNetworkAddresses(result.paymentIntentId, result.depositAddresses); -// Testnet helper — gates on sk_test_ and looks up the PI for you. No-op on live keys. +// Testnet helper: gates on sk_test_ and looks up the PI for you. No-op on live keys. await simulateDepositIfTestMode({ getPaymentIntentId: piCache.getPaymentIntentId, depositAddress: baseAddress!, @@ -287,11 +287,11 @@ import { } from "@agent-score/commerce/payment"; import { respond402 } from "@agent-score/commerce/challenge"; -// Boot-time guard — raises if a configured network isn't supported. +// Boot-time guard: raises if a configured network isn't supported. validateX402NetworkConfig({ baseNetwork: X402_BASE }); app.post("/purchase", async (c) => { - // Path A — agent presented an x402 X-Payment header + // Path A: agent presented an x402 X-Payment header if (c.req.header("payment-signature") || c.req.header("x-payment")) { const verified = await verifyX402Request({ request: c.req.raw, @@ -319,7 +319,7 @@ app.post("/purchase", async (c) => { return c.json({ ok: true }, { headers }); } - // Path B — cold call (or Authorization: Payment for mppx). After mppx.compose() returns 402, + // Path B: cold call (or Authorization: Payment for mppx). After mppx.compose() returns 402, // respond402 PRESERVES mppx's WWW-Authenticate and ADDS x402's PAYMENT-REQUIRED. return respond402({ mppxChallenge: mppxResult.challenge as Response, @@ -343,22 +343,22 @@ app.use('/purchase', gate); app.post('/purchase', async (c) => { const { degraded, infraReason } = getGateDegradedState(c); if (degraded) { - // Compliance was NOT enforced this request — log/alert/refund-async/etc. + // Compliance was NOT enforced this request; log/alert/refund-async/etc. console.warn(`[gate] degraded: ${infraReason}`); } // ...rest of handler }); ``` -When `failOpen: true` AND the failure is infra-shape, the gate carries `degraded: true` + `infraReason: 'quota_exceeded' | 'api_error' | 'network_timeout'` so merchants can log/alert without parsing console output. **Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of `failOpen`** — `failOpen` only covers "AgentScore couldn't tell us," never "AgentScore said no." +When `failOpen: true` AND the failure is infra-shape, the gate carries `degraded: true` + `infraReason: 'quota_exceeded' | 'api_error' | 'network_timeout'` so merchants can log/alert without parsing console output. **Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of `failOpen`**; `failOpen` only covers "AgentScore couldn't tell us," never "AgentScore said no." -For regulated commerce (alcohol, age-gated, sanctioned-jurisdiction-relevant) keep the default `failOpen: false` — outage is the correct posture; bypassing compliance on infra failure is a compliance gap. For low-stakes commerce or high-uptime SLAs, opt in and use the `degraded` flag as the audit trail. +For regulated commerce (alcohol, age-gated, sanctioned-jurisdiction-relevant) keep the default `failOpen: false`: outage is the correct posture, and bypassing compliance on infra failure is a compliance gap. For low-stakes commerce or high-uptime SLAs, opt in and use the `degraded` flag as the audit trail. The `getGateDegradedState` helper is exported by every framework adapter (Hono, Express, Fastify, Next.js, Web Fetch). For `withAgentScoreGate` (Next.js / Web Fetch), the `degraded` + `infraReason` fields land directly on the `gate` object passed to your handler. ## Examples -The [examples/](./examples) directory has 7 runnable single-file Hono apps covering common merchant scenarios — copy-paste templates, not frameworks. See [examples/README.md](./examples/README.md) for the full table. +The [examples/](./examples) directory has 8 runnable single-file Hono apps covering common merchant scenarios; copy-paste templates, not frameworks. See [examples/README.md](./examples/README.md) for the full table. ## Vendor profile examples @@ -369,11 +369,11 @@ The [examples/](./examples) directory has 7 runnable single-file Hono apps cover | Tempo-only merchant | `/payment` | `npm install @agent-score/commerce mppx` | | Crypto-native, no Stripe | `/identity/*`, `/payment`, `/challenge` | `npm install @agent-score/commerce @x402/core` | -The SDK is genuinely a toolkit — vendors compose only what they need. Helpers don't bundle assumptions about which rails or protocols you support, and don't recommend one rail over another. +The SDK is genuinely a toolkit; vendors compose only what they need. Helpers don't bundle assumptions about which rails or protocols you support, and don't recommend one rail over another. ## Stability -`@agent-score/commerce@1.0.0` ships with the full merchant SDK surface stable. Helpers are protocol translations + configurable opinions — most evolution is additive (new optional params, new helpers, new networks/rails). Major bumps are reserved for genuine protocol-mapping bugs. +The full merchant SDK surface is stable. Helpers are protocol translations + configurable opinions; most evolution is additive (new optional params, new helpers, new networks/rails). Major bumps are reserved for genuine protocol-mapping bugs. ## Documentation diff --git a/examples/README.md b/examples/README.md index 0c9b71f..81c296c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -10,7 +10,8 @@ Runnable, copy-pasteable example integrations covering the most common merchant | [`stripe-multichain-merchant.ts`](./stripe-multichain-merchant.ts) | Stripe-anchored multi-chain | Stripe PaymentIntent with deposit_options for tempo/base/solana; crypto deposits flow through Stripe | | [`variable-cost-merchant.ts`](./variable-cost-merchant.ts) | Pay-per-actual-usage (LLM, transcode, etc.) | Same use case on **two protocols**: x402 upto (Permit2 authorize-max → Settlement-Overrides settle-actual) AND MPP tempo session (channel + SSE + mid-stream vouchers). Vendor offers both so agents pick whichever their wallet supports. | | [`compliance-merchant.ts`](./compliance-merchant.ts) | Regulated-goods merchant (wine, cannabis, etc.) | Full compliance gate (KYC + sanctions + age + jurisdiction) + custom `onDenied` composing commerce helpers: `verificationAgentInstructions`, `isFixableDenial`, `buildContactSupportNextSteps`, `denialReasonToBody`/`denialReasonStatus`, `buildSignerMismatchBody`. Shows how vendors write only the business-specific branches and let commerce handle the rest. | -| [`per-product-policy-merchant.ts`](./per-product-policy-merchant.ts) | Multi-product merchant with mixed compliance needs | One product hard-gates KYC + 21 + US-state allowlist (wine), one is anonymous (merch, ships anywhere), a third uses `enforcement: 'soft'` to request KYC as a fraud signal but accept anonymous sales — stamps `identity_status: 'unverified'` on the order. Uses `PolicyBlock`, `policyToGateOptions`, `runGateWithEnforcement`, `shippingCountryAllowed`, `shippingStateAllowed`. | +| [`per-product-policy-merchant.ts`](./per-product-policy-merchant.ts) | Multi-product merchant with mixed compliance needs | One product hard-gates KYC + 21 + US-state allowlist (wine), one is anonymous (merch, ships anywhere), a third uses `enforcement: 'soft'` to request KYC as a fraud signal but accept anonymous sales (stamps `identity_status: 'unverified'` on the order). Uses `PolicyBlock`, `policyToGateOptions`, `runGateWithEnforcement`, `shippingCountryAllowed`, `shippingStateAllowed`. | +| [`signed-ucp-merchant.ts`](./signed-ucp-merchant.ts) | Signed UCP profile + JWKS endpoint | Wires `/.well-known/ucp` (signed envelope) + `/.well-known/jwks.json` against a persistent signing key. UCP §6 trust-mode requires the JWS signature; this example shows ephemeral-for-dev / env-JWK-for-prod, key rotation, and `Cache-Control` posture on the JWKS endpoint. Uses `generateUCPSigningKey`, `signUCPProfile`, `buildJWKSResponse`, `ucpSigningKeyFromJWK`. | ## How to use @@ -19,11 +20,11 @@ Runnable, copy-pasteable example integrations covering the most common merchant 3. Install peer deps mentioned at the top of the file (only what you actually need) 4. Set the env vars listed at the top of the file 5. Run with `bun run ` or `node` (after build) -6. Iterate — these are templates, not frameworks +6. Iterate; these are templates, not frameworks ## Patterns -All seven examples follow the same rough shape: +All examples follow the same rough shape: 1. **Boot:** instantiate framework, identity gate (if any), x402/mppx servers (if any) via commerce factories 2. **Discovery routes:** `/llms.txt` + `/.well-known/mpp.json` + `/openapi.json` (where applicable) using commerce/discovery helpers @@ -39,6 +40,6 @@ These examples are intentionally thin on domain logic. Vendors plug in their own - Order storage (DB, durable queue, etc.) - Customer email / fulfillment notifications - Tax / shipping calculators -- Frontend UI (none of these examples include one — they're agent-only APIs) +- Frontend UI (none of these examples include one; they're agent-only APIs) AgentScore Commerce handles the agent commerce protocol layer; everything else is your business. From 0f2092d014471b3747428f0b5d86c8e9108f5b90 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 08:50:30 -0700 Subject: [PATCH 21/35] test: cover uncovered branches to clear 90% global coverage gate 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) --- tests/discovery/openapi.test.ts | 18 +++++++ tests/discovery/robots_tag.test.ts | 54 +++++++++++++++++++ tests/identity/a2a.test.ts | 55 +++++++++++++++++++ tests/identity/ucp-signing.test.ts | 4 +- tests/payment/headers.test.ts | 43 +++++++++++++++ tests/signer-match.test.ts | 84 ++++++++++++++++++++++++++++++ tests/signer.test.ts | 47 +++++++++++++++++ 7 files changed, 304 insertions(+), 1 deletion(-) diff --git a/tests/discovery/openapi.test.ts b/tests/discovery/openapi.test.ts index f9a1296..9e267f3 100644 --- a/tests/discovery/openapi.test.ts +++ b/tests/discovery/openapi.test.ts @@ -100,4 +100,22 @@ describe('agentscoreOpenApiSnippets', () => { expect(agentscoreOpenApiSnippets({ security: false }).securitySchemes).toBeUndefined(); expect(agentscoreOpenApiSnippets({ denials: false, paymentRequired: false }).schemas).toBeUndefined(); }); + + it('emits only paymentRequired schema when denials=false', () => { + // Drives the falsey side of `opts.denials !== false ? agentscoreDenialSchemas() : {}`. + const snippets = agentscoreOpenApiSnippets({ denials: false }); + expect(snippets.schemas).toBeDefined(); + expect(snippets.schemas).toHaveProperty('AgentScorePaymentRequired'); + expect(snippets.schemas).not.toHaveProperty('AgentScoreDenialReason'); + expect(snippets.schemas).not.toHaveProperty('AgentScoreDenialBody'); + }); + + it('emits only denial schemas when paymentRequired=false', () => { + // Drives the falsey side of `opts.paymentRequired !== false ? agentscorePaymentRequiredSchema() : {}`. + const snippets = agentscoreOpenApiSnippets({ paymentRequired: false }); + expect(snippets.schemas).toBeDefined(); + expect(snippets.schemas).toHaveProperty('AgentScoreDenialReason'); + expect(snippets.schemas).toHaveProperty('AgentScoreDenialBody'); + expect(snippets.schemas).not.toHaveProperty('AgentScorePaymentRequired'); + }); }); diff --git a/tests/discovery/robots_tag.test.ts b/tests/discovery/robots_tag.test.ts index 2bf7cf8..53875fa 100644 --- a/tests/discovery/robots_tag.test.ts +++ b/tests/discovery/robots_tag.test.ts @@ -133,6 +133,18 @@ describe('noindexNonDiscoveryPathsExpress', () => { ); expect(headers['X-Robots-Tag']).toBeUndefined(); }); + + it('honors customPaths as additive discovery surfaces', () => { + // Drives the truthy `options?.customPaths` branch in Express. + const mw = noindexNonDiscoveryPathsExpress({ customPaths: ['/sitemap.xml'] }); + const headers: Record = {}; + mw( + { path: '/sitemap.xml' }, + { setHeader: (k, v) => { headers[k] = v; } }, + () => {}, + ); + expect(headers['X-Robots-Tag']).toBeUndefined(); + }); }); describe('noindexNonDiscoveryPathsFastify', () => { @@ -165,6 +177,40 @@ describe('noindexNonDiscoveryPathsFastify', () => { ); expect(discoveryHeaders['X-Robots-Tag']).toBeUndefined(); }); + + it('honors customPaths and falls back to routerPath when url is absent', () => { + // Drives the truthy `options?.customPaths` branch in Fastify, plus the + // `req.url ?? req.routerPath` fallback when url is undefined. + let registeredHook: FastifyHook | undefined; + const fakeApp = { + addHook: (event: 'onRequest', handler: FastifyHook) => { + if (event === 'onRequest') registeredHook = handler; + }, + }; + noindexNonDiscoveryPathsFastify( + fakeApp, + { customPaths: ['/sitemap.xml'] }, + () => {}, + ); + + // url undefined → routerPath used; routerPath is in customPaths so noindex is SKIPPED. + const customHeaders: Record = {}; + registeredHook!( + { routerPath: '/sitemap.xml' } as { url?: string }, + { header: (k: string, v: string) => { customHeaders[k] = v; } }, + () => {}, + ); + expect(customHeaders['X-Robots-Tag']).toBeUndefined(); + + // Both url and routerPath undefined → empty path, not in defaults → noindex applied. + const emptyHeaders: Record = {}; + registeredHook!( + {} as { url?: string }, + { header: (k: string, v: string) => { emptyHeaders[k] = v; } }, + () => {}, + ); + expect(emptyHeaders['X-Robots-Tag']).toBe('noindex, nofollow, noarchive, nosnippet'); + }); }); describe('wrapNoindexResponse / applyNoindexHeader (Web Fetch + Next.js)', () => { @@ -184,4 +230,12 @@ describe('wrapNoindexResponse / applyNoindexHeader (Web Fetch + Next.js)', () => it('applyNoindexHeader is the same helper (Next.js alias)', () => { expect(applyNoindexHeader).toBe(wrapNoindexResponse); }); + + it('honors customPaths so a wrapped non-default path skips noindex', () => { + // Drives the truthy `options?.customPaths` branch in wrapNoindexResponse. + const original = Response.json({ ok: true }); + const wrapped = wrapNoindexResponse('/sitemap.xml', original, { customPaths: ['/sitemap.xml'] }); + expect(wrapped).toBe(original); + expect(wrapped.headers.get('x-robots-tag')).toBeNull(); + }); }); diff --git a/tests/identity/a2a.test.ts b/tests/identity/a2a.test.ts index 1d1a86a..03656ae 100644 --- a/tests/identity/a2a.test.ts +++ b/tests/identity/a2a.test.ts @@ -73,4 +73,59 @@ describe('buildA2AAgentCard', () => { expect(card.identity?.issuer).toBe('https://other.example'); expect(card.identity?.verify_url).toBe('https://other.example/v'); }); + + it('falls back to operator_verification fields when account_verification is absent', () => { + // Drives the `?? operatorVerification?.level` and `?? operatorVerification?.verified_at` + // branches plus the default `'unknown'` / `''` / `verify_url` fallbacks. + const card = buildA2AAgentCard({ + name: 'X', + data: { + decision: 'allow', + decision_reasons: [], + resolved_operator: 'op_only_op_verif', + operator_verification: { + level: 'basic', + operator_type: 'agent', + verified_at: '2026-01-01T00:00:00Z', + }, + }, + }); + expect(card.identity).not.toBeNull(); + expect(card.identity?.kyc_level).toBe('basic'); + expect(card.identity?.sanctions_clear).toBe(false); + expect(card.identity?.age_bracket).toBe('unknown'); + expect(card.identity?.jurisdiction).toBe(''); + expect(card.identity?.verified_at).toBe('2026-01-01T00:00:00Z'); + expect(card.identity?.verify_url).toBe('https://agentscore.sh/verify'); + }); + + it('falls back to default kyc_level "none" when neither verification block is present', () => { + // Drives the trailing `?? 'none'` fallback in the kyc_level chain plus the + // `?? null` fallback for verified_at. + const card = buildA2AAgentCard({ + name: 'X', + data: { + decision: 'allow', + decision_reasons: [], + resolved_operator: 'op_no_verif', + }, + }); + expect(card.identity).not.toBeNull(); + expect(card.identity?.kyc_level).toBe('none'); + expect(card.identity?.verified_at).toBeNull(); + }); + + it('reads verify_url from data when input.verifyUrl is absent', () => { + // Drives the `data.verify_url` branch of the verify_url ?? chain. + const card = buildA2AAgentCard({ + name: 'X', + data: { + decision: 'allow', + decision_reasons: [], + resolved_operator: 'op_with_verify_url', + verify_url: 'https://from-data.example/verify', + }, + }); + expect(card.identity?.verify_url).toBe('https://from-data.example/verify'); + }); }); diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index 2edff55..6e612b0 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -253,10 +253,12 @@ describe('UCP signing — security: alg-confusion + typ + dup-kid', () => { const flippedChar = sig[0] === 'A' ? 'B' : 'A'; const flipped = flippedChar + sig.slice(1); const tampered = { ...signed, signature: `${segments[0]}.${segments[1]}.${flipped}` }; + // JWSSignatureVerificationFailed → wraps to signature_invalid (line 465-466 in ucp-jwks.ts). await expect(verifyUCPProfile(tampered, buildJWKSResponse([publicJWK]))) - .rejects.toThrow(); + .rejects.toMatchObject({ name: 'UCPVerificationError', code: 'signature_invalid' }); }); + it('signing twice with EdDSA is idempotent (deterministic signature)', async () => { const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); diff --git a/tests/payment/headers.test.ts b/tests/payment/headers.test.ts index 31f6bdd..ccf452a 100644 --- a/tests/payment/headers.test.ts +++ b/tests/payment/headers.test.ts @@ -92,4 +92,47 @@ describe('buildPaymentHeaders', () => { expect(result['www-authenticate']).toContain('intent="session"'); expect(result['www-authenticate']).toContain(`expires="${expires}"`); }); + + it('forwards optional chainId / currency / decimals / method overrides', () => { + // Drives the four conditional spreads on the per-rail directiveInput. + const result = buildPaymentHeaders({ + orderId: 'ord_8', + realm: 'a.example', + rails: [{ + rail: 'x402-base-mainnet', + amountUsd: 1, + recipient: '0xa', + chainId: 999, + currency: '0xCustomToken000000000000000000000000000000', + decimals: 8, + method: 'x402/exact-evm', + }], + }); + const directive = result['www-authenticate']; + expect(directive).toContain('method="x402/exact-evm"'); + const requestMatch = /request="([^"]+)"/.exec(directive); + expect(requestMatch).toBeTruthy(); + const requestBlob = JSON.parse( + Buffer.from(requestMatch![1]!, 'base64url').toString(), + ); + expect(requestBlob.currency).toBe('0xCustomToken000000000000000000000000000000'); + expect(requestBlob.decimals).toBe(8); + expect(requestBlob.methodDetails?.chainId).toBe(999); + }); + + it('forwards optional x402.resource into the PAYMENT-REQUIRED header', () => { + // Drives the `...(input.x402.resource ? ... : {})` spread branch. + const result = buildPaymentHeaders({ + orderId: 'ord_9', + realm: 'a.example', + rails: [{ rail: 'x402-base-mainnet', amountUsd: 1, recipient: '0xa' }], + x402: { + accepts: [{ scheme: 'exact', network: 'eip155:8453' }], + version: 1, + resource: { url: 'https://api.example/buy', mimeType: 'application/json' }, + }, + }); + const decoded = JSON.parse(Buffer.from(result['PAYMENT-REQUIRED']!, 'base64').toString()); + expect(decoded.resource).toEqual({ url: 'https://api.example/buy', mimeType: 'application/json' }); + }); }); diff --git a/tests/signer-match.test.ts b/tests/signer-match.test.ts index 0994470..89f986a 100644 --- a/tests/signer-match.test.ts +++ b/tests/signer-match.test.ts @@ -311,6 +311,90 @@ describe('AgentScoreCore.verifyWalletSignerMatch — coverage paths', () => { }); expect(result.kind).toBe('api_error'); }); + + it('projects API-emitted signer_match wallet_auth_requires_wallet_signing verdict', async () => { + // Drives the `kind === 'wallet_auth_requires_wallet_signing'` branch in projectSignerMatch. + // The API itself decides to surface this verdict (e.g. server-side policy says wallet + // identity requires a wallet-signing rail even though a signer was provided). + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + status: 200, + json: async () => ({ + decision: 'allow', + decision_reasons: [], + signer_match: { + kind: 'wallet_auth_requires_wallet_signing', + claimed_wallet: '0xaaa0000000000000000000000000000000000000', + agent_instructions: '{"recovery_action": "use_wallet_signing"}', + }, + }), + } as unknown as Response); + const core = createAgentScoreCore({ apiKey: API_KEY }); + const result = await core.verifyWalletSignerMatch({ + claimedWallet: '0xaaa0000000000000000000000000000000000000', + signer: '0xbbb0000000000000000000000000000000000000', + }); + expect(result.kind).toBe('wallet_auth_requires_wallet_signing'); + if (result.kind === 'wallet_auth_requires_wallet_signing') { + expect(result.claimedWallet).toBe('0xaaa0000000000000000000000000000000000000'); + expect(result.agentInstructions).toContain('use_wallet_signing'); + } + }); + + it('falls back to api_error when fallback resolveWalletToOperator fails after missing signer_match', async () => { + // Drives the `if (!claimedResolve.ok || !signerResolve.ok)` branch in the legacy + // 2-resolve fallback path, plus the resolveWalletToOperator catch (line 850). + // Sequence: first assess succeeds with NO signer_match → fallback runs two + // resolveWalletToOperator calls; the second throws so we hit { ok: false }. + let callIdx = 0; + global.fetch = vi.fn().mockImplementation(async (url: string) => { + if (typeof url === 'string' && url.includes('/v1/assess')) { + callIdx++; + if (callIdx === 1) { + // Initial verifyWalletSignerMatch assess — server omitted signer_match. + return { + ok: true, + status: 200, + json: async () => ({ + decision: 'allow', + decision_reasons: [], + resolved_operator: 'op_claimed', + }), + } as unknown as Response; + } + if (callIdx === 2) { + // First fallback resolve (claimed) — succeeds. + return { + ok: true, + status: 200, + json: async () => ({ + decision: 'allow', + decision_reasons: [], + resolved_operator: 'op_claimed', + }), + } as unknown as Response; + } + // Second fallback resolve (signer) — fails, hits resolveWalletToOperator catch. + throw new Error('upstream resolve outage'); + } + return { ok: true, status: 201, json: async () => ({}) } as unknown as Response; + }); + const warn = vi.spyOn(console, 'warn').mockImplementation(() => {}); + const core = createAgentScoreCore({ apiKey: API_KEY }); + const result = await core.verifyWalletSignerMatch({ + claimedWallet: '0xddd0000000000000000000000000000000000000', + signer: '0xeee0000000000000000000000000000000000000', + }); + expect(result.kind).toBe('api_error'); + if (result.kind === 'api_error') { + expect(result.claimedWallet).toBe('0xddd0000000000000000000000000000000000000'); + } + expect(warn).toHaveBeenCalledWith( + expect.stringContaining('resolveWalletToOperator failed'), + expect.anything(), + ); + warn.mockRestore(); + }); }); describe('AgentScoreCore.verifyWalletSignerMatch — telemetry', () => { diff --git a/tests/signer.test.ts b/tests/signer.test.ts index a425b5c..74ff4da 100644 --- a/tests/signer.test.ts +++ b/tests/signer.test.ts @@ -195,6 +195,53 @@ describe('extractPaymentSignerAddress — MPP path', () => { vi.doUnmock('@solana/kit'); }); + it('returns null when @solana/kit ships getBase64Codec but lacks getTransactionDecoder', async () => { + // Drives the second condition of the `!kit?.getBase64Codec || !kit.getTransactionDecoder + // || !kit.getCompiledTransactionMessageDecoder` short-circuit in extractSolanaSignerFromCredential. + vi.doMock('mppx', () => ({ + Credential: { + extractPaymentScheme: () => true, + fromRequest: () => ({ + payload: { transaction: 'AAAA', type: 'transaction' }, + }), + }, + })); + vi.doMock('@solana/kit', () => ({ + getBase64Codec: () => ({ encode: () => new Uint8Array([0]) }), + getCompiledTransactionMessageDecoder: () => ({ decode: () => ({}) }), + })); + const { extractPaymentSigner: freshExtract } = await import( + `../src/signer?solana-no-tx-decoder=${freshImportKey()}` + ); + const req = makeRequest({ authorization: 'Payment mpp-cred' }); + expect(await freshExtract(req)).toBeNull(); + vi.doUnmock('mppx'); + vi.doUnmock('@solana/kit'); + }); + + it('returns null when @solana/kit ships getBase64Codec + getTransactionDecoder but lacks getCompiledTransactionMessageDecoder', async () => { + // Drives the third condition of the same short-circuit. + vi.doMock('mppx', () => ({ + Credential: { + extractPaymentScheme: () => true, + fromRequest: () => ({ + payload: { transaction: 'AAAA', type: 'transaction' }, + }), + }, + })); + vi.doMock('@solana/kit', () => ({ + getBase64Codec: () => ({ encode: () => new Uint8Array([0]) }), + getTransactionDecoder: () => ({ decode: () => ({ messageBytes: new Uint8Array([0]) }) }), + })); + const { extractPaymentSigner: freshExtract } = await import( + `../src/signer?solana-no-msg-decoder=${freshImportKey()}` + ); + const req = makeRequest({ authorization: 'Payment mpp-cred' }); + expect(await freshExtract(req)).toBeNull(); + vi.doUnmock('mppx'); + vi.doUnmock('@solana/kit'); + }); + it('skips and warns when TransferChecked authority resolves through an address lookup table', async () => { // staticAccounts has only 4 entries; instruction asks for index 99 (i.e. lookup-table-loaded). // Path: bounds-check trips, console.warn, continue, no further match → null. From d0e450bdb902d51f06adc3870d9689a4840c2090 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 10:07:52 -0700 Subject: [PATCH 22/35] feat(identity): vendor-namespace UCP signing typ + capability name (1.4.0) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 2 +- package.json | 2 +- scripts/regenerate-cross-lang-fixtures.ts | 337 ++++++++++++++++++ src/identity/ucp-jwks.ts | 14 +- src/identity/ucp.ts | 20 +- .../fixtures/cross-lang/node-capability.json | 8 +- .../cross-lang/node-data-driven-claims.json | 10 +- .../fixtures/cross-lang/node-emoji-keys.json | 16 +- .../fixtures/cross-lang/node-es256-rails.json | 10 +- .../fixtures/cross-lang/node-extras-int.json | 6 +- .../cross-lang/node-int-boundary.json | 6 +- tests/fixtures/cross-lang/node-minimal.json | 6 +- tests/fixtures/cross-lang/node-multikey.json | 10 +- .../cross-lang/node-typed-claims.json | 10 +- tests/fixtures/cross-lang/node-unicode.json | 6 +- tests/fixtures/cross-lang/py-capability.json | 8 +- .../cross-lang/py-data-driven-claims.json | 10 +- tests/fixtures/cross-lang/py-emoji-keys.json | 12 +- tests/fixtures/cross-lang/py-es256-rails.json | 10 +- tests/fixtures/cross-lang/py-extras-int.json | 6 +- .../fixtures/cross-lang/py-int-boundary.json | 12 +- tests/fixtures/cross-lang/py-minimal.json | 6 +- tests/fixtures/cross-lang/py-multikey.json | 10 +- .../fixtures/cross-lang/py-typed-claims.json | 10 +- tests/fixtures/cross-lang/py-unicode.json | 6 +- tests/identity/ucp-signing.test.ts | 10 +- tests/identity/ucp.test.ts | 5 +- 27 files changed, 455 insertions(+), 113 deletions(-) create mode 100644 scripts/regenerate-cross-lang-fixtures.ts diff --git a/README.md b/README.md index 76a0bd5..a954cf3 100644 --- a/README.md +++ b/README.md @@ -210,7 +210,7 @@ const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: publ const jwks = buildJWKSResponse([publicJWK]); ``` -`verifyUCPProfile` enforces the JWS protected header `typ: "ucp-profile+jws"`, restricts `alg` to `EdDSA` / `ES256`, requires a `kid`, rejects duplicate kids in the JWKS, and compares the canonical body bytes against the JWS payload to catch swap-after-sign tampering. Failures throw `UCPVerificationError` with a discriminated `code` (`no_signature` / `missing_kid` / `kid_not_found` / `duplicate_kid` / `unsupported_alg` / `wrong_typ` / `signature_invalid` / `body_mismatch` / `malformed_jws` / `malformed_jwks` / `unusable_key` / `unrecognized_critical_header`). `malformed_jwks` covers a JWKS argument that isn't a `{ keys: [...] }` document. `unusable_key` covers a matched JWK whose `use` is not `sig` (e.g. `enc`). `unrecognized_critical_header` covers a JWS whose `crit` header lists an extension the verifier doesn't understand (RFC 7515 §4.1.11). +`verifyUCPProfile` enforces the JWS protected header `typ: "agentscore-profile+jws"` (vendor-namespaced; UCP §6 does not define a profile-as-JWS typ), restricts `alg` to `EdDSA` / `ES256`, requires a `kid`, rejects duplicate kids in the JWKS, and compares the canonical body bytes against the JWS payload to catch swap-after-sign tampering. Failures throw `UCPVerificationError` with a discriminated `code` (`no_signature` / `missing_kid` / `kid_not_found` / `duplicate_kid` / `unsupported_alg` / `wrong_typ` / `signature_invalid` / `body_mismatch` / `malformed_jws` / `malformed_jwks` / `unusable_key` / `unrecognized_critical_header`). `malformed_jwks` covers a JWKS argument that isn't a `{ keys: [...] }` document. `unusable_key` covers a matched JWK whose `use` is not `sig` (e.g. `enc`). `unrecognized_critical_header` covers a JWS whose `crit` header lists an extension the verifier doesn't understand (RFC 7515 §4.1.11). `signUCPProfile` rejects profiles containing non-integer `Number` values and integers outside `Number.MAX_SAFE_INTEGER` (cross-language float canonicalization is not stable; values past 2^53 lose precision when JS verifiers reparse the canonical body). Use decimal strings for monetary or fractional fields and for any integer that may exceed the safe range. diff --git a/package.json b/package.json index 31f276f..1e848f6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@agent-score/commerce", - "version": "1.3.4", + "version": "1.4.0", "description": "Agent commerce SDK — identity middleware (Hono, Express, Fastify, Next.js, Web Fetch) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agent commerce.", "main": "./dist/index.js", "module": "./dist/index.mjs", diff --git a/scripts/regenerate-cross-lang-fixtures.ts b/scripts/regenerate-cross-lang-fixtures.ts new file mode 100644 index 0000000..9918ca8 --- /dev/null +++ b/scripts/regenerate-cross-lang-fixtures.ts @@ -0,0 +1,337 @@ +/** + * Regenerate the full cross-lang fixture corpus (Node side). + * + * Writes all `node-*.json` fixtures under `tests/fixtures/cross-lang/`. Used + * after a canonicalization-relevant change (typ rename, capability-name + * rename, schema-URL rename, key-sort tweak, etc.) where every JWS in the + * corpus needs to be re-signed. + * + * Each scenario hand-crafts the profile body, signs with a fresh keypair, and + * writes the `{ profile, jwks, alg, kid, generator }` envelope. Cross-lang + * verify in `tests/identity/cross-lang.test.ts` (and the python sibling) + * pulls these in alongside the `py-*` fixtures generated by the python + * sibling. + */ + +import { writeFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { buildUCPProfile, type UCPSigningKey } from '../src/identity/ucp'; +import { + buildJWKSResponse, + generateUCPSigningKey, + signUCPProfile, + type SignedUCPProfile, +} from '../src/identity/ucp-jwks'; +import type { AgentScoreData } from '../src/core'; + +const OUT_DIR = join(__dirname, '..', 'tests', 'fixtures', 'cross-lang'); + +interface FixtureEnvelope { + profile: SignedUCPProfile; + jwks: { keys: UCPSigningKey[] }; + alg: 'EdDSA' | 'ES256'; + kid: string; + generator: 'node'; +} + +function writeFixture(name: string, env: FixtureEnvelope): void { + const out = join(OUT_DIR, `${name}.json`); + writeFileSync(out, `${JSON.stringify(env, null, 2)}\n`); + console.warn(`wrote ${out}`); +} + +async function main(): Promise { + // ------------------------------------------------------------------------- + // node-minimal + // ------------------------------------------------------------------------- + { + const KID = 'node-minimal-EdDSA'; + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); + const profile = buildUCPProfile({ + name: 'Minimal Merchant', + services: [{ type: 'rest', url: 'https://m.example.com' }], + payment_handlers: [], + signing_keys: [publicJWK as UCPSigningKey], + }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); + writeFixture('node-minimal', { + profile: signed, + jwks: buildJWKSResponse([publicJWK]), + alg: 'EdDSA', + kid: KID, + generator: 'node', + }); + } + + // ------------------------------------------------------------------------- + // node-es256-rails — multi-service + multi-rail + ES256 signing key + // ------------------------------------------------------------------------- + { + const KID = 'node-es256-rails-ES256'; + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID, alg: 'ES256' }); + const profile = buildUCPProfile({ + name: 'ES256 Merchant', + services: [ + { type: 'rest', url: 'https://a.example.com' }, + { type: 'a2a', url: 'https://a.example.com/agent-card.json' }, + ], + payment_handlers: [ + { name: 'tempo', config: { rail: 'tempo-mainnet', chain_id: 4217 } }, + { name: 'x402', config: { networks: ['base-8453'] } }, + ], + signing_keys: [publicJWK as UCPSigningKey], + }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID, alg: 'ES256' }); + writeFixture('node-es256-rails', { + profile: signed, + jwks: buildJWKSResponse([publicJWK]), + alg: 'ES256', + kid: KID, + generator: 'node', + }); + } + + // ------------------------------------------------------------------------- + // node-extras-int — payment_handler config with int + string fields + // ------------------------------------------------------------------------- + { + const KID = 'node-extras-int-EdDSA'; + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); + const profile = buildUCPProfile({ + name: 'Extras Merchant', + services: [{ type: 'rest', url: 'https://e.example.com' }], + payment_handlers: [{ name: 'stripe', config: { profile_id: 'abc', count: 7 } }], + signing_keys: [publicJWK as UCPSigningKey], + }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); + writeFixture('node-extras-int', { + profile: signed, + jwks: buildJWKSResponse([publicJWK]), + alg: 'EdDSA', + kid: KID, + generator: 'node', + }); + } + + // ------------------------------------------------------------------------- + // node-capability — a hand-crafted vendor capability (renamed to + // sh.agentscore.identity to match the new namespace; the in-fixture name is + // independent of the SDK's auto-injection but consistency keeps the corpus + // honest about what callers should publish). + // ------------------------------------------------------------------------- + { + const KID = 'node-capability-EdDSA'; + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); + const profile = buildUCPProfile({ + name: 'Capability Merchant', + services: [{ type: 'rest', url: 'https://c.example.com' }], + capabilities: [ + { + name: 'sh.agentscore.identity', + schema: 'https://agentscore.sh/schema/identity/1', + version: '1', + kyc_required: true, + }, + ], + payment_handlers: [ + { name: 'tempo', config: { rail: 'tempo-mainnet', chain_id: 4217 } }, + ], + signing_keys: [publicJWK as UCPSigningKey], + }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); + writeFixture('node-capability', { + profile: signed, + jwks: buildJWKSResponse([publicJWK]), + alg: 'EdDSA', + kid: KID, + generator: 'node', + }); + } + + // ------------------------------------------------------------------------- + // node-unicode — multi-byte UTF-8 in name / url / config + // ------------------------------------------------------------------------- + { + const KID = 'node-unicode-EdDSA'; + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); + const profile = buildUCPProfile({ + name: 'Café 日本 🍷 Merchant', + services: [{ type: 'rest', url: 'https://日本.example.com' }], + payment_handlers: [{ name: 'tempo', config: { note: 'メモ' } }], + signing_keys: [publicJWK as UCPSigningKey], + }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); + writeFixture('node-unicode', { + profile: signed, + jwks: buildJWKSResponse([publicJWK]), + alg: 'EdDSA', + kid: KID, + generator: 'node', + }); + } + + // ------------------------------------------------------------------------- + // node-multikey — JWKS with two keys, signed by the newer one + // ------------------------------------------------------------------------- + { + const oldKey = await generateUCPSigningKey({ kid: 'node-multikey-old' }); + const newKey = await generateUCPSigningKey({ kid: 'node-multikey-new' }); + const profile = buildUCPProfile({ + name: 'Multi-Key Merchant', + services: [{ type: 'rest', url: 'https://mk.example.com' }], + payment_handlers: [{ name: 'tempo', config: { rail: 'tempo-mainnet' } }], + signing_keys: [oldKey.publicJWK as UCPSigningKey, newKey.publicJWK as UCPSigningKey], + }); + const signed = await signUCPProfile(profile, { signingKey: newKey.privateKey, kid: 'node-multikey-new' }); + writeFixture('node-multikey', { + profile: signed, + jwks: buildJWKSResponse([oldKey.publicJWK, newKey.publicJWK]), + alg: 'EdDSA', + kid: 'node-multikey-new', + generator: 'node', + }); + } + + // ------------------------------------------------------------------------- + // node-emoji-keys — extras with non-ASCII object keys (BMP private use, CJK + // compatibility, supplementary plane). Exercises codepoint-vs-UTF-16 sort. + // ------------------------------------------------------------------------- + { + const KID = 'node-emoji-keys-EdDSA'; + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); + const profile = buildUCPProfile({ + name: 'Emoji Keys Merchant', + services: [{ type: 'rest', url: 'https://emoji.example.com' }], + payment_handlers: [{ name: 'tempo', config: {} }], + signing_keys: [publicJWK as UCPSigningKey], + extras: { + a: 1, + '豈': 2, + '': 3, + '🍷': 4, + }, + }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); + writeFixture('node-emoji-keys', { + profile: signed, + jwks: buildJWKSResponse([publicJWK]), + alg: 'EdDSA', + kid: KID, + generator: 'node', + }); + } + + // ------------------------------------------------------------------------- + // node-int-boundary — exercises Number.MAX_SAFE_INTEGER round-trip + // ------------------------------------------------------------------------- + { + const KID = 'node-int-boundary-EdDSA'; + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); + const profile = buildUCPProfile({ + name: 'Int Boundary Merchant', + services: [{ type: 'rest', url: 'https://i.example.com' }], + payment_handlers: [], + signing_keys: [publicJWK as UCPSigningKey], + extras: { + max_safe_int: 9007199254740991, + min_safe_int: -9007199254740991, + small_int: 42, + neg_small_int: -42, + zero: 0, + }, + }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); + writeFixture('node-int-boundary', { + profile: signed, + jwks: buildJWKSResponse([publicJWK]), + alg: 'EdDSA', + kid: KID, + generator: 'node', + }); + } + + // ------------------------------------------------------------------------- + // node-data-driven-claims — exercises the buildUCPProfile data path with + // API-shape "missing" sentinels (empty string + null). Both languages MUST + // emit identical canonical bytes for this input. + // ------------------------------------------------------------------------- + { + const KID = 'node-data-driven-claims-EdDSA'; + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); + const data: AgentScoreData = { + decision: 'allow', + decision_reasons: [], + resolved_operator: 'op_data_driven', + verify_url: 'https://agentscore.sh/verify/op_data_driven', + account_verification: { + kyc_level: '', + sanctions_clear: false, + age_bracket: null as unknown as string, + jurisdiction: null as unknown as string, + verified_at: null, + }, + }; + const profile = buildUCPProfile({ + name: 'Data Driven Claims Merchant', + services: [{ type: 'rest', url: 'https://d.example.com' }], + payment_handlers: [], + signing_keys: [publicJWK as UCPSigningKey], + data, + }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); + writeFixture('node-data-driven-claims', { + profile: signed, + jwks: buildJWKSResponse([publicJWK]), + alg: 'EdDSA', + kid: KID, + generator: 'node', + }); + } + + // ------------------------------------------------------------------------- + // node-typed-claims — exercises the typed AssessResult fields (no raw + // fallback). Cross-lang parity check for the typed-field-only call site. + // ------------------------------------------------------------------------- + { + const KID = 'node-typed-claims-EdDSA'; + const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); + const data: AgentScoreData = { + decision: 'allow', + decision_reasons: [], + resolved_operator: 'op_typed_claims', + verify_url: 'https://agentscore.sh/verify/op_typed_claims', + operator_verification: { + level: 'enhanced', + operator_type: 'api', + verified_at: '2026-04-01T00:00:00Z', + }, + account_verification: { + kyc_level: 'enhanced', + sanctions_clear: true, + age_bracket: '21+', + jurisdiction: 'US', + verified_at: '2026-04-01T00:00:00Z', + }, + }; + const profile = buildUCPProfile({ + name: 'Typed Claims Merchant', + services: [{ type: 'rest', url: 'https://t.example.com' }], + payment_handlers: [], + signing_keys: [publicJWK as UCPSigningKey], + data, + }); + const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); + writeFixture('node-typed-claims', { + profile: signed, + jwks: buildJWKSResponse([publicJWK]), + alg: 'EdDSA', + kid: KID, + generator: 'node', + }); + } +} + +main().catch((err) => { + console.error(err); + process.exit(1); +}); diff --git a/src/identity/ucp-jwks.ts b/src/identity/ucp-jwks.ts index fb2eafa..86e207a 100644 --- a/src/identity/ucp-jwks.ts +++ b/src/identity/ucp-jwks.ts @@ -67,9 +67,11 @@ const JOSE_INSTALL_HINT = 'Install the optional peer dependency: `npm install jo const ALLOWED_ALGS = ['EdDSA', 'ES256'] as const; type AllowedAlg = (typeof ALLOWED_ALGS)[number]; -/** UCP §6.2 — JWS protected header `typ` value. Verifiers SHOULD enforce this to - * prevent cross-protocol token reuse (RFC 8725 §3.11). */ -const UCP_TYP = 'ucp-profile+jws'; +/** JWS protected header `typ` value. Vendor-namespaced because UCP §6 does not define + * a profile-as-JWS typ; the value advertises that this signed envelope follows the + * AgentScore extension semantics rather than a UCP-canonical signing convention. + * Verifiers SHOULD enforce this to prevent cross-protocol token reuse (RFC 8725 §3.11). */ +const PROFILE_TYP = 'agentscore-profile+jws'; /** Discriminated error class so consumers can branch on failure mode without * parsing message strings or importing jose internals. */ @@ -304,7 +306,7 @@ export async function signUCPProfile( const payloadBytes = new TextEncoder().encode(canonicalBody); const signature = await new jose.CompactSign(payloadBytes) - .setProtectedHeader({ alg, kid: opts.kid, typ: UCP_TYP }) + .setProtectedHeader({ alg, kid: opts.kid, typ: PROFILE_TYP }) .sign(opts.signingKey as Parameters[0]); return { ...profile, signature }; @@ -381,8 +383,8 @@ export async function verifyUCPProfile( // Header check order is typ → alg → kid → crit to match the Python sibling's // _peek_jws_header. RFC 8725 §3.11: enforce expected typ to prevent // cross-protocol token reuse. - if (header.typ !== UCP_TYP) { - throw new UCPVerificationError('wrong_typ', `UCP signature typ must be "${UCP_TYP}"; got ${String(header.typ)}.`); + if (header.typ !== PROFILE_TYP) { + throw new UCPVerificationError('wrong_typ', `UCP signature typ must be "${PROFILE_TYP}"; got ${String(header.typ)}.`); } // RFC 8725 §3.1: restrict to allow-listed algorithms before key resolution // so a hostile JWK can never be used with HS256/none/RS256/etc. diff --git a/src/identity/ucp.ts b/src/identity/ucp.ts index 68c0f93..1e6bde5 100644 --- a/src/identity/ucp.ts +++ b/src/identity/ucp.ts @@ -81,7 +81,7 @@ export interface UCPService { } export interface UCPCapability { - /** Capability name — `checkout`, `catalog`, `agentscore-identity`, etc. */ + /** Capability name — `checkout`, `catalog`, `sh.agentscore.identity`, etc. */ name: string; /** URL of the JSON Schema describing this capability's payload. */ schema?: string; @@ -128,13 +128,13 @@ export interface BuildUCPProfileInput { name?: string; /** Service transport bindings. At minimum, the agent's primary REST endpoint. */ services: UCPService[]; - /** Capabilities offered. AgentScore identity is auto-added as a capability when `data` is provided. */ + /** Capabilities offered. AgentScore identity (vendor-namespaced as `sh.agentscore.identity`) is auto-added as a capability when `data` is provided. */ capabilities?: UCPCapability[]; /** Payment handlers — rails the merchant accepts. */ payment_handlers?: UCPPaymentHandler[]; /** JWKS — public keys the merchant signs requests with. REQUIRED by spec. */ signing_keys: UCPSigningKey[]; - /** AgentScore assess data — adds an `agentscore-identity` capability + claims block when present. */ + /** AgentScore assess data — adds an `sh.agentscore.identity` capability + claims block when present. */ data?: AgentScoreData | null; /** Optional override for the AgentScore capability schema URL. */ agentscoreSchemaUrl?: string; @@ -144,14 +144,18 @@ export interface BuildUCPProfileInput { const DEFAULT_VERSION = '2026-04-17'; const SPEC_URL = 'https://ucp.dev/'; -const AGENTSCORE_CAPABILITY_NAME = 'agentscore-identity'; +// Reverse-DNS namespacing per UCP convention (`^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9_]*)+$`). +// The bare `agentscore-identity` form fails the spec regex; vendor-namespacing under the +// `sh.agentscore` authority is honest about the capability being our extension, not a +// UCP-canonical slot. +const AGENTSCORE_CAPABILITY_NAME = 'sh.agentscore.identity'; const AGENTSCORE_CAPABILITY_VERSION = '1'; /** * Compose a UCP profile body for `/.well-known/ucp` publication. Merges AgentScore - * identity claims into the `capabilities` array as an `agentscore-identity` capability - * so UCP-aware consumers can discover verified-buyer claims alongside the standard - * UCP transport metadata. + * identity claims into the `capabilities` array as an `sh.agentscore.identity` + * capability so UCP-aware consumers can discover verified-buyer claims alongside the + * standard UCP transport metadata. * * Example: * ```ts @@ -197,7 +201,7 @@ export function buildUCPProfile(input: BuildUCPProfileInput): UCPProfile { baseCapabilities.push({ name: AGENTSCORE_CAPABILITY_NAME, version: AGENTSCORE_CAPABILITY_VERSION, - schema: input.agentscoreSchemaUrl ?? 'https://agentscore.sh/schemas/ucp/agentscore-identity.v1.json', + schema: input.agentscoreSchemaUrl ?? 'https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json', claims, }); } diff --git a/tests/fixtures/cross-lang/node-capability.json b/tests/fixtures/cross-lang/node-capability.json index b8abade..ec8943f 100644 --- a/tests/fixtures/cross-lang/node-capability.json +++ b/tests/fixtures/cross-lang/node-capability.json @@ -10,7 +10,7 @@ ], "capabilities": [ { - "name": "agentscore-identity", + "name": "sh.agentscore.identity", "schema": "https://agentscore.sh/schema/identity/1", "version": "1", "kyc_required": true @@ -32,11 +32,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "8zz-L1N_SZ0EUmciU1IzuxBuGd67MSg-OemKm6ofmgg" + "x": "kFgwv82ZN7H3jk9gHbUDTi6EZZeaUUBsLBgnfm8Mtog" } ], "name": "Capability Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6ImFnZW50c2NvcmUtaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hL2lkZW50aXR5LzEiLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkNhcGFiaWxpdHkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9jLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI4enotTDFOX1NaMEVVbWNpVTFJenV4QnVHZDY3TVNnLU9lbUttNm9mbWdnIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.YmiTy87alEbVfAEXYzXYkBrsbO_kHqgTSlv3gKuzy6Oere-pJl0PmZ8zGW2uTyjaGC9OFbjLUIzowY3jnJmGAg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hL2lkZW50aXR5LzEiLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkNhcGFiaWxpdHkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9jLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJrRmd3djgyWk43SDNqazlnSGJVRFRpNkVaWmVhVVVCc0xCZ25mbThNdG9nIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.PIG6fQt84ZM1r08g7_vsl1Hhi6B385BFnPCKo7WkbsyjcpKFpvidTmwBjZ6auUzEOyag6IF0OmEz_8gotuEZAw" }, "jwks": { "keys": [ @@ -46,7 +46,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "8zz-L1N_SZ0EUmciU1IzuxBuGd67MSg-OemKm6ofmgg" + "x": "kFgwv82ZN7H3jk9gHbUDTi6EZZeaUUBsLBgnfm8Mtog" } ] }, diff --git a/tests/fixtures/cross-lang/node-data-driven-claims.json b/tests/fixtures/cross-lang/node-data-driven-claims.json index 65a77b6..57fcc25 100644 --- a/tests/fixtures/cross-lang/node-data-driven-claims.json +++ b/tests/fixtures/cross-lang/node-data-driven-claims.json @@ -10,9 +10,9 @@ ], "capabilities": [ { - "name": "agentscore-identity", + "name": "sh.agentscore.identity", "version": "1", - "schema": "https://agentscore.sh/schemas/ucp/agentscore-identity.v1.json", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", "claims": { "operator_id": "op_data_driven", "kyc_level": "none", @@ -33,11 +33,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "1GQBzacuSLmz5l6LPHluSWLNI1xgcriiRdqs9sO22hY" + "x": "iABhA50IBuZJsPzheBf_qf7suVsmKUQCL7Dw8Uk6CHU" } ], "name": "Data Driven Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwidHlwIjoidWNwLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoiYWdlbnRzY29yZS1pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9hZ2VudHNjb3JlLWlkZW50aXR5LnYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1kYXRhLWRyaXZlbi1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiMUdRQnphY3VTTG16NWw2TFBIbHVTV0xOSTF4Z2NyaWlSZHFzOXNPMjJoWSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.yBVx0_My6D8OAF-g6866FiM24IChFrfQqE5IPhhoxHiNO8qjgBRlE0MCGhUdW0i-3mF8TroUsnsaVv0NV_vbDw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1kYXRhLWRyaXZlbi1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiaUFCaEE1MElCdVpKc1B6aGVCZl9xZjdzdVZzbUtVUUNMN0R3OFVrNkNIVSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.0RqtC9uJRddT-U28IcM2BU8yeqxCAvuR7-nLNYQRlxhTeMiYpVRTFHDI54NRGKuzQL1c_X_EFMcsMxPhl073Dg" }, "jwks": { "keys": [ @@ -47,7 +47,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "1GQBzacuSLmz5l6LPHluSWLNI1xgcriiRdqs9sO22hY" + "x": "iABhA50IBuZJsPzheBf_qf7suVsmKUQCL7Dw8Uk6CHU" } ] }, diff --git a/tests/fixtures/cross-lang/node-emoji-keys.json b/tests/fixtures/cross-lang/node-emoji-keys.json index 700e4f3..d682770 100644 --- a/tests/fixtures/cross-lang/node-emoji-keys.json +++ b/tests/fixtures/cross-lang/node-emoji-keys.json @@ -22,17 +22,15 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "SEqAXr_hDfmdqLqepK--97NMkVlYF_A1ByPa2xycou8" + "x": "3GZayUqGDFe-3oMlX3-ztVFvprBcPJZBreNbGiNWtJA" } ], "name": "Emoji Keys Merchant", - "extras": { - "a": 1, - "豈": 2, - "": 3, - "🍷": 4 - }, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZW1vamkta2V5cy1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6MSwi6LGIIjoyLCLugIAiOjMsIvCfjbciOjR9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWVtb2ppLWtleXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiU0VxQVhyX2hEZm1kcUxxZXBLLS05N05Na1ZsWUZfQTFCeVBhMnh5Y291OCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.QD_zQMZ4UkUkuZQ-rNNEDrEalu2eYrI280Migljdk67UqHWMMOcB4nsBR9mj4E3RJ5M7sgAZ9CWWptdrcTqXCQ" + "a": 1, + "豈": 2, + "": 3, + "🍷": 4, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZW1vamkta2V5cy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJhIjoxLCJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWVtb2ppLWtleXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiM0daYXlVcUdERmUtM29NbFgzLXp0VkZ2cHJCY1BKWkJyZU5iR2lOV3RKQSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsIuixiCI6Miwi7oCAIjozLCLwn423Ijo0fQ.PB3GPO2nViPHCCEq7EZXIhYrLdf4STu0jgvE2SHMBlftC8yZzTxoDRU4yEpBIVDoUGXV0nNBXC6yNUlui4jOBw" }, "jwks": { "keys": [ @@ -42,7 +40,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "SEqAXr_hDfmdqLqepK--97NMkVlYF_A1ByPa2xycou8" + "x": "3GZayUqGDFe-3oMlX3-ztVFvprBcPJZBreNbGiNWtJA" } ] }, diff --git a/tests/fixtures/cross-lang/node-es256-rails.json b/tests/fixtures/cross-lang/node-es256-rails.json index 7eb0535..03702f1 100644 --- a/tests/fixtures/cross-lang/node-es256-rails.json +++ b/tests/fixtures/cross-lang/node-es256-rails.json @@ -37,12 +37,12 @@ "use": "sig", "crv": "P-256", "kty": "EC", - "x": "xMvwGE1713BNeAABNZZhj00pivlto9FNz1YKqzAUvP0", - "y": "BzgzXRAWbR0VWJNL7F59684mX3_fP-0BDUQSmZAvy38" + "x": "CkXn7DA7i8sfzXfW7lCMhtFQ7B22baNab72gYKTbhLk", + "y": "Xti6FK9qpiBtI6WoDSl7fMbaOMi0SVM9B0w7QjGAmiI" } ], "name": "ES256 Merchant", - "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJrdHkiOiJFQyIsInVzZSI6InNpZyIsIngiOiJ4TXZ3R0UxNzEzQk5lQUFCTlpaaGowMHBpdmx0bzlGTnoxWUtxekFVdlAwIiwieSI6IkJ6Z3pYUkFXYlIwVldKTkw3RjU5Njg0bVgzX2ZQLTBCRFVRU21aQXZ5MzgifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.kdcN5xFTZ3Fd4nA9qXlr04F5CxdIVv04zRggY2U6820Gn4sJ9guvJij-Fne26xTEXLIuLlbulwe1bUIJXWBZuQ" + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJrdHkiOiJFQyIsInVzZSI6InNpZyIsIngiOiJDa1huN0RBN2k4c2Z6WGZXN2xDTWh0RlE3QjIyYmFOYWI3MmdZS1RiaExrIiwieSI6Ilh0aTZGSzlxcGlCdEk2V29EU2w3Zk1iYU9NaTBTVk05QjB3N1FqR0FtaUkifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.YUy5baNvQdxWp9Wowcxu0mVufOv6mOkCIrbQJQpLFQuPt45nX-d3g1f04zSNxLhTS-5Z9OFmz3UsWjGJOLpaLQ" }, "jwks": { "keys": [ @@ -52,8 +52,8 @@ "use": "sig", "crv": "P-256", "kty": "EC", - "x": "xMvwGE1713BNeAABNZZhj00pivlto9FNz1YKqzAUvP0", - "y": "BzgzXRAWbR0VWJNL7F59684mX3_fP-0BDUQSmZAvy38" + "x": "CkXn7DA7i8sfzXfW7lCMhtFQ7B22baNab72gYKTbhLk", + "y": "Xti6FK9qpiBtI6WoDSl7fMbaOMi0SVM9B0w7QjGAmiI" } ] }, diff --git a/tests/fixtures/cross-lang/node-extras-int.json b/tests/fixtures/cross-lang/node-extras-int.json index b5354a3..9448838 100644 --- a/tests/fixtures/cross-lang/node-extras-int.json +++ b/tests/fixtures/cross-lang/node-extras-int.json @@ -25,11 +25,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "QdPh4oYqDA7zIBaNkfW_HJLEGiMS_mgZU98a-_8vLpM" + "x": "teOaKZqmXMzlEA-VliLxgfI66IcrOQ-v3CQftdC8rJ8" } ], "name": "Extras Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiUWRQaDRvWXFEQTd6SUJhTmtmV19ISkxFR2lNU19tZ1pVOThhLV84dkxwTSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.JglqGMtdQKucptR-w8YtNQ3hG6QLB5McUIGlnTHYsa9vl3SfQ3UaoLqKsVH2DHLmf8lRl4qKzB8EHS9mJ9Z0Bw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoidGVPYUtacW1YTXpsRUEtVmxpTHhnZkk2Nkljck9RLXYzQ1FmdGRDOHJKOCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.1XePOww8hYlUbgG2agc-DYCW540mSXYPoAwNTpLs0bkQZ7KxSBZ3ywpjrKh3B6VdGtpRKySgXuEBN9Y-oO3fBA" }, "jwks": { "keys": [ @@ -39,7 +39,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "QdPh4oYqDA7zIBaNkfW_HJLEGiMS_mgZU98a-_8vLpM" + "x": "teOaKZqmXMzlEA-VliLxgfI66IcrOQ-v3CQftdC8rJ8" } ] }, diff --git a/tests/fixtures/cross-lang/node-int-boundary.json b/tests/fixtures/cross-lang/node-int-boundary.json index bf60b31..7e27c5b 100644 --- a/tests/fixtures/cross-lang/node-int-boundary.json +++ b/tests/fixtures/cross-lang/node-int-boundary.json @@ -17,7 +17,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "uCH2zVsMZjpjmCGrrBSSmvWMftXFFCYDAUC5YG54XKw" + "x": "sXu5mABH7PE57nRP1-oRCs3ubCDb4-n12Y8rLOl4UTE" } ], "name": "Int Boundary Merchant", @@ -26,7 +26,7 @@ "small_int": 42, "neg_small_int": -42, "zero": 0, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaW50LWJvdW5kYXJ5LUVkRFNBIiwidHlwIjoidWNwLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoidUNIMnpWc01aanBqbUNHcnJCU1NtdldNZnRYRkZDWURBVUM1WUc1NFhLdyJ9XSwic21hbGxfaW50Ijo0Miwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsInplcm8iOjB9.MABQW9Af3K1ThGkncreJJk-Pv2JdRssGkhO0-UHcZpQmnlriPCJJskL91sgaANfBfNMFRvq6v0xqWeAiMWPqDg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaW50LWJvdW5kYXJ5LUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4Ijoic1h1NW1BQkg3UEU1N25SUDEtb1JDczN1YkNEYjQtbjEyWThyTE9sNFVURSJ9XSwic21hbGxfaW50Ijo0Miwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsInplcm8iOjB9.h4E6dSRdyvnJ1bB15NmTetEWnAkLsJYQKMBf1HaxlO_REUsVyz69qjFzs274bEZXv_SAumvZhA0KHjwMVHlQBA" }, "jwks": { "keys": [ @@ -36,7 +36,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "uCH2zVsMZjpjmCGrrBSSmvWMftXFFCYDAUC5YG54XKw" + "x": "sXu5mABH7PE57nRP1-oRCs3ubCDb4-n12Y8rLOl4UTE" } ] }, diff --git a/tests/fixtures/cross-lang/node-minimal.json b/tests/fixtures/cross-lang/node-minimal.json index cc3f976..da5bf21 100644 --- a/tests/fixtures/cross-lang/node-minimal.json +++ b/tests/fixtures/cross-lang/node-minimal.json @@ -17,11 +17,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "QCSceDALov_XB5V0ACkZlnjhhIxBqpoYpaO5HlAf0aw" + "x": "sDSRPsve9rtfqQCJu0TySrPz7cUZR2rGaqN-HY2Rn5c" } ], "name": "Minimal Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1taW5pbWFsLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IlFDU2NlREFMb3ZfWEI1VjBBQ2tabG5qaGhJeEJxcG9ZcGFPNUhsQWYwYXcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.amoy1Vf2vIzfR_asZp0dxc0ywNo0nc4dvoX1BjnJimE_ClfvtcTuGDfglyBYLvk4aRtaqru1DCpYgCSEnI2NBA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1taW5pbWFsLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InNEU1JQc3ZlOXJ0ZnFRQ0p1MFR5U3JQejdjVVpSMnJHYXFOLUhZMlJuNWMifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.S6ZWBp4P6xpbIDbDnoZk3F2yAF2cxDM5QDQ0SnWaASNSKq6henttDoe1XS5Kgr8o8CF51fXwNEUcBKyg91F8CA" }, "jwks": { "keys": [ @@ -31,7 +31,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "QCSceDALov_XB5V0ACkZlnjhhIxBqpoYpaO5HlAf0aw" + "x": "sDSRPsve9rtfqQCJu0TySrPz7cUZR2rGaqN-HY2Rn5c" } ] }, diff --git a/tests/fixtures/cross-lang/node-multikey.json b/tests/fixtures/cross-lang/node-multikey.json index 8c32881..4c45aff 100644 --- a/tests/fixtures/cross-lang/node-multikey.json +++ b/tests/fixtures/cross-lang/node-multikey.json @@ -24,7 +24,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "qSF9p7IJIFIKxoFl6Od1G8qj65Prx35EnN44zMxJs6U" + "x": "Ap8p8aQoUCi_zL0WPN9zW_-W1ch3KxT8VulefBvLKlY" }, { "kid": "node-multikey-new", @@ -32,11 +32,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "9Al1EZLHjgl02MWGtIGaStOPnR9cBc0WXNYuGbU5r-g" + "x": "ryOmTDKCGw-Ln_homqdAVYZNOrmxDpila_S-04GxP-A" } ], "name": "Multi-Key Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3IiwidHlwIjoidWNwLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJxU0Y5cDdJSklGSUt4b0ZsNk9kMUc4cWo2NVByeDM1RW5ONDR6TXhKczZVIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW5ldyIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI5QWwxRVpMSGpnbDAyTVdHdElHYVN0T1BuUjljQmMwV1hOWXVHYlU1ci1nIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.jXm8ZRUWa9_BUYfSv_PCJNqIWbAYf39DUwOdqvExMPTLWDoDzNAwoIleWfyiAGXMOyK0J-0DPeeCFTzmOPMnBQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJBcDhwOGFRb1VDaV96TDBXUE45eldfLVcxY2gzS3hUOFZ1bGVmQnZMS2xZIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW5ldyIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJyeU9tVERLQ0d3LUxuX2hvbXFkQVZZWk5Pcm14RHBpbGFfUy0wNEd4UC1BIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.kXvc8pxI8tTQeJAqYFmd99w4VA2m0gjbMoqDspGz7UnJ2ycz9ap3oMfx209f4cX-eOidnZVApVW3QguILj-5CQ" }, "jwks": { "keys": [ @@ -46,7 +46,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "qSF9p7IJIFIKxoFl6Od1G8qj65Prx35EnN44zMxJs6U" + "x": "Ap8p8aQoUCi_zL0WPN9zW_-W1ch3KxT8VulefBvLKlY" }, { "kid": "node-multikey-new", @@ -54,7 +54,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "9Al1EZLHjgl02MWGtIGaStOPnR9cBc0WXNYuGbU5r-g" + "x": "ryOmTDKCGw-Ln_homqdAVYZNOrmxDpila_S-04GxP-A" } ] }, diff --git a/tests/fixtures/cross-lang/node-typed-claims.json b/tests/fixtures/cross-lang/node-typed-claims.json index aebf243..4475c48 100644 --- a/tests/fixtures/cross-lang/node-typed-claims.json +++ b/tests/fixtures/cross-lang/node-typed-claims.json @@ -10,9 +10,9 @@ ], "capabilities": [ { - "name": "agentscore-identity", + "name": "sh.agentscore.identity", "version": "1", - "schema": "https://agentscore.sh/schemas/ucp/agentscore-identity.v1.json", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", "claims": { "operator_id": "op_typed_claims", "kyc_level": "enhanced", @@ -33,11 +33,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "hkhmYJSOPyC7tC2baujBsjvTdDs0M2gnmiTGEm_H9y0" + "x": "GgQANYQYeQgylzTJo4WpjfVjUh_OqKwdRWX_n2H8YoU" } ], "name": "Typed Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwidHlwIjoidWNwLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoiYWdlbnRzY29yZS1pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9hZ2VudHNjb3JlLWlkZW50aXR5LnYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS10eXBlZC1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiaGtobVlKU09QeUM3dEMyYmF1akJzanZUZERzME0yZ25taVRHRW1fSDl5MCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.GJZcFBMvdIPmELSrUGzu--PmKwjItbpV74peSvcJcXRk6DRHgivYZOaTOPjFgZgOqnvhAEeG-gvy4O6jP5NrCA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS10eXBlZC1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiR2dRQU5ZUVllUWd5bHpUSm80V3BqZlZqVWhfT3FLd2RSV1hfbjJIOFlvVSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.-LXXBRXXwPs7UmnW3hatAhOBxDBnQrvKCvLuiy8pitNB4b0rtrRx1oI6ATdWKguYSM1Cks9-xMbsrx_PFHAXCw" }, "jwks": { "keys": [ @@ -47,7 +47,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "hkhmYJSOPyC7tC2baujBsjvTdDs0M2gnmiTGEm_H9y0" + "x": "GgQANYQYeQgylzTJo4WpjfVjUh_OqKwdRWX_n2H8YoU" } ] }, diff --git a/tests/fixtures/cross-lang/node-unicode.json b/tests/fixtures/cross-lang/node-unicode.json index 18bf117..06c9024 100644 --- a/tests/fixtures/cross-lang/node-unicode.json +++ b/tests/fixtures/cross-lang/node-unicode.json @@ -24,11 +24,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "mxtclpNy58uer_3ivEk9HfPp5_6zXtYUpc_ItTLz0sA" + "x": "cc_2e1ln2ovqQW1kPc4nWWYi_06rxM7k1LAEHLd5JI8" } ], "name": "Café 日本 🍷 Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJteHRjbHBOeTU4dWVyXzNpdkVrOUhmUHA1XzZ6WHRZVXBjX0l0VEx6MHNBIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.21NUepmkXaXs6cRnPBgheUR0F7EoqysnAkOEqiaqZEG8OEGMkegGsVeEvtaxEjpQZfC4KAeTqvjy6Vc-FSDzBg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJjY18yZTFsbjJvdnFRVzFrUGM0bldXWWlfMDZyeE03azFMQUVITGQ1Skk4In1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.3RUvt9pT1v4nQ5wS50HbffZbdDJecMpBRcA6aJKRZGN5CYLCFes5QnAhrR431Nv99ekdKwieScxryGXa767EAQ" }, "jwks": { "keys": [ @@ -38,7 +38,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "mxtclpNy58uer_3ivEk9HfPp5_6zXtYUpc_ItTLz0sA" + "x": "cc_2e1ln2ovqQW1kPc4nWWYi_06rxM7k1LAEHLd5JI8" } ] }, diff --git a/tests/fixtures/cross-lang/py-capability.json b/tests/fixtures/cross-lang/py-capability.json index e7db70a..ed6f012 100644 --- a/tests/fixtures/cross-lang/py-capability.json +++ b/tests/fixtures/cross-lang/py-capability.json @@ -10,7 +10,7 @@ ], "capabilities": [ { - "name": "agentscore-identity", + "name": "sh.agentscore.identity", "schema": "https://agentscore.sh/schema/identity/1", "version": "1", "kyc_required": true @@ -32,17 +32,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "gqL1GB3M3r0MBCjHc7ORpjfaLgZHY-PhyJwcg8V1y1c" + "x": "BXRO6rmnby3lVMe-h0IKk1WuY_HlUgA1VzYsiB4nRdw" } ], "name": "Capability Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6ImFnZW50c2NvcmUtaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hL2lkZW50aXR5LzEiLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkNhcGFiaWxpdHkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9jLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiZ3FMMUdCM00zcjBNQkNqSGM3T1JwamZhTGdaSFktUGh5SndjZzhWMXkxYyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.4e8OMTQExso6-qIa4p2US1ViX7FBgfjX8Ey8iuaQPgJl2SkjjQs7PTBFa6h57W3Pk8JJgYWFCbFYvm7mJp4nBQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hL2lkZW50aXR5LzEiLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkNhcGFiaWxpdHkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9jLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiQlhSTzZybW5ieTNsVk1lLWgwSUtrMVd1WV9IbFVnQTFWellzaUI0blJkdyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.m8yHS1g49kY0QdrL0hLiHnwZTOMP6Iu_Hee9dceWhgMSyjDR3qeupkam3P0sbcT0922OZ_tZ2O_bPa_7grHGDw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "gqL1GB3M3r0MBCjHc7ORpjfaLgZHY-PhyJwcg8V1y1c", + "x": "BXRO6rmnby3lVMe-h0IKk1WuY_HlUgA1VzYsiB4nRdw", "kid": "py-capability-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-data-driven-claims.json b/tests/fixtures/cross-lang/py-data-driven-claims.json index 8e31df0..067155f 100644 --- a/tests/fixtures/cross-lang/py-data-driven-claims.json +++ b/tests/fixtures/cross-lang/py-data-driven-claims.json @@ -10,8 +10,8 @@ ], "capabilities": [ { - "name": "agentscore-identity", - "schema": "https://agentscore.sh/schemas/ucp/agentscore-identity.v1.json", + "name": "sh.agentscore.identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", "version": "1", "claims": { "operator_id": "op_data_driven", @@ -33,17 +33,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "e0tM2PG2SrWLVh2twzUQqc4wVi5isQJTWZLWe9Jceqg" + "x": "awUvcjZ9GvvUA8U9-YIcNYi874ritVW28g5OEqnCxvU" } ], "name": "Data Driven Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoiYWdlbnRzY29yZS1pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9hZ2VudHNjb3JlLWlkZW50aXR5LnYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImUwdE0yUEcyU3JXTFZoMnR3elVRcWM0d1ZpNWlzUUpUV1pMV2U5SmNlcWcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.IRSaAW3aI_uT0YBakBlQ_DalJNlvmiID89pmeK2avjS1rZ1FWTjTnYv4fHYbkolTYKYSW4PNC8rV4hTYtPOzDg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImF3VXZjalo5R3Z2VUE4VTktWUljTllpODc0cml0VlcyOGc1T0VxbkN4dlUifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.OdQJnBqEivje-fJNKX6iZkJp0OvhwGiv2l6Idmbtfw1Wy_Y0WsKkvnAJ3aZpkNbSgtDP1ZMYtdAqVeMJXjZjDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "e0tM2PG2SrWLVh2twzUQqc4wVi5isQJTWZLWe9Jceqg", + "x": "awUvcjZ9GvvUA8U9-YIcNYi874ritVW28g5OEqnCxvU", "kid": "py-data-driven-claims-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-emoji-keys.json b/tests/fixtures/cross-lang/py-emoji-keys.json index aa3b589..a7181af 100644 --- a/tests/fixtures/cross-lang/py-emoji-keys.json +++ b/tests/fixtures/cross-lang/py-emoji-keys.json @@ -2,7 +2,6 @@ "profile": { "version": "2026-04-17", "spec": "https://ucp.dev/", - "name": "Emoji Keys Merchant", "services": [ { "type": "rest", @@ -18,27 +17,28 @@ ], "signing_keys": [ { - "crv": "Ed25519", - "x": "xrTm5ZIZUbFC1_S2Yw5KZkf-9m8--CmwP6-bkttx-ik", "kid": "py-emoji-keys-EdDSA", + "kty": "OKP", "alg": "EdDSA", "use": "sig", - "kty": "OKP" + "crv": "Ed25519", + "x": "bwPt5nJziggvuu2goCiscN4VdBz7TtYWPomXZEfPGNQ" } ], + "name": "Emoji Keys Merchant", "extras": { "a": 1, "豈": 2, "": 3, "🍷": 4 }, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6MSwi6LGIIjoyLCLugIAiOjMsIvCfjbciOjR9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InhyVG01WklaVWJGQzFfUzJZdzVLWmtmLTltOC0tQ213UDYtYmt0dHgtaWsifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.O2ENDO4OJreRSvRZqbyMzbQlaG3SKy_zsfMFqqV6HUkwvIzmpH2bot_XtJzyz23RTsBdwvZtLxQJOSnBFkIfBQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6MSwi6LGIIjoyLCLugIAiOjMsIvCfjbciOjR9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImJ3UHQ1bkp6aWdndnV1MmdvQ2lzY040VmRCejdUdFlXUG9tWFpFZlBHTlEifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.l-nAn3fjDxhLuJabAk9RiKWh2PY6U0qfNSPdkO0gDpFr3nFO9QWxfkdhwBi-Z56eWz_O2Z0AeYigGnfymjq3DA" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "xrTm5ZIZUbFC1_S2Yw5KZkf-9m8--CmwP6-bkttx-ik", + "x": "bwPt5nJziggvuu2goCiscN4VdBz7TtYWPomXZEfPGNQ", "kid": "py-emoji-keys-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-es256-rails.json b/tests/fixtures/cross-lang/py-es256-rails.json index dbe5d70..f0f5ff7 100644 --- a/tests/fixtures/cross-lang/py-es256-rails.json +++ b/tests/fixtures/cross-lang/py-es256-rails.json @@ -37,19 +37,19 @@ "alg": "ES256", "use": "sig", "crv": "P-256", - "x": "l45yeK-s3eujIDwIU-rEeiv1l6KQq-1GUm4-0P8gpVk", - "y": "3o_N_dWi26UxSRzIIjvuQCKCgkxN6pO_5xYSrZrHYaQ" + "x": "hZLxjfZFnyfL_eHxSk8wUfO7BmfLVHrk9zdZO38JrsM", + "y": "xOGf2dmncLo4CrqrrJt8Q_GFjgw5jrUTWO6W_OEJyo0" } ], "name": "ES256 Merchant", - "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoidWNwLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoibDQ1eWVLLXMzZXVqSUR3SVUtckVlaXYxbDZLUXEtMUdVbTQtMFA4Z3BWayIsInkiOiIzb19OX2RXaTI2VXhTUnpJSWp2dVFDS0Nna3hONnBPXzV4WVNyWnJIWWFRIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.Qh5KIH-aP8KVqIFDUnUOwnVC0L8Tii03u6NM6Bt-lePUlgnzLwOogNvMRK7hl5YqwYgzhrkWM2KbHRakv-x_cw" + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiaFpMeGpmWkZueWZMX2VIeFNrOHdVZk83Qm1mTFZIcms5emRaTzM4SnJzTSIsInkiOiJ4T0dmMmRtbmNMbzRDcnFyckp0OFFfR0ZqZ3c1anJVVFdPNldfT0VKeW8wIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.oFLtXApvevr2l1uJ8qvzA9NrLGinPpZLRQGXayyTmirfiGGOIfffGe8zRRoeR4G4XxTuexobKRKxdO_UqzI38A" }, "jwks": { "keys": [ { "crv": "P-256", - "x": "l45yeK-s3eujIDwIU-rEeiv1l6KQq-1GUm4-0P8gpVk", - "y": "3o_N_dWi26UxSRzIIjvuQCKCgkxN6pO_5xYSrZrHYaQ", + "x": "hZLxjfZFnyfL_eHxSk8wUfO7BmfLVHrk9zdZO38JrsM", + "y": "xOGf2dmncLo4CrqrrJt8Q_GFjgw5jrUTWO6W_OEJyo0", "kid": "py-es256-rails-ES256", "alg": "ES256", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-extras-int.json b/tests/fixtures/cross-lang/py-extras-int.json index cd9c680..436749a 100644 --- a/tests/fixtures/cross-lang/py-extras-int.json +++ b/tests/fixtures/cross-lang/py-extras-int.json @@ -25,17 +25,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "-ZXadF3IWTfw9_0GOs5imZKusJ5ID8vAZgcN4hH7iWw" + "x": "QvDeYnvr6i5EgiSwQ3EDgiPMv1obGL0nScKyBZ26Yi0" } ], "name": "Extras Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1leHRyYXMtaW50LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ii1aWGFkRjNJV1RmdzlfMEdPczVpbVpLdXNKNUlEOHZBWmdjTjRoSDdpV3cifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.71PP5YsYjSIA2PVI0B4HNg5MrRQbn0GrUGjeQ4R6SPNK4-n8AMuACSjKqEF7df9hLVrmfuiwUyAJhSItQuFYCA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1leHRyYXMtaW50LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IlF2RGVZbnZyNmk1RWdpU3dRM0VEZ2lQTXYxb2JHTDBuU2NLeUJaMjZZaTAifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.OXvd3vtjSd-gcfaF-od9V76PXZL0ebugDWzjmK8BbTlyIB9SnDFn2MHVk00XwFXX4ZM8TX2UQpAQ3HB6TXsvBg" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "-ZXadF3IWTfw9_0GOs5imZKusJ5ID8vAZgcN4hH7iWw", + "x": "QvDeYnvr6i5EgiSwQ3EDgiPMv1obGL0nScKyBZ26Yi0", "kid": "py-extras-int-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-int-boundary.json b/tests/fixtures/cross-lang/py-int-boundary.json index b8b4481..6b5c697 100644 --- a/tests/fixtures/cross-lang/py-int-boundary.json +++ b/tests/fixtures/cross-lang/py-int-boundary.json @@ -2,7 +2,6 @@ "profile": { "version": "2026-04-17", "spec": "https://ucp.dev/", - "name": "Int Boundary Merchant", "services": [ { "type": "rest", @@ -13,14 +12,15 @@ "payment_handlers": [], "signing_keys": [ { - "crv": "Ed25519", - "x": "orncEOVmokkWyFRnJFYk1TeRC9nrMQG1Ip9kloaOd98", "kid": "py-int-boundary-EdDSA", + "kty": "OKP", "alg": "EdDSA", "use": "sig", - "kty": "OKP" + "crv": "Ed25519", + "x": "mDepmBzMdz9K5ujAdDAbE9Q-50SIRoa8CQu_386C6Os" } ], + "name": "Int Boundary Merchant", "extras": { "max_safe_int": 9007199254740991, "min_safe_int": -9007199254740991, @@ -28,13 +28,13 @@ "neg_small_int": -42, "zero": 0 }, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsibWF4X3NhZmVfaW50Ijo5MDA3MTk5MjU0NzQwOTkxLCJtaW5fc2FmZV9pbnQiOi05MDA3MTk5MjU0NzQwOTkxLCJuZWdfc21hbGxfaW50IjotNDIsInNtYWxsX2ludCI6NDIsInplcm8iOjB9LCJuYW1lIjoiSW50IEJvdW5kYXJ5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W10sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4Ijoib3JuY0VPVm1va2tXeUZSbkpGWWsxVGVSQzluck1RRzFJcDlrbG9hT2Q5OCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.p4tNJUnyRRHUtEBN3_y4DtuKk4CLBQnMfmGHz76wYYaxiAYa0oN251EC4PrkAHrZ6OlgKagTS027yisUf3qeDA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsibWF4X3NhZmVfaW50Ijo5MDA3MTk5MjU0NzQwOTkxLCJtaW5fc2FmZV9pbnQiOi05MDA3MTk5MjU0NzQwOTkxLCJuZWdfc21hbGxfaW50IjotNDIsInNtYWxsX2ludCI6NDIsInplcm8iOjB9LCJuYW1lIjoiSW50IEJvdW5kYXJ5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W10sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoibURlcG1Cek1kejlLNXVqQWREQWJFOVEtNTBTSVJvYThDUXVfMzg2QzZPcyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.s1msUhEmZIQ--XsMO4z7ODtWy7jVVn5zjJ3fmxIYmjMVxgz_oHcVfKNHsgETnyUf3P9PZLXQBetmCZoXA-8BCA" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "orncEOVmokkWyFRnJFYk1TeRC9nrMQG1Ip9kloaOd98", + "x": "mDepmBzMdz9K5ujAdDAbE9Q-50SIRoa8CQu_386C6Os", "kid": "py-int-boundary-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-minimal.json b/tests/fixtures/cross-lang/py-minimal.json index 685aa07..df6d66a 100644 --- a/tests/fixtures/cross-lang/py-minimal.json +++ b/tests/fixtures/cross-lang/py-minimal.json @@ -17,17 +17,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "DvrugQWOA-k_RSYLM4IbjA_IoO_DiFeDfDXAy6PvQM8" + "x": "8ACvl-Jzck2_55z9rb4CeETjDUUKJpmRulL0yi0LeVg" } ], "name": "Minimal Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJEdnJ1Z1FXT0Eta19SU1lMTTRJYmpBX0lvT19EaUZlRGZEWEF5NlB2UU04In1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.Uk1fCmzYJvfxp_6CbmgTzdpuZzziodaroFTEjfKZ_qK_FU2i2HfG-SkYdz8icZLQxWVhMtTaoTtqeV6BvjNHBA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI4QUN2bC1KemNrMl81NXo5cmI0Q2VFVGpEVVVLSnBtUnVsTDB5aTBMZVZnIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.CdDIBYUH8v1TWdICnPXKTA0MJF-ImzrZ7CpV6YaR1Ka1B-nPfjQKyq6_lDVIuit4xbSRI3p69bwhYP_aJ_6JDw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "DvrugQWOA-k_RSYLM4IbjA_IoO_DiFeDfDXAy6PvQM8", + "x": "8ACvl-Jzck2_55z9rb4CeETjDUUKJpmRulL0yi0LeVg", "kid": "py-minimal-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-multikey.json b/tests/fixtures/cross-lang/py-multikey.json index 3b6f990..16c0711 100644 --- a/tests/fixtures/cross-lang/py-multikey.json +++ b/tests/fixtures/cross-lang/py-multikey.json @@ -24,7 +24,7 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "CCNBoeaXWgTni7QcDtNohjUmhVEGHelxV3qLYHXZovk" + "x": "72U644xYLhnRGvY4aiTSppjuhSO14_tSlAR0oN9mo_g" }, { "kid": "py-multikey-new", @@ -32,17 +32,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "m-pu9Un4958pSkTHuM5laNjzrFxh8VyBh4cOguNyuMY" + "x": "cUE5GPqn-uWHWUheNve6AInP8PTSF0i-F-lxXhaZnhc" } ], "name": "Multi-Key Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1vbGQiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiQ0NOQm9lYVhXZ1RuaTdRY0R0Tm9oalVtaFZFR0hlbHhWM3FMWUhYWm92ayJ9LHsiYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Im0tcHU5VW40OTU4cFNrVEh1TTVsYU5qenJGeGg4VnlCaDRjT2d1Tnl1TVkifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.QElsGEGovjoZtyMQX20MZwZ9JjmVUzTxYZ_V5z5z-Co-5uhNi49BAyV1QiBzbP54kZwm_WbwZ_x-9OVYw9rtCg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1vbGQiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiNzJVNjQ0eFlMaG5SR3ZZNGFpVFNwcGp1aFNPMTRfdFNsQVIwb045bW9fZyJ9LHsiYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImNVRTVHUHFuLXVXSFdVaGVOdmU2QUluUDhQVFNGMGktRi1seFhoYVpuaGMifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.iL3NR2xylMO6i21kGvgbXwf6kiXaN4dI7qbGdD0mWcA-HvQjB31LCHjENEcqQrYxxmi40a2YP_6r-HAOAheYDw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "CCNBoeaXWgTni7QcDtNohjUmhVEGHelxV3qLYHXZovk", + "x": "72U644xYLhnRGvY4aiTSppjuhSO14_tSlAR0oN9mo_g", "kid": "py-multikey-old", "alg": "EdDSA", "use": "sig", @@ -50,7 +50,7 @@ }, { "crv": "Ed25519", - "x": "m-pu9Un4958pSkTHuM5laNjzrFxh8VyBh4cOguNyuMY", + "x": "cUE5GPqn-uWHWUheNve6AInP8PTSF0i-F-lxXhaZnhc", "kid": "py-multikey-new", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-typed-claims.json b/tests/fixtures/cross-lang/py-typed-claims.json index af21d35..9b90095 100644 --- a/tests/fixtures/cross-lang/py-typed-claims.json +++ b/tests/fixtures/cross-lang/py-typed-claims.json @@ -10,8 +10,8 @@ ], "capabilities": [ { - "name": "agentscore-identity", - "schema": "https://agentscore.sh/schemas/ucp/agentscore-identity.v1.json", + "name": "sh.agentscore.identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", "version": "1", "claims": { "operator_id": "op_typed_claims", @@ -33,17 +33,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "Qu9H2p75WjLc0DCdYY7MTaTkDZ0YPBFKHH3jsZMjFiA" + "x": "mXjsvqH0EcymykVn2tr8hwh5QmRk0z-viXjVtxydRWE" } ], "name": "Typed Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6InVjcC1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoiYWdlbnRzY29yZS1pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9hZ2VudHNjb3JlLWlkZW50aXR5LnYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IlF1OUgycDc1V2pMYzBEQ2RZWTdNVGFUa0RaMFlQQkZLSEgzanNaTWpGaUEifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.Awkp_QIMwjiiBE4CSiZQBkxXNdxwGBIPW36sAFIngbax_otu5N5S2kBlnt4xUhvRCJ-_CHieGCPJseIXa0i9Dg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Im1YanN2cUgwRWN5bXlrVm4ydHI4aHdoNVFtUmswei12aVhqVnR4eWRSV0UifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.GsJB8FWsiitETgYR6M8HgGDOgEQ-o2JOAkLpV2emdr9AtMWDlewhs78L-ErX7biQ14joDiuI2BCGpWPXytIuCA" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "Qu9H2p75WjLc0DCdYY7MTaTkDZ0YPBFKHH3jsZMjFiA", + "x": "mXjsvqH0EcymykVn2tr8hwh5QmRk0z-viXjVtxydRWE", "kid": "py-typed-claims-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-unicode.json b/tests/fixtures/cross-lang/py-unicode.json index 71484d0..f0dd135 100644 --- a/tests/fixtures/cross-lang/py-unicode.json +++ b/tests/fixtures/cross-lang/py-unicode.json @@ -24,17 +24,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "Sfrt68PbX5aBkynHGPHclnr0eKFfynzCIC0urH8-o9s" + "x": "T0Zmk7XdS5Wc4VuEXNKmISZBCkSrYlreWHsHA5u8eKg" } ], "name": "Café 日本 🍷 Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiU2ZydDY4UGJYNWFCa3luSEdQSGNsbnIwZUtGZnluekNJQzB1ckg4LW85cyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.GO42vYTPp0B8y1MjgN3iqMzby1vJmCzkOEWnV2O0C5ckUePV-QtTyRchmyAEttjr66HOSVEMyU8CgtfVirhCBg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiVDBabWs3WGRTNVdjNFZ1RVhOS21JU1pCQ2tTcllscmVXSHNIQTV1OGVLZyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.EHFh_jdwp22iavAYT1fohKdE6ZG6tszSNIF2dcliQQ7hFAeeWyLP1c3MjhJdEs7zakGYJ8hv_PTgJrXKfAlZDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "Sfrt68PbX5aBkynHGPHclnr0eKFfynzCIC0urH8-o9s", + "x": "T0Zmk7XdS5Wc4VuEXNKmISZBCkSrYlreWHsHA5u8eKg", "kid": "py-unicode-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index 6e612b0..5de7946 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -168,14 +168,14 @@ describe('UCP signing — security: alg-confusion + typ + dup-kid', () => { return JSON.stringify(sort(stripped)); })(); const evilSig = await new jose.CompactSign(new TextEncoder().encode(sortedJson)) - .setProtectedHeader({ alg: 'HS256', kid: 'attacker', typ: 'ucp-profile+jws' }) + .setProtectedHeader({ alg: 'HS256', kid: 'attacker', typ: 'agentscore-profile+jws' }) .sign(sharedSecret); const tampered = { ...profile, signature: evilSig }; await expect(verifyUCPProfile(tampered as never, buildJWKSResponse([ocJwk as never]))) .rejects.toThrow(UCPVerificationError); }); - it('rejects a JWS with typ != "ucp-profile+jws"', async () => { + it('rejects a JWS with typ != "agentscore-profile+jws"', async () => { const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: 'k' }); const profile = buildUCPProfile({ ...baseInput, signing_keys: [publicJWK] }); const jose = await import('jose'); @@ -351,7 +351,7 @@ describe('UCP signing — additional hardening', () => { return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${ss(o[k])}`).join(',')}}`; } const canonical = ss(profile); - const headerJson = JSON.stringify({ alg: 'EdDSA', kid: 'k', typ: 'ucp-profile+jws', crit: ['fakething'], fakething: 'x' }); + const headerJson = JSON.stringify({ alg: 'EdDSA', kid: 'k', typ: 'agentscore-profile+jws', crit: ['fakething'], fakething: 'x' }); const headerB64 = base64url.encode(new TextEncoder().encode(headerJson)); const payloadB64 = base64url.encode(new TextEncoder().encode(canonical)); const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`); @@ -387,7 +387,7 @@ describe('UCP signing — additional hardening', () => { return `{${Object.keys(o).sort().map((k) => `${JSON.stringify(k)}:${ss(o[k])}`).join(',')}}`; } const canonical = ss(profile); - const headerJson = JSON.stringify({ alg: 'EdDSA', kid: 'k', typ: 'ucp-profile+jws', crit }); + const headerJson = JSON.stringify({ alg: 'EdDSA', kid: 'k', typ: 'agentscore-profile+jws', crit }); const headerB64 = base64url.encode(new TextEncoder().encode(headerJson)); const payloadB64 = base64url.encode(new TextEncoder().encode(canonical)); const data = new TextEncoder().encode(`${headerB64}.${payloadB64}`); @@ -674,7 +674,7 @@ describe('UCP signing — error precedence parity (profile-first)', () => { it('mixed body-malformed + wrong-typ JWS emits wrong_typ (header-first like Python)', async () => { // Build a profile, sign it with the wrong typ (typ="JWT" instead of - // ucp-profile+jws) so header validation rejects, then mutate the body + // agentscore-profile+jws) so header validation rejects, then mutate the body // to also carry a non-integer Number that would fail canonicalize. The // verifier must surface `wrong_typ` (header-first), matching the Python // sibling's _peek_jws_header order. diff --git a/tests/identity/ucp.test.ts b/tests/identity/ucp.test.ts index bac166d..35f0ebb 100644 --- a/tests/identity/ucp.test.ts +++ b/tests/identity/ucp.test.ts @@ -32,12 +32,13 @@ describe('buildUCPProfile', () => { expect(profile.payment_handlers).toEqual([]); }); - it('appends agentscore-identity capability when data carries a resolved operator', () => { + it('appends sh.agentscore.identity capability when data carries a resolved operator', () => { const profile = buildUCPProfile({ ...baseInput, data: fullData }); const cap = profile.capabilities.find((c) => c.name === AGENTSCORE_UCP_CAPABILITY); expect(cap).toBeDefined(); expect(cap?.version).toBe('1'); - expect(cap?.schema).toContain('agentscore-identity.v1.json'); + expect(cap?.name).toBe('sh.agentscore.identity'); + expect(cap?.schema).toContain('sh-agentscore-identity-v1.json'); const claims = (cap as Record).claims as Record; expect(claims.operator_id).toBe('op_abc'); expect(claims.kyc_level).toBe('enhanced'); From a99eba86bac7e80d8583209c1103126b493cd346 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 10:33:49 -0700 Subject: [PATCH 23/35] fix(identity): align hand-crafted capability fixture schema URL with SDK 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) --- scripts/regenerate-cross-lang-fixtures.ts | 2 +- tests/fixtures/cross-lang/node-capability.json | 8 ++++---- tests/fixtures/cross-lang/node-data-driven-claims.json | 6 +++--- tests/fixtures/cross-lang/node-emoji-keys.json | 6 +++--- tests/fixtures/cross-lang/node-es256-rails.json | 10 +++++----- tests/fixtures/cross-lang/node-extras-int.json | 6 +++--- tests/fixtures/cross-lang/node-int-boundary.json | 6 +++--- tests/fixtures/cross-lang/node-minimal.json | 6 +++--- tests/fixtures/cross-lang/node-multikey.json | 10 +++++----- tests/fixtures/cross-lang/node-typed-claims.json | 6 +++--- tests/fixtures/cross-lang/node-unicode.json | 6 +++--- tests/fixtures/cross-lang/py-capability.json | 8 ++++---- tests/fixtures/cross-lang/py-data-driven-claims.json | 6 +++--- tests/fixtures/cross-lang/py-emoji-keys.json | 6 +++--- tests/fixtures/cross-lang/py-es256-rails.json | 10 +++++----- tests/fixtures/cross-lang/py-extras-int.json | 6 +++--- tests/fixtures/cross-lang/py-int-boundary.json | 6 +++--- tests/fixtures/cross-lang/py-minimal.json | 6 +++--- tests/fixtures/cross-lang/py-multikey.json | 10 +++++----- tests/fixtures/cross-lang/py-typed-claims.json | 6 +++--- tests/fixtures/cross-lang/py-unicode.json | 6 +++--- 21 files changed, 71 insertions(+), 71 deletions(-) diff --git a/scripts/regenerate-cross-lang-fixtures.ts b/scripts/regenerate-cross-lang-fixtures.ts index 9918ca8..044035a 100644 --- a/scripts/regenerate-cross-lang-fixtures.ts +++ b/scripts/regenerate-cross-lang-fixtures.ts @@ -128,7 +128,7 @@ async function main(): Promise { capabilities: [ { name: 'sh.agentscore.identity', - schema: 'https://agentscore.sh/schema/identity/1', + schema: 'https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json', version: '1', kyc_required: true, }, diff --git a/tests/fixtures/cross-lang/node-capability.json b/tests/fixtures/cross-lang/node-capability.json index ec8943f..cf5bc1e 100644 --- a/tests/fixtures/cross-lang/node-capability.json +++ b/tests/fixtures/cross-lang/node-capability.json @@ -11,7 +11,7 @@ "capabilities": [ { "name": "sh.agentscore.identity", - "schema": "https://agentscore.sh/schema/identity/1", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", "version": "1", "kyc_required": true } @@ -32,11 +32,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "kFgwv82ZN7H3jk9gHbUDTi6EZZeaUUBsLBgnfm8Mtog" + "x": "W2J3iYt-Q1U5AIkFuN205U90p-BZLwCI-dYhaAbZdUA" } ], "name": "Capability Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hL2lkZW50aXR5LzEiLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkNhcGFiaWxpdHkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9jLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJrRmd3djgyWk43SDNqazlnSGJVRFRpNkVaWmVhVVVCc0xCZ25mbThNdG9nIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.PIG6fQt84ZM1r08g7_vsl1Hhi6B385BFnPCKo7WkbsyjcpKFpvidTmwBjZ6auUzEOyag6IF0OmEz_8gotuEZAw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwidmVyc2lvbiI6IjEifV0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiVzJKM2lZdC1RMVU1QUlrRnVOMjA1VTkwcC1CWkx3Q0ktZFloYUFiWmRVQSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.mwtNLE6M0xn5ghwoqUPVFpVgNTDcaXApIKRRZyzUG_WMwOFD1tekXQU5uEcNJ6kt9rRLAAxfMyz_PB0pUe2dBg" }, "jwks": { "keys": [ @@ -46,7 +46,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "kFgwv82ZN7H3jk9gHbUDTi6EZZeaUUBsLBgnfm8Mtog" + "x": "W2J3iYt-Q1U5AIkFuN205U90p-BZLwCI-dYhaAbZdUA" } ] }, diff --git a/tests/fixtures/cross-lang/node-data-driven-claims.json b/tests/fixtures/cross-lang/node-data-driven-claims.json index 57fcc25..6b59f73 100644 --- a/tests/fixtures/cross-lang/node-data-driven-claims.json +++ b/tests/fixtures/cross-lang/node-data-driven-claims.json @@ -33,11 +33,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "iABhA50IBuZJsPzheBf_qf7suVsmKUQCL7Dw8Uk6CHU" + "x": "9X-YThZErqEd3mhlWLPolMQ_E-ZVtR6nKOJlJvzPLpo" } ], "name": "Data Driven Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1kYXRhLWRyaXZlbi1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiaUFCaEE1MElCdVpKc1B6aGVCZl9xZjdzdVZzbUtVUUNMN0R3OFVrNkNIVSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.0RqtC9uJRddT-U28IcM2BU8yeqxCAvuR7-nLNYQRlxhTeMiYpVRTFHDI54NRGKuzQL1c_X_EFMcsMxPhl073Dg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1kYXRhLWRyaXZlbi1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiOVgtWVRoWkVycUVkM21obFdMUG9sTVFfRS1aVnRSNm5LT0psSnZ6UExwbyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.PUbT2371mHGBf4wfE6m5jhFwb7VEinUlfBmmP7yKj2_uPHFybTsAdmIGIV9PgfH9l_4FkxBfcVk4WNTjU3a7CA" }, "jwks": { "keys": [ @@ -47,7 +47,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "iABhA50IBuZJsPzheBf_qf7suVsmKUQCL7Dw8Uk6CHU" + "x": "9X-YThZErqEd3mhlWLPolMQ_E-ZVtR6nKOJlJvzPLpo" } ] }, diff --git a/tests/fixtures/cross-lang/node-emoji-keys.json b/tests/fixtures/cross-lang/node-emoji-keys.json index d682770..d7c57d0 100644 --- a/tests/fixtures/cross-lang/node-emoji-keys.json +++ b/tests/fixtures/cross-lang/node-emoji-keys.json @@ -22,7 +22,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "3GZayUqGDFe-3oMlX3-ztVFvprBcPJZBreNbGiNWtJA" + "x": "fiqJRqiAc0s1XVU4B8soGyQeu_a6x-Vlm2q93AjpKG0" } ], "name": "Emoji Keys Merchant", @@ -30,7 +30,7 @@ "豈": 2, "": 3, "🍷": 4, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZW1vamkta2V5cy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJhIjoxLCJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWVtb2ppLWtleXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiM0daYXlVcUdERmUtM29NbFgzLXp0VkZ2cHJCY1BKWkJyZU5iR2lOV3RKQSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsIuixiCI6Miwi7oCAIjozLCLwn423Ijo0fQ.PB3GPO2nViPHCCEq7EZXIhYrLdf4STu0jgvE2SHMBlftC8yZzTxoDRU4yEpBIVDoUGXV0nNBXC6yNUlui4jOBw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZW1vamkta2V5cy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJhIjoxLCJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWVtb2ppLWtleXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiZmlxSlJxaUFjMHMxWFZVNEI4c29HeVFldV9hNngtVmxtMnE5M0FqcEtHMCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsIuixiCI6Miwi7oCAIjozLCLwn423Ijo0fQ.qNiFf0E5z9tpQdRYHQBzcTJYE9E-CctXUV0ZisplcBNKPeM7a5rXi05Z-nfIwlg9L3kIhA5Pi0iqdmymwrwSBQ" }, "jwks": { "keys": [ @@ -40,7 +40,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "3GZayUqGDFe-3oMlX3-ztVFvprBcPJZBreNbGiNWtJA" + "x": "fiqJRqiAc0s1XVU4B8soGyQeu_a6x-Vlm2q93AjpKG0" } ] }, diff --git a/tests/fixtures/cross-lang/node-es256-rails.json b/tests/fixtures/cross-lang/node-es256-rails.json index 03702f1..87108de 100644 --- a/tests/fixtures/cross-lang/node-es256-rails.json +++ b/tests/fixtures/cross-lang/node-es256-rails.json @@ -37,12 +37,12 @@ "use": "sig", "crv": "P-256", "kty": "EC", - "x": "CkXn7DA7i8sfzXfW7lCMhtFQ7B22baNab72gYKTbhLk", - "y": "Xti6FK9qpiBtI6WoDSl7fMbaOMi0SVM9B0w7QjGAmiI" + "x": "eF7O-0UhTnZOMUGovXaHe_shSo6DdXZcKm5jBbGTY4w", + "y": "haXjE-ASyvJQdyGQ7zbRUTLjiOLHcQT2_e1cPr39lhg" } ], "name": "ES256 Merchant", - "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJrdHkiOiJFQyIsInVzZSI6InNpZyIsIngiOiJDa1huN0RBN2k4c2Z6WGZXN2xDTWh0RlE3QjIyYmFOYWI3MmdZS1RiaExrIiwieSI6Ilh0aTZGSzlxcGlCdEk2V29EU2w3Zk1iYU9NaTBTVk05QjB3N1FqR0FtaUkifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.YUy5baNvQdxWp9Wowcxu0mVufOv6mOkCIrbQJQpLFQuPt45nX-d3g1f04zSNxLhTS-5Z9OFmz3UsWjGJOLpaLQ" + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJrdHkiOiJFQyIsInVzZSI6InNpZyIsIngiOiJlRjdPLTBVaFRuWk9NVUdvdlhhSGVfc2hTbzZEZFhaY0ttNWpCYkdUWTR3IiwieSI6ImhhWGpFLUFTeXZKUWR5R1E3emJSVVRMamlPTEhjUVQyX2UxY1ByMzlsaGcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.BgY_Em8HJ4WIro_-WpmiTPnBqi5W5e06fOF662as2v6bTQRmP-JfIqkOdoWKs68YrnVwk-zax8APgcAiOo6ERQ" }, "jwks": { "keys": [ @@ -52,8 +52,8 @@ "use": "sig", "crv": "P-256", "kty": "EC", - "x": "CkXn7DA7i8sfzXfW7lCMhtFQ7B22baNab72gYKTbhLk", - "y": "Xti6FK9qpiBtI6WoDSl7fMbaOMi0SVM9B0w7QjGAmiI" + "x": "eF7O-0UhTnZOMUGovXaHe_shSo6DdXZcKm5jBbGTY4w", + "y": "haXjE-ASyvJQdyGQ7zbRUTLjiOLHcQT2_e1cPr39lhg" } ] }, diff --git a/tests/fixtures/cross-lang/node-extras-int.json b/tests/fixtures/cross-lang/node-extras-int.json index 9448838..f478236 100644 --- a/tests/fixtures/cross-lang/node-extras-int.json +++ b/tests/fixtures/cross-lang/node-extras-int.json @@ -25,11 +25,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "teOaKZqmXMzlEA-VliLxgfI66IcrOQ-v3CQftdC8rJ8" + "x": "76eVD6Zq7GlZJb7YTAKpl7t5GmCz1MRCC0QixYG8ZiU" } ], "name": "Extras Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoidGVPYUtacW1YTXpsRUEtVmxpTHhnZkk2Nkljck9RLXYzQ1FmdGRDOHJKOCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.1XePOww8hYlUbgG2agc-DYCW540mSXYPoAwNTpLs0bkQZ7KxSBZ3ywpjrKh3B6VdGtpRKySgXuEBN9Y-oO3fBA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiNzZlVkQ2WnE3R2xaSmI3WVRBS3BsN3Q1R21DejFNUkNDMFFpeFlHOFppVSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.xFnQJQ9aOp8ErMBxsuUqS9RCD49ZvIBApbSPMzgwtPykYkqj2Wbx32sYlLY2GFu4ipQx-XshDaK6sImzHy2IBA" }, "jwks": { "keys": [ @@ -39,7 +39,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "teOaKZqmXMzlEA-VliLxgfI66IcrOQ-v3CQftdC8rJ8" + "x": "76eVD6Zq7GlZJb7YTAKpl7t5GmCz1MRCC0QixYG8ZiU" } ] }, diff --git a/tests/fixtures/cross-lang/node-int-boundary.json b/tests/fixtures/cross-lang/node-int-boundary.json index 7e27c5b..aafea5c 100644 --- a/tests/fixtures/cross-lang/node-int-boundary.json +++ b/tests/fixtures/cross-lang/node-int-boundary.json @@ -17,7 +17,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "sXu5mABH7PE57nRP1-oRCs3ubCDb4-n12Y8rLOl4UTE" + "x": "u8lJmcdJDrxcns-GfXH4Tnv5XsbMbanbW6tflZWXZig" } ], "name": "Int Boundary Merchant", @@ -26,7 +26,7 @@ "small_int": 42, "neg_small_int": -42, "zero": 0, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaW50LWJvdW5kYXJ5LUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4Ijoic1h1NW1BQkg3UEU1N25SUDEtb1JDczN1YkNEYjQtbjEyWThyTE9sNFVURSJ9XSwic21hbGxfaW50Ijo0Miwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsInplcm8iOjB9.h4E6dSRdyvnJ1bB15NmTetEWnAkLsJYQKMBf1HaxlO_REUsVyz69qjFzs274bEZXv_SAumvZhA0KHjwMVHlQBA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaW50LWJvdW5kYXJ5LUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoidThsSm1jZEpEcnhjbnMtR2ZYSDRUbnY1WHNiTWJhbmJXNnRmbFpXWFppZyJ9XSwic21hbGxfaW50Ijo0Miwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsInplcm8iOjB9.WMSttoe3Gw89si_bPwYG7X0s8MxuyY0z3K73Bx9-JzfKIUDcQCo5HkFWN8pjdgkQDKLa6qHg1voj4fSf87qpAg" }, "jwks": { "keys": [ @@ -36,7 +36,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "sXu5mABH7PE57nRP1-oRCs3ubCDb4-n12Y8rLOl4UTE" + "x": "u8lJmcdJDrxcns-GfXH4Tnv5XsbMbanbW6tflZWXZig" } ] }, diff --git a/tests/fixtures/cross-lang/node-minimal.json b/tests/fixtures/cross-lang/node-minimal.json index da5bf21..8a831e4 100644 --- a/tests/fixtures/cross-lang/node-minimal.json +++ b/tests/fixtures/cross-lang/node-minimal.json @@ -17,11 +17,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "sDSRPsve9rtfqQCJu0TySrPz7cUZR2rGaqN-HY2Rn5c" + "x": "Eu3xy7R5qKTAVsCTUVwHevr-kuSThQs3rrTrCDTVXYw" } ], "name": "Minimal Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1taW5pbWFsLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InNEU1JQc3ZlOXJ0ZnFRQ0p1MFR5U3JQejdjVVpSMnJHYXFOLUhZMlJuNWMifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.S6ZWBp4P6xpbIDbDnoZk3F2yAF2cxDM5QDQ0SnWaASNSKq6henttDoe1XS5Kgr8o8CF51fXwNEUcBKyg91F8CA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1taW5pbWFsLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IkV1M3h5N1I1cUtUQVZzQ1RVVndIZXZyLWt1U1RoUXMzcnJUckNEVFZYWXcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.ILipSybEiTS5CZqKGB3gtqiK0nQY8GqNG8B_K9rp9MtJJtIrmVGmDsWDMLBnCjntLL-dCr0OZnbTVTQvjUFaAw" }, "jwks": { "keys": [ @@ -31,7 +31,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "sDSRPsve9rtfqQCJu0TySrPz7cUZR2rGaqN-HY2Rn5c" + "x": "Eu3xy7R5qKTAVsCTUVwHevr-kuSThQs3rrTrCDTVXYw" } ] }, diff --git a/tests/fixtures/cross-lang/node-multikey.json b/tests/fixtures/cross-lang/node-multikey.json index 4c45aff..5f7d73c 100644 --- a/tests/fixtures/cross-lang/node-multikey.json +++ b/tests/fixtures/cross-lang/node-multikey.json @@ -24,7 +24,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "Ap8p8aQoUCi_zL0WPN9zW_-W1ch3KxT8VulefBvLKlY" + "x": "wRNqLwTtRaQaHxrhcelM7SGsDYot5O9Wl-acDn3yy4M" }, { "kid": "node-multikey-new", @@ -32,11 +32,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "ryOmTDKCGw-Ln_homqdAVYZNOrmxDpila_S-04GxP-A" + "x": "_yZm0qvM75hqnp5ShYwGAe1OWkpxriymzwhXmQAbwuw" } ], "name": "Multi-Key Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJBcDhwOGFRb1VDaV96TDBXUE45eldfLVcxY2gzS3hUOFZ1bGVmQnZMS2xZIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW5ldyIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJyeU9tVERLQ0d3LUxuX2hvbXFkQVZZWk5Pcm14RHBpbGFfUy0wNEd4UC1BIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.kXvc8pxI8tTQeJAqYFmd99w4VA2m0gjbMoqDspGz7UnJ2ycz9ap3oMfx209f4cX-eOidnZVApVW3QguILj-5CQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJ3Uk5xTHdUdFJhUWFIeHJoY2VsTTdTR3NEWW90NU85V2wtYWNEbjN5eTRNIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW5ldyIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJfeVptMHF2TTc1aHFucDVTaFl3R0FlMU9Xa3B4cml5bXp3aFhtUUFid3V3In1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.6AFnRp5fphJLvR0pCw5n-uPqA7ZcO5sGKJtsb0lepJeqdaonfp1FxrK1UeyW-WHRr5wNjk24W8DpKwvljyroCg" }, "jwks": { "keys": [ @@ -46,7 +46,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "Ap8p8aQoUCi_zL0WPN9zW_-W1ch3KxT8VulefBvLKlY" + "x": "wRNqLwTtRaQaHxrhcelM7SGsDYot5O9Wl-acDn3yy4M" }, { "kid": "node-multikey-new", @@ -54,7 +54,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "ryOmTDKCGw-Ln_homqdAVYZNOrmxDpila_S-04GxP-A" + "x": "_yZm0qvM75hqnp5ShYwGAe1OWkpxriymzwhXmQAbwuw" } ] }, diff --git a/tests/fixtures/cross-lang/node-typed-claims.json b/tests/fixtures/cross-lang/node-typed-claims.json index 4475c48..c916148 100644 --- a/tests/fixtures/cross-lang/node-typed-claims.json +++ b/tests/fixtures/cross-lang/node-typed-claims.json @@ -33,11 +33,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "GgQANYQYeQgylzTJo4WpjfVjUh_OqKwdRWX_n2H8YoU" + "x": "VPF2xK_U57i0mas-nh4xk0jwXc8uwTkw24UMfKm1raA" } ], "name": "Typed Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS10eXBlZC1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiR2dRQU5ZUVllUWd5bHpUSm80V3BqZlZqVWhfT3FLd2RSV1hfbjJIOFlvVSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.-LXXBRXXwPs7UmnW3hatAhOBxDBnQrvKCvLuiy8pitNB4b0rtrRx1oI6ATdWKguYSM1Cks9-xMbsrx_PFHAXCw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS10eXBlZC1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiVlBGMnhLX1U1N2kwbWFzLW5oNHhrMGp3WGM4dXdUa3cyNFVNZkttMXJhQSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.BR_pASRexALLD4u45fGpkCSfkdyXCUbXp_cgbq8Yf3etzahb--JcEpKBEPvGT9NNH0nLqvX_tPD_SLD52eudBw" }, "jwks": { "keys": [ @@ -47,7 +47,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "GgQANYQYeQgylzTJo4WpjfVjUh_OqKwdRWX_n2H8YoU" + "x": "VPF2xK_U57i0mas-nh4xk0jwXc8uwTkw24UMfKm1raA" } ] }, diff --git a/tests/fixtures/cross-lang/node-unicode.json b/tests/fixtures/cross-lang/node-unicode.json index 06c9024..1a97535 100644 --- a/tests/fixtures/cross-lang/node-unicode.json +++ b/tests/fixtures/cross-lang/node-unicode.json @@ -24,11 +24,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "cc_2e1ln2ovqQW1kPc4nWWYi_06rxM7k1LAEHLd5JI8" + "x": "-xGkkfTRqK75LsQQalVkmCia5gqMiurDLr-y82H_MkY" } ], "name": "Café 日本 🍷 Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJjY18yZTFsbjJvdnFRVzFrUGM0bldXWWlfMDZyeE03azFMQUVITGQ1Skk4In1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.3RUvt9pT1v4nQ5wS50HbffZbdDJecMpBRcA6aJKRZGN5CYLCFes5QnAhrR431Nv99ekdKwieScxryGXa767EAQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiIteEdra2ZUUnFLNzVMc1FRYWxWa21DaWE1Z3FNaXVyRExyLXk4MkhfTWtZIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.jnU_QJqzrqU9vZzxXXp1Cc2MN-O-aIUCk3JLwpG6nWycRa5vOC5MRkHjgkqA6t8fq5b3XhM4iIiIyH6gAnIhBA" }, "jwks": { "keys": [ @@ -38,7 +38,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "cc_2e1ln2ovqQW1kPc4nWWYi_06rxM7k1LAEHLd5JI8" + "x": "-xGkkfTRqK75LsQQalVkmCia5gqMiurDLr-y82H_MkY" } ] }, diff --git a/tests/fixtures/cross-lang/py-capability.json b/tests/fixtures/cross-lang/py-capability.json index ed6f012..d58978f 100644 --- a/tests/fixtures/cross-lang/py-capability.json +++ b/tests/fixtures/cross-lang/py-capability.json @@ -11,7 +11,7 @@ "capabilities": [ { "name": "sh.agentscore.identity", - "schema": "https://agentscore.sh/schema/identity/1", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", "version": "1", "kyc_required": true } @@ -32,17 +32,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "BXRO6rmnby3lVMe-h0IKk1WuY_HlUgA1VzYsiB4nRdw" + "x": "79JoRX5sYr_-mkVrJr6_auhrF8Wmvnm9t5tT32yH7rk" } ], "name": "Capability Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hL2lkZW50aXR5LzEiLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkNhcGFiaWxpdHkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9jLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiQlhSTzZybW5ieTNsVk1lLWgwSUtrMVd1WV9IbFVnQTFWellzaUI0blJkdyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.m8yHS1g49kY0QdrL0hLiHnwZTOMP6Iu_Hee9dceWhgMSyjDR3qeupkam3P0sbcT0922OZ_tZ2O_bPa_7grHGDw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwidmVyc2lvbiI6IjEifV0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1jYXBhYmlsaXR5LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ijc5Sm9SWDVzWXJfLW1rVnJKcjZfYXVockY4V212bm05dDV0VDMyeUg3cmsifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.ziJgpRk5f06Pp9W5gT8UEJuu8IoFIkotNL4czSLUPSc_D_YRsxy9x-l2YI0tEBs4v9KNR3C-tI6AN7sJF_qjBA" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "BXRO6rmnby3lVMe-h0IKk1WuY_HlUgA1VzYsiB4nRdw", + "x": "79JoRX5sYr_-mkVrJr6_auhrF8Wmvnm9t5tT32yH7rk", "kid": "py-capability-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-data-driven-claims.json b/tests/fixtures/cross-lang/py-data-driven-claims.json index 067155f..d30d650 100644 --- a/tests/fixtures/cross-lang/py-data-driven-claims.json +++ b/tests/fixtures/cross-lang/py-data-driven-claims.json @@ -33,17 +33,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "awUvcjZ9GvvUA8U9-YIcNYi874ritVW28g5OEqnCxvU" + "x": "pyn3Z6hJWE6e2uO3yIdUBTjEryetm9zT5YC1gPVwuEg" } ], "name": "Data Driven Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImF3VXZjalo5R3Z2VUE4VTktWUljTllpODc0cml0VlcyOGc1T0VxbkN4dlUifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.OdQJnBqEivje-fJNKX6iZkJp0OvhwGiv2l6Idmbtfw1Wy_Y0WsKkvnAJ3aZpkNbSgtDP1ZMYtdAqVeMJXjZjDQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InB5bjNaNmhKV0U2ZTJ1TzN5SWRVQlRqRXJ5ZXRtOXpUNVlDMWdQVnd1RWcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.J8tLxhY6RzzZbN7_FNqdQb_jg2ZnWys_b9pgwcAF0lo_D8OYxkRVMXyy9f8HybeN100NftIe2MxLtdvgo-nJDw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "awUvcjZ9GvvUA8U9-YIcNYi874ritVW28g5OEqnCxvU", + "x": "pyn3Z6hJWE6e2uO3yIdUBTjEryetm9zT5YC1gPVwuEg", "kid": "py-data-driven-claims-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-emoji-keys.json b/tests/fixtures/cross-lang/py-emoji-keys.json index a7181af..672532a 100644 --- a/tests/fixtures/cross-lang/py-emoji-keys.json +++ b/tests/fixtures/cross-lang/py-emoji-keys.json @@ -22,7 +22,7 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "bwPt5nJziggvuu2goCiscN4VdBz7TtYWPomXZEfPGNQ" + "x": "gqXBEPq7Ljqwi47V9a1hXAw_cF47ozlPWOAUyl7PJPw" } ], "name": "Emoji Keys Merchant", @@ -32,13 +32,13 @@ "": 3, "🍷": 4 }, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6MSwi6LGIIjoyLCLugIAiOjMsIvCfjbciOjR9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImJ3UHQ1bkp6aWdndnV1MmdvQ2lzY040VmRCejdUdFlXUG9tWFpFZlBHTlEifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.l-nAn3fjDxhLuJabAk9RiKWh2PY6U0qfNSPdkO0gDpFr3nFO9QWxfkdhwBi-Z56eWz_O2Z0AeYigGnfymjq3DA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6MSwi6LGIIjoyLCLugIAiOjMsIvCfjbciOjR9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImdxWEJFUHE3TGpxd2k0N1Y5YTFoWEF3X2NGNDdvemxQV09BVXlsN1BKUHcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ._9z8cnB3LBigLDCURFGWftfmCSx1TAjBMK8Jj6LiPDp5Bf9K64A91N7LPARyEA4XevTcGuZgfUkOPkyDHLLrBw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "bwPt5nJziggvuu2goCiscN4VdBz7TtYWPomXZEfPGNQ", + "x": "gqXBEPq7Ljqwi47V9a1hXAw_cF47ozlPWOAUyl7PJPw", "kid": "py-emoji-keys-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-es256-rails.json b/tests/fixtures/cross-lang/py-es256-rails.json index f0f5ff7..6721404 100644 --- a/tests/fixtures/cross-lang/py-es256-rails.json +++ b/tests/fixtures/cross-lang/py-es256-rails.json @@ -37,19 +37,19 @@ "alg": "ES256", "use": "sig", "crv": "P-256", - "x": "hZLxjfZFnyfL_eHxSk8wUfO7BmfLVHrk9zdZO38JrsM", - "y": "xOGf2dmncLo4CrqrrJt8Q_GFjgw5jrUTWO6W_OEJyo0" + "x": "ij53rGpVeEdMvXp9SPsQjDYrlXwHxHzr0ztBBWzh3nE", + "y": "4BC0ugFM_APcWohu5tGL3mQ6VNkoBMbPnvhXyN-b9-M" } ], "name": "ES256 Merchant", - "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiaFpMeGpmWkZueWZMX2VIeFNrOHdVZk83Qm1mTFZIcms5emRaTzM4SnJzTSIsInkiOiJ4T0dmMmRtbmNMbzRDcnFyckp0OFFfR0ZqZ3c1anJVVFdPNldfT0VKeW8wIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.oFLtXApvevr2l1uJ8qvzA9NrLGinPpZLRQGXayyTmirfiGGOIfffGe8zRRoeR4G4XxTuexobKRKxdO_UqzI38A" + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiaWo1M3JHcFZlRWRNdlhwOVNQc1FqRFlybFh3SHhIenIwenRCQld6aDNuRSIsInkiOiI0QkMwdWdGTV9BUGNXb2h1NXRHTDNtUTZWTmtvQk1iUG52aFh5Ti1iOS1NIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0._MuId-pYaECzZjm3ZcbKqXAtH4INWqOnFwSsDlM7U9tmbwtsOTw0-4v4XXDJtU1KZt1dB7J-TOwdRd1OAv7mVg" }, "jwks": { "keys": [ { "crv": "P-256", - "x": "hZLxjfZFnyfL_eHxSk8wUfO7BmfLVHrk9zdZO38JrsM", - "y": "xOGf2dmncLo4CrqrrJt8Q_GFjgw5jrUTWO6W_OEJyo0", + "x": "ij53rGpVeEdMvXp9SPsQjDYrlXwHxHzr0ztBBWzh3nE", + "y": "4BC0ugFM_APcWohu5tGL3mQ6VNkoBMbPnvhXyN-b9-M", "kid": "py-es256-rails-ES256", "alg": "ES256", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-extras-int.json b/tests/fixtures/cross-lang/py-extras-int.json index 436749a..a8eab9f 100644 --- a/tests/fixtures/cross-lang/py-extras-int.json +++ b/tests/fixtures/cross-lang/py-extras-int.json @@ -25,17 +25,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "QvDeYnvr6i5EgiSwQ3EDgiPMv1obGL0nScKyBZ26Yi0" + "x": "oLnV-NyrlmlJnhDsqcYH6dRgh0A_W76k-dcTWKX8wpU" } ], "name": "Extras Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1leHRyYXMtaW50LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IlF2RGVZbnZyNmk1RWdpU3dRM0VEZ2lQTXYxb2JHTDBuU2NLeUJaMjZZaTAifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.OXvd3vtjSd-gcfaF-od9V76PXZL0ebugDWzjmK8BbTlyIB9SnDFn2MHVk00XwFXX4ZM8TX2UQpAQ3HB6TXsvBg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1leHRyYXMtaW50LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Im9MblYtTnlybG1sSm5oRHNxY1lINmRSZ2gwQV9XNzZrLWRjVFdLWDh3cFUifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.EEqKepCa3CQnoqICbmOnQ6L132k9v0NEqA-8_-LwH01oW5cgcL7gX0LWR6jDLQlZAqMqXYmlSqfHJU4MfEQVCw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "QvDeYnvr6i5EgiSwQ3EDgiPMv1obGL0nScKyBZ26Yi0", + "x": "oLnV-NyrlmlJnhDsqcYH6dRgh0A_W76k-dcTWKX8wpU", "kid": "py-extras-int-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-int-boundary.json b/tests/fixtures/cross-lang/py-int-boundary.json index 6b5c697..e2f26fe 100644 --- a/tests/fixtures/cross-lang/py-int-boundary.json +++ b/tests/fixtures/cross-lang/py-int-boundary.json @@ -17,7 +17,7 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "mDepmBzMdz9K5ujAdDAbE9Q-50SIRoa8CQu_386C6Os" + "x": "4SHOZmcJU7-moS3oCHJZ4Tg5EUUDVt6SJ5ipCez2HQY" } ], "name": "Int Boundary Merchant", @@ -28,13 +28,13 @@ "neg_small_int": -42, "zero": 0 }, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsibWF4X3NhZmVfaW50Ijo5MDA3MTk5MjU0NzQwOTkxLCJtaW5fc2FmZV9pbnQiOi05MDA3MTk5MjU0NzQwOTkxLCJuZWdfc21hbGxfaW50IjotNDIsInNtYWxsX2ludCI6NDIsInplcm8iOjB9LCJuYW1lIjoiSW50IEJvdW5kYXJ5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W10sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoibURlcG1Cek1kejlLNXVqQWREQWJFOVEtNTBTSVJvYThDUXVfMzg2QzZPcyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.s1msUhEmZIQ--XsMO4z7ODtWy7jVVn5zjJ3fmxIYmjMVxgz_oHcVfKNHsgETnyUf3P9PZLXQBetmCZoXA-8BCA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsibWF4X3NhZmVfaW50Ijo5MDA3MTk5MjU0NzQwOTkxLCJtaW5fc2FmZV9pbnQiOi05MDA3MTk5MjU0NzQwOTkxLCJuZWdfc21hbGxfaW50IjotNDIsInNtYWxsX2ludCI6NDIsInplcm8iOjB9LCJuYW1lIjoiSW50IEJvdW5kYXJ5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W10sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiNFNIT1ptY0pVNy1tb1Mzb0NISlo0VGc1RVVVRFZ0NlNKNWlwQ2V6MkhRWSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.IECpY2YYknqwNIR44iHDqcgW_ssf7s50NJySbSIjXU-Nx3vZYRw96oXAt5DGTnfUm5gxBykJrOOrb1hDycm4DA" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "mDepmBzMdz9K5ujAdDAbE9Q-50SIRoa8CQu_386C6Os", + "x": "4SHOZmcJU7-moS3oCHJZ4Tg5EUUDVt6SJ5ipCez2HQY", "kid": "py-int-boundary-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-minimal.json b/tests/fixtures/cross-lang/py-minimal.json index df6d66a..802fe64 100644 --- a/tests/fixtures/cross-lang/py-minimal.json +++ b/tests/fixtures/cross-lang/py-minimal.json @@ -17,17 +17,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "8ACvl-Jzck2_55z9rb4CeETjDUUKJpmRulL0yi0LeVg" + "x": "TPmFauB427PT9-4Bw3UFpEoDlOTgqBKhO1c35oFS84s" } ], "name": "Minimal Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI4QUN2bC1KemNrMl81NXo5cmI0Q2VFVGpEVVVLSnBtUnVsTDB5aTBMZVZnIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.CdDIBYUH8v1TWdICnPXKTA0MJF-ImzrZ7CpV6YaR1Ka1B-nPfjQKyq6_lDVIuit4xbSRI3p69bwhYP_aJ_6JDw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJUUG1GYXVCNDI3UFQ5LTRCdzNVRnBFb0RsT1RncUJLaE8xYzM1b0ZTODRzIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.Y2lCI49wwYviyczTZY34je4zJSWrcE1Bvba1XuBZRqYy0x_EW4pEnEpK5-Ldkb7KCxcrIGhtSCcMm1wMC6zyDg" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "8ACvl-Jzck2_55z9rb4CeETjDUUKJpmRulL0yi0LeVg", + "x": "TPmFauB427PT9-4Bw3UFpEoDlOTgqBKhO1c35oFS84s", "kid": "py-minimal-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-multikey.json b/tests/fixtures/cross-lang/py-multikey.json index 16c0711..c50a1a2 100644 --- a/tests/fixtures/cross-lang/py-multikey.json +++ b/tests/fixtures/cross-lang/py-multikey.json @@ -24,7 +24,7 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "72U644xYLhnRGvY4aiTSppjuhSO14_tSlAR0oN9mo_g" + "x": "d2NIlHf-SNBZEDSPgHcQBW8NURtFe3ILy5sUObcCS1A" }, { "kid": "py-multikey-new", @@ -32,17 +32,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "cUE5GPqn-uWHWUheNve6AInP8PTSF0i-F-lxXhaZnhc" + "x": "xgZThpeX4H8TUX2UZazZpI0wsjgGKn8MTZcckeTBPWk" } ], "name": "Multi-Key Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1vbGQiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiNzJVNjQ0eFlMaG5SR3ZZNGFpVFNwcGp1aFNPMTRfdFNsQVIwb045bW9fZyJ9LHsiYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImNVRTVHUHFuLXVXSFdVaGVOdmU2QUluUDhQVFNGMGktRi1seFhoYVpuaGMifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.iL3NR2xylMO6i21kGvgbXwf6kiXaN4dI7qbGdD0mWcA-HvQjB31LCHjENEcqQrYxxmi40a2YP_6r-HAOAheYDw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1vbGQiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiZDJOSWxIZi1TTkJaRURTUGdIY1FCVzhOVVJ0RmUzSUx5NXNVT2JjQ1MxQSJ9LHsiYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InhnWlRocGVYNEg4VFVYMlVaYXpacEkwd3NqZ0dLbjhNVFpjY2tlVEJQV2sifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.9hcCiYi0FNGHXSJ_Lywa0QBR8r7kTGFbH3DOm7TgbIcRj-YbsIQjpE65V5rqwQS6qwnfq5DJQg6c5R7SA7k4Dg" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "72U644xYLhnRGvY4aiTSppjuhSO14_tSlAR0oN9mo_g", + "x": "d2NIlHf-SNBZEDSPgHcQBW8NURtFe3ILy5sUObcCS1A", "kid": "py-multikey-old", "alg": "EdDSA", "use": "sig", @@ -50,7 +50,7 @@ }, { "crv": "Ed25519", - "x": "cUE5GPqn-uWHWUheNve6AInP8PTSF0i-F-lxXhaZnhc", + "x": "xgZThpeX4H8TUX2UZazZpI0wsjgGKn8MTZcckeTBPWk", "kid": "py-multikey-new", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-typed-claims.json b/tests/fixtures/cross-lang/py-typed-claims.json index 9b90095..40b46f2 100644 --- a/tests/fixtures/cross-lang/py-typed-claims.json +++ b/tests/fixtures/cross-lang/py-typed-claims.json @@ -33,17 +33,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "mXjsvqH0EcymykVn2tr8hwh5QmRk0z-viXjVtxydRWE" + "x": "KtFSWARHs9TmHwRGKXUTLsIg0PfG0oo7j7wRUKQ-OT4" } ], "name": "Typed Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Im1YanN2cUgwRWN5bXlrVm4ydHI4aHdoNVFtUmswei12aVhqVnR4eWRSV0UifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.GsJB8FWsiitETgYR6M8HgGDOgEQ-o2JOAkLpV2emdr9AtMWDlewhs78L-ErX7biQ14joDiuI2BCGpWPXytIuCA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ikt0RlNXQVJIczlUbUh3UkdLWFVUTHNJZzBQZkcwb283ajd3UlVLUS1PVDQifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.MYdFE3z36uXmleFYuF4-PdQglR7lHAEYaPbcy5MyWEwLcfQQZARPW_V3AMrm7YVHw-RjkgTYYoRoQRQeqzwPAA" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "mXjsvqH0EcymykVn2tr8hwh5QmRk0z-viXjVtxydRWE", + "x": "KtFSWARHs9TmHwRGKXUTLsIg0PfG0oo7j7wRUKQ-OT4", "kid": "py-typed-claims-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-unicode.json b/tests/fixtures/cross-lang/py-unicode.json index f0dd135..6483595 100644 --- a/tests/fixtures/cross-lang/py-unicode.json +++ b/tests/fixtures/cross-lang/py-unicode.json @@ -24,17 +24,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "T0Zmk7XdS5Wc4VuEXNKmISZBCkSrYlreWHsHA5u8eKg" + "x": "rLZoFrjW3H24e5W996_q_0oLJ4vB1iP81hCN9uRW92U" } ], "name": "Café 日本 🍷 Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiVDBabWs3WGRTNVdjNFZ1RVhOS21JU1pCQ2tTcllscmVXSHNIQTV1OGVLZyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.EHFh_jdwp22iavAYT1fohKdE6ZG6tszSNIF2dcliQQ7hFAeeWyLP1c3MjhJdEs7zakGYJ8hv_PTgJrXKfAlZDQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4Ijoickxab0ZyalczSDI0ZTVXOTk2X3FfMG9MSjR2QjFpUDgxaENOOXVSVzkyVSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.wNjLqaN9mHK-jLlHlVLPYB9SonwFurK0bE41_zRx-aJedCE4uUSrCoqkOOdqwq4h5yx4-tTQnqLjYkoyng3BAQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "T0Zmk7XdS5Wc4VuEXNKmISZBCkSrYlreWHsHA5u8eKg", + "x": "rLZoFrjW3H24e5W996_q_0oLJ4vB1iP81hCN9uRW92U", "kid": "py-unicode-EdDSA", "alg": "EdDSA", "use": "sig", From dc406538865c2f91b34365998996b538e910489b Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 10:50:16 -0700 Subject: [PATCH 24/35] test(identity): refresh cross-lang fixture corpus with corrected py emoji-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) --- tests/fixtures/cross-lang/node-capability.json | 6 +++--- .../cross-lang/node-data-driven-claims.json | 6 +++--- tests/fixtures/cross-lang/node-emoji-keys.json | 6 +++--- .../fixtures/cross-lang/node-es256-rails.json | 10 +++++----- tests/fixtures/cross-lang/node-extras-int.json | 6 +++--- .../fixtures/cross-lang/node-int-boundary.json | 6 +++--- tests/fixtures/cross-lang/node-minimal.json | 6 +++--- tests/fixtures/cross-lang/node-multikey.json | 10 +++++----- .../fixtures/cross-lang/node-typed-claims.json | 6 +++--- tests/fixtures/cross-lang/node-unicode.json | 6 +++--- tests/fixtures/cross-lang/py-capability.json | 6 +++--- .../cross-lang/py-data-driven-claims.json | 6 +++--- tests/fixtures/cross-lang/py-emoji-keys.json | 16 +++++++--------- tests/fixtures/cross-lang/py-es256-rails.json | 10 +++++----- tests/fixtures/cross-lang/py-extras-int.json | 6 +++--- tests/fixtures/cross-lang/py-int-boundary.json | 18 ++++++++---------- tests/fixtures/cross-lang/py-minimal.json | 6 +++--- tests/fixtures/cross-lang/py-multikey.json | 10 +++++----- tests/fixtures/cross-lang/py-typed-claims.json | 6 +++--- tests/fixtures/cross-lang/py-unicode.json | 6 +++--- 20 files changed, 77 insertions(+), 81 deletions(-) diff --git a/tests/fixtures/cross-lang/node-capability.json b/tests/fixtures/cross-lang/node-capability.json index cf5bc1e..f148b37 100644 --- a/tests/fixtures/cross-lang/node-capability.json +++ b/tests/fixtures/cross-lang/node-capability.json @@ -32,11 +32,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "W2J3iYt-Q1U5AIkFuN205U90p-BZLwCI-dYhaAbZdUA" + "x": "AeclvTjS8f6B3AwW9kO4yjbZCEShPVIBiNFGFR4ZZp4" } ], "name": "Capability Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwidmVyc2lvbiI6IjEifV0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiVzJKM2lZdC1RMVU1QUlrRnVOMjA1VTkwcC1CWkx3Q0ktZFloYUFiWmRVQSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.mwtNLE6M0xn5ghwoqUPVFpVgNTDcaXApIKRRZyzUG_WMwOFD1tekXQU5uEcNJ6kt9rRLAAxfMyz_PB0pUe2dBg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwidmVyc2lvbiI6IjEifV0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiQWVjbHZUalM4ZjZCM0F3VzlrTzR5amJaQ0VTaFBWSUJpTkZHRlI0WlpwNCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.3ThDzMfTI4znrd0200TO0r-vTK2rS_w9BV6_PD0yyKcXvvu_dEqZVOs9R4kZRLJlPpnmoO8YpKg65qzcp5SwDA" }, "jwks": { "keys": [ @@ -46,7 +46,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "W2J3iYt-Q1U5AIkFuN205U90p-BZLwCI-dYhaAbZdUA" + "x": "AeclvTjS8f6B3AwW9kO4yjbZCEShPVIBiNFGFR4ZZp4" } ] }, diff --git a/tests/fixtures/cross-lang/node-data-driven-claims.json b/tests/fixtures/cross-lang/node-data-driven-claims.json index 6b59f73..287e64a 100644 --- a/tests/fixtures/cross-lang/node-data-driven-claims.json +++ b/tests/fixtures/cross-lang/node-data-driven-claims.json @@ -33,11 +33,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "9X-YThZErqEd3mhlWLPolMQ_E-ZVtR6nKOJlJvzPLpo" + "x": "_-gSp0gvGWvi1K8l3CY5F_jVGRSnogFBxUwwUiz_wcw" } ], "name": "Data Driven Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1kYXRhLWRyaXZlbi1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiOVgtWVRoWkVycUVkM21obFdMUG9sTVFfRS1aVnRSNm5LT0psSnZ6UExwbyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.PUbT2371mHGBf4wfE6m5jhFwb7VEinUlfBmmP7yKj2_uPHFybTsAdmIGIV9PgfH9l_4FkxBfcVk4WNTjU3a7CA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1kYXRhLWRyaXZlbi1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiXy1nU3AwZ3ZHV3ZpMUs4bDNDWTVGX2pWR1JTbm9nRkJ4VXd3VWl6X3djdyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.iDuLf2JRjIr-Dx2siH9gft6X7UUsY1X3uZAa1cSuED33hlNePVK5j-oOOn0c66DrFVeGfCgBrlpG0KZ3InVvCA" }, "jwks": { "keys": [ @@ -47,7 +47,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "9X-YThZErqEd3mhlWLPolMQ_E-ZVtR6nKOJlJvzPLpo" + "x": "_-gSp0gvGWvi1K8l3CY5F_jVGRSnogFBxUwwUiz_wcw" } ] }, diff --git a/tests/fixtures/cross-lang/node-emoji-keys.json b/tests/fixtures/cross-lang/node-emoji-keys.json index d7c57d0..c1507dc 100644 --- a/tests/fixtures/cross-lang/node-emoji-keys.json +++ b/tests/fixtures/cross-lang/node-emoji-keys.json @@ -22,7 +22,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "fiqJRqiAc0s1XVU4B8soGyQeu_a6x-Vlm2q93AjpKG0" + "x": "c3dtoA-lzWibbsG7II88-F90FpkjTaBejCYYpNzCLKw" } ], "name": "Emoji Keys Merchant", @@ -30,7 +30,7 @@ "豈": 2, "": 3, "🍷": 4, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZW1vamkta2V5cy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJhIjoxLCJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWVtb2ppLWtleXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiZmlxSlJxaUFjMHMxWFZVNEI4c29HeVFldV9hNngtVmxtMnE5M0FqcEtHMCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsIuixiCI6Miwi7oCAIjozLCLwn423Ijo0fQ.qNiFf0E5z9tpQdRYHQBzcTJYE9E-CctXUV0ZisplcBNKPeM7a5rXi05Z-nfIwlg9L3kIhA5Pi0iqdmymwrwSBQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZW1vamkta2V5cy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJhIjoxLCJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWVtb2ppLWtleXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiYzNkdG9BLWx6V2liYnNHN0lJODgtRjkwRnBralRhQmVqQ1lZcE56Q0xLdyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsIuixiCI6Miwi7oCAIjozLCLwn423Ijo0fQ.mH8PvKgGWxaUZMBIAu7ePxjpWaP9RM970enSZkSlLUKTURgmoWCkvxqWvwm4eIiQ2q-OK3UMOw7_I1qO2xEBCQ" }, "jwks": { "keys": [ @@ -40,7 +40,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "fiqJRqiAc0s1XVU4B8soGyQeu_a6x-Vlm2q93AjpKG0" + "x": "c3dtoA-lzWibbsG7II88-F90FpkjTaBejCYYpNzCLKw" } ] }, diff --git a/tests/fixtures/cross-lang/node-es256-rails.json b/tests/fixtures/cross-lang/node-es256-rails.json index 87108de..f8e67ca 100644 --- a/tests/fixtures/cross-lang/node-es256-rails.json +++ b/tests/fixtures/cross-lang/node-es256-rails.json @@ -37,12 +37,12 @@ "use": "sig", "crv": "P-256", "kty": "EC", - "x": "eF7O-0UhTnZOMUGovXaHe_shSo6DdXZcKm5jBbGTY4w", - "y": "haXjE-ASyvJQdyGQ7zbRUTLjiOLHcQT2_e1cPr39lhg" + "x": "_Hq8UqyZbxKGSySRkLkNNigGoBOs9O49vbV6NEPPFfw", + "y": "1NIEwISSuJ8qbASd6QBCFooBPsphl4m4-zYM56bm-Dg" } ], "name": "ES256 Merchant", - "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJrdHkiOiJFQyIsInVzZSI6InNpZyIsIngiOiJlRjdPLTBVaFRuWk9NVUdvdlhhSGVfc2hTbzZEZFhaY0ttNWpCYkdUWTR3IiwieSI6ImhhWGpFLUFTeXZKUWR5R1E3emJSVVRMamlPTEhjUVQyX2UxY1ByMzlsaGcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.BgY_Em8HJ4WIro_-WpmiTPnBqi5W5e06fOF662as2v6bTQRmP-JfIqkOdoWKs68YrnVwk-zax8APgcAiOo6ERQ" + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJrdHkiOiJFQyIsInVzZSI6InNpZyIsIngiOiJfSHE4VXF5WmJ4S0dTeVNSa0xrTk5pZ0dvQk9zOU80OXZiVjZORVBQRmZ3IiwieSI6IjFOSUV3SVNTdUo4cWJBU2Q2UUJDRm9vQlBzcGhsNG00LXpZTTU2Ym0tRGcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.Pt-f7cc8KNxAuLM4vlCIt69qCoUlb5SnzQjSncvX-qsMwlwKCKwNNe9n0oRoZ75qQg8v1PZN5RWMwPmhJxIeNA" }, "jwks": { "keys": [ @@ -52,8 +52,8 @@ "use": "sig", "crv": "P-256", "kty": "EC", - "x": "eF7O-0UhTnZOMUGovXaHe_shSo6DdXZcKm5jBbGTY4w", - "y": "haXjE-ASyvJQdyGQ7zbRUTLjiOLHcQT2_e1cPr39lhg" + "x": "_Hq8UqyZbxKGSySRkLkNNigGoBOs9O49vbV6NEPPFfw", + "y": "1NIEwISSuJ8qbASd6QBCFooBPsphl4m4-zYM56bm-Dg" } ] }, diff --git a/tests/fixtures/cross-lang/node-extras-int.json b/tests/fixtures/cross-lang/node-extras-int.json index f478236..e43a6ec 100644 --- a/tests/fixtures/cross-lang/node-extras-int.json +++ b/tests/fixtures/cross-lang/node-extras-int.json @@ -25,11 +25,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "76eVD6Zq7GlZJb7YTAKpl7t5GmCz1MRCC0QixYG8ZiU" + "x": "q8TPukNcTGlAQITtxzuMx-VPo7b0u78TZ6l7tPLZ1Lk" } ], "name": "Extras Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiNzZlVkQ2WnE3R2xaSmI3WVRBS3BsN3Q1R21DejFNUkNDMFFpeFlHOFppVSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.xFnQJQ9aOp8ErMBxsuUqS9RCD49ZvIBApbSPMzgwtPykYkqj2Wbx32sYlLY2GFu4ipQx-XshDaK6sImzHy2IBA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoicThUUHVrTmNUR2xBUUlUdHh6dU14LVZQbzdiMHU3OFRaNmw3dFBMWjFMayJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.9KMviIIukuWTKrLYyrLzyWpSLterso4-TWMe6-i_IPnZ1DkVEeo09ql73NF1dcBk2E8bNetcJ2o603JyLD5pCQ" }, "jwks": { "keys": [ @@ -39,7 +39,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "76eVD6Zq7GlZJb7YTAKpl7t5GmCz1MRCC0QixYG8ZiU" + "x": "q8TPukNcTGlAQITtxzuMx-VPo7b0u78TZ6l7tPLZ1Lk" } ] }, diff --git a/tests/fixtures/cross-lang/node-int-boundary.json b/tests/fixtures/cross-lang/node-int-boundary.json index aafea5c..2a6df84 100644 --- a/tests/fixtures/cross-lang/node-int-boundary.json +++ b/tests/fixtures/cross-lang/node-int-boundary.json @@ -17,7 +17,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "u8lJmcdJDrxcns-GfXH4Tnv5XsbMbanbW6tflZWXZig" + "x": "Szf46vxQ_9bY6fp12Tzs2jxUtDtRnPSLAI2FeaMkhk8" } ], "name": "Int Boundary Merchant", @@ -26,7 +26,7 @@ "small_int": 42, "neg_small_int": -42, "zero": 0, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaW50LWJvdW5kYXJ5LUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoidThsSm1jZEpEcnhjbnMtR2ZYSDRUbnY1WHNiTWJhbmJXNnRmbFpXWFppZyJ9XSwic21hbGxfaW50Ijo0Miwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsInplcm8iOjB9.WMSttoe3Gw89si_bPwYG7X0s8MxuyY0z3K73Bx9-JzfKIUDcQCo5HkFWN8pjdgkQDKLa6qHg1voj4fSf87qpAg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaW50LWJvdW5kYXJ5LUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiU3pmNDZ2eFFfOWJZNmZwMTJUenMyanhVdER0Um5QU0xBSTJGZWFNa2hrOCJ9XSwic21hbGxfaW50Ijo0Miwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsInplcm8iOjB9.DvkIZ_rn0utUn4LQhLsIFeoA9iIhpEb3Gk3_Q93LOLaHg5kuw226m35IFODJV2WwkJtjmJ6-Ib829V_-7iF8Bg" }, "jwks": { "keys": [ @@ -36,7 +36,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "u8lJmcdJDrxcns-GfXH4Tnv5XsbMbanbW6tflZWXZig" + "x": "Szf46vxQ_9bY6fp12Tzs2jxUtDtRnPSLAI2FeaMkhk8" } ] }, diff --git a/tests/fixtures/cross-lang/node-minimal.json b/tests/fixtures/cross-lang/node-minimal.json index 8a831e4..9f840e2 100644 --- a/tests/fixtures/cross-lang/node-minimal.json +++ b/tests/fixtures/cross-lang/node-minimal.json @@ -17,11 +17,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "Eu3xy7R5qKTAVsCTUVwHevr-kuSThQs3rrTrCDTVXYw" + "x": "2Jgxm4RvhN3zQ-tmfPw3e-kqm80FGByMyjmswNnjl0I" } ], "name": "Minimal Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1taW5pbWFsLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IkV1M3h5N1I1cUtUQVZzQ1RVVndIZXZyLWt1U1RoUXMzcnJUckNEVFZYWXcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.ILipSybEiTS5CZqKGB3gtqiK0nQY8GqNG8B_K9rp9MtJJtIrmVGmDsWDMLBnCjntLL-dCr0OZnbTVTQvjUFaAw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1taW5pbWFsLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjJKZ3htNFJ2aE4zelEtdG1mUHczZS1rcW04MEZHQnlNeWptc3dObmpsMEkifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.hcmlPgS0XaPdSe9kPFhaMbIvmNPzEaBJY9jW_ZYW7kDMftsaBt4wwF-SocM8z-dcpos4kNGsPAWKEzGHnYAaAg" }, "jwks": { "keys": [ @@ -31,7 +31,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "Eu3xy7R5qKTAVsCTUVwHevr-kuSThQs3rrTrCDTVXYw" + "x": "2Jgxm4RvhN3zQ-tmfPw3e-kqm80FGByMyjmswNnjl0I" } ] }, diff --git a/tests/fixtures/cross-lang/node-multikey.json b/tests/fixtures/cross-lang/node-multikey.json index 5f7d73c..b218774 100644 --- a/tests/fixtures/cross-lang/node-multikey.json +++ b/tests/fixtures/cross-lang/node-multikey.json @@ -24,7 +24,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "wRNqLwTtRaQaHxrhcelM7SGsDYot5O9Wl-acDn3yy4M" + "x": "OegF87KiSAOfa6Wd3qJKCNPmoFKHBxPaf9YqgvlTQvY" }, { "kid": "node-multikey-new", @@ -32,11 +32,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "_yZm0qvM75hqnp5ShYwGAe1OWkpxriymzwhXmQAbwuw" + "x": "mrwPS0Dm8qiH2sFDtECXMgiRa5u3GS1h5UCG4NqxwxY" } ], "name": "Multi-Key Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJ3Uk5xTHdUdFJhUWFIeHJoY2VsTTdTR3NEWW90NU85V2wtYWNEbjN5eTRNIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW5ldyIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJfeVptMHF2TTc1aHFucDVTaFl3R0FlMU9Xa3B4cml5bXp3aFhtUUFid3V3In1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.6AFnRp5fphJLvR0pCw5n-uPqA7ZcO5sGKJtsb0lepJeqdaonfp1FxrK1UeyW-WHRr5wNjk24W8DpKwvljyroCg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJPZWdGODdLaVNBT2ZhNldkM3FKS0NOUG1vRktIQnhQYWY5WXFndmxUUXZZIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW5ldyIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJtcndQUzBEbThxaUgyc0ZEdEVDWE1naVJhNXUzR1MxaDVVQ0c0TnF4d3hZIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.oc_dpoITPPuL0oft-Lu0rEa7Hm10LNTe8RYqMpPbaA0xtJcQzRyJMiQgJ3sWOiXSRst6CzeuO_von36ansqWCA" }, "jwks": { "keys": [ @@ -46,7 +46,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "wRNqLwTtRaQaHxrhcelM7SGsDYot5O9Wl-acDn3yy4M" + "x": "OegF87KiSAOfa6Wd3qJKCNPmoFKHBxPaf9YqgvlTQvY" }, { "kid": "node-multikey-new", @@ -54,7 +54,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "_yZm0qvM75hqnp5ShYwGAe1OWkpxriymzwhXmQAbwuw" + "x": "mrwPS0Dm8qiH2sFDtECXMgiRa5u3GS1h5UCG4NqxwxY" } ] }, diff --git a/tests/fixtures/cross-lang/node-typed-claims.json b/tests/fixtures/cross-lang/node-typed-claims.json index c916148..22b0c25 100644 --- a/tests/fixtures/cross-lang/node-typed-claims.json +++ b/tests/fixtures/cross-lang/node-typed-claims.json @@ -33,11 +33,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "VPF2xK_U57i0mas-nh4xk0jwXc8uwTkw24UMfKm1raA" + "x": "ZdOtA_ss6CHrTAhGqci7hIFTL8027NfXNMypu_tCKE4" } ], "name": "Typed Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS10eXBlZC1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiVlBGMnhLX1U1N2kwbWFzLW5oNHhrMGp3WGM4dXdUa3cyNFVNZkttMXJhQSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.BR_pASRexALLD4u45fGpkCSfkdyXCUbXp_cgbq8Yf3etzahb--JcEpKBEPvGT9NNH0nLqvX_tPD_SLD52eudBw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS10eXBlZC1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiWmRPdEFfc3M2Q0hyVEFoR3FjaTdoSUZUTDgwMjdOZlhOTXlwdV90Q0tFNCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.KtuEN5n05QAQHkmvcxpYzfXfivsGwSfAx_tODt4nb7qhEOpyqBVGKwwJbX2NEy5D0TpEjyoFp_E6OUAmDkgcBg" }, "jwks": { "keys": [ @@ -47,7 +47,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "VPF2xK_U57i0mas-nh4xk0jwXc8uwTkw24UMfKm1raA" + "x": "ZdOtA_ss6CHrTAhGqci7hIFTL8027NfXNMypu_tCKE4" } ] }, diff --git a/tests/fixtures/cross-lang/node-unicode.json b/tests/fixtures/cross-lang/node-unicode.json index 1a97535..77b7f2f 100644 --- a/tests/fixtures/cross-lang/node-unicode.json +++ b/tests/fixtures/cross-lang/node-unicode.json @@ -24,11 +24,11 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "-xGkkfTRqK75LsQQalVkmCia5gqMiurDLr-y82H_MkY" + "x": "4bSDSdklqdRpcUwMmerxArvPlqFwGDUH13wqNQVRfHc" } ], "name": "Café 日本 🍷 Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiIteEdra2ZUUnFLNzVMc1FRYWxWa21DaWE1Z3FNaXVyRExyLXk4MkhfTWtZIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.jnU_QJqzrqU9vZzxXXp1Cc2MN-O-aIUCk3JLwpG6nWycRa5vOC5MRkHjgkqA6t8fq5b3XhM4iIiIyH6gAnIhBA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI0YlNEU2RrbHFkUnBjVXdNbWVyeEFydlBscUZ3R0RVSDEzd3FOUVZSZkhjIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.sopMGjSMti21_96dyk8cbrkv6tIDStW-lc74IbVhnakgovuGAunvSIMRzqvAXAweYksBrvvuuAVpoSjBXH8fCQ" }, "jwks": { "keys": [ @@ -38,7 +38,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "-xGkkfTRqK75LsQQalVkmCia5gqMiurDLr-y82H_MkY" + "x": "4bSDSdklqdRpcUwMmerxArvPlqFwGDUH13wqNQVRfHc" } ] }, diff --git a/tests/fixtures/cross-lang/py-capability.json b/tests/fixtures/cross-lang/py-capability.json index d58978f..b1c3c6c 100644 --- a/tests/fixtures/cross-lang/py-capability.json +++ b/tests/fixtures/cross-lang/py-capability.json @@ -32,17 +32,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "79JoRX5sYr_-mkVrJr6_auhrF8Wmvnm9t5tT32yH7rk" + "x": "TMBp_r4E06Pdu0h-53QW7ncMNSYwPXLem6kMjY-3We8" } ], "name": "Capability Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwidmVyc2lvbiI6IjEifV0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1jYXBhYmlsaXR5LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ijc5Sm9SWDVzWXJfLW1rVnJKcjZfYXVockY4V212bm05dDV0VDMyeUg3cmsifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.ziJgpRk5f06Pp9W5gT8UEJuu8IoFIkotNL4czSLUPSc_D_YRsxy9x-l2YI0tEBs4v9KNR3C-tI6AN7sJF_qjBA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwidmVyc2lvbiI6IjEifV0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1jYXBhYmlsaXR5LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IlRNQnBfcjRFMDZQZHUwaC01M1FXN25jTU5TWXdQWExlbTZrTWpZLTNXZTgifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.tFu690kDz0E2Iy45Y0MgpUS3G2ocaBgeWFUwwPQSjsXXroi1T4GFZROrA_6MntaKb07CfjLgWoMh9z8Cl3ecBA" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "79JoRX5sYr_-mkVrJr6_auhrF8Wmvnm9t5tT32yH7rk", + "x": "TMBp_r4E06Pdu0h-53QW7ncMNSYwPXLem6kMjY-3We8", "kid": "py-capability-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-data-driven-claims.json b/tests/fixtures/cross-lang/py-data-driven-claims.json index d30d650..8153869 100644 --- a/tests/fixtures/cross-lang/py-data-driven-claims.json +++ b/tests/fixtures/cross-lang/py-data-driven-claims.json @@ -33,17 +33,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "pyn3Z6hJWE6e2uO3yIdUBTjEryetm9zT5YC1gPVwuEg" + "x": "DAaVG_-gxUtcNYUOizP4YJNRwhHhnDWn-stsD7jPO4w" } ], "name": "Data Driven Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InB5bjNaNmhKV0U2ZTJ1TzN5SWRVQlRqRXJ5ZXRtOXpUNVlDMWdQVnd1RWcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.J8tLxhY6RzzZbN7_FNqdQb_jg2ZnWys_b9pgwcAF0lo_D8OYxkRVMXyy9f8HybeN100NftIe2MxLtdvgo-nJDw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IkRBYVZHXy1neFV0Y05ZVU9pelA0WUpOUndoSGhuRFduLXN0c0Q3alBPNHcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.Bo9sH1eHbuXfl6XMp43smJDrGd6KFFoEMejcmgAYTScOiBzgRk9bs7s7YgNSjM4QXnZ-2YXtrI58d1n7tu-4BA" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "pyn3Z6hJWE6e2uO3yIdUBTjEryetm9zT5YC1gPVwuEg", + "x": "DAaVG_-gxUtcNYUOizP4YJNRwhHhnDWn-stsD7jPO4w", "kid": "py-data-driven-claims-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-emoji-keys.json b/tests/fixtures/cross-lang/py-emoji-keys.json index 672532a..8502a8e 100644 --- a/tests/fixtures/cross-lang/py-emoji-keys.json +++ b/tests/fixtures/cross-lang/py-emoji-keys.json @@ -22,23 +22,21 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "gqXBEPq7Ljqwi47V9a1hXAw_cF47ozlPWOAUyl7PJPw" + "x": "rI2JPvoFJRRLd3EbGgoiKut82R3us1TTAIpwhp97BSY" } ], "name": "Emoji Keys Merchant", - "extras": { - "a": 1, - "豈": 2, - "": 3, - "🍷": 4 - }, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6MSwi6LGIIjoyLCLugIAiOjMsIvCfjbciOjR9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImdxWEJFUHE3TGpxd2k0N1Y5YTFoWEF3X2NGNDdvemxQV09BVXlsN1BKUHcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ._9z8cnB3LBigLDCURFGWftfmCSx1TAjBMK8Jj6LiPDp5Bf9K64A91N7LPARyEA4XevTcGuZgfUkOPkyDHLLrBw" + "a": 1, + "豈": 2, + "": 3, + "🍷": 4, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJhIjoxLCJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InJJMkpQdm9GSlJSTGQzRWJHZ29pS3V0ODJSM3VzMVRUQUlwd2hwOTdCU1kifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTciLCLosYgiOjIsIu6AgCI6Mywi8J-NtyI6NH0.1zOHof4cekmU2iphy-mp5DQHS66klN45KOrJ87bKsQYnO6_o2cL6iCEH6GOuQ9qBFTBUfzOWapK1hCAIX3vOAQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "gqXBEPq7Ljqwi47V9a1hXAw_cF47ozlPWOAUyl7PJPw", + "x": "rI2JPvoFJRRLd3EbGgoiKut82R3us1TTAIpwhp97BSY", "kid": "py-emoji-keys-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-es256-rails.json b/tests/fixtures/cross-lang/py-es256-rails.json index 6721404..7694b1e 100644 --- a/tests/fixtures/cross-lang/py-es256-rails.json +++ b/tests/fixtures/cross-lang/py-es256-rails.json @@ -37,19 +37,19 @@ "alg": "ES256", "use": "sig", "crv": "P-256", - "x": "ij53rGpVeEdMvXp9SPsQjDYrlXwHxHzr0ztBBWzh3nE", - "y": "4BC0ugFM_APcWohu5tGL3mQ6VNkoBMbPnvhXyN-b9-M" + "x": "_2HJ2FjbKQFI-uVpxIY8ZIS3htpNb-92IGfIY8kpQhk", + "y": "0KlKsjssFxnAPoozOr2CvYXbuoXih_IzMv-wHQCsBXA" } ], "name": "ES256 Merchant", - "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiaWo1M3JHcFZlRWRNdlhwOVNQc1FqRFlybFh3SHhIenIwenRCQld6aDNuRSIsInkiOiI0QkMwdWdGTV9BUGNXb2h1NXRHTDNtUTZWTmtvQk1iUG52aFh5Ti1iOS1NIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0._MuId-pYaECzZjm3ZcbKqXAtH4INWqOnFwSsDlM7U9tmbwtsOTw0-4v4XXDJtU1KZt1dB7J-TOwdRd1OAv7mVg" + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiXzJISjJGamJLUUZJLXVWcHhJWThaSVMzaHRwTmItOTJJR2ZJWThrcFFoayIsInkiOiIwS2xLc2pzc0Z4bkFQb296T3IyQ3ZZWGJ1b1hpaF9Jek12LXdIUUNzQlhBIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.MmqXHAzDEAPjH6wmOvwDq-yyq-0TCyMXVDwZlgryCN0O2E1sDS4jVeOb69F05uWdiPrLBP-HpJGTxruvHJre-g" }, "jwks": { "keys": [ { "crv": "P-256", - "x": "ij53rGpVeEdMvXp9SPsQjDYrlXwHxHzr0ztBBWzh3nE", - "y": "4BC0ugFM_APcWohu5tGL3mQ6VNkoBMbPnvhXyN-b9-M", + "x": "_2HJ2FjbKQFI-uVpxIY8ZIS3htpNb-92IGfIY8kpQhk", + "y": "0KlKsjssFxnAPoozOr2CvYXbuoXih_IzMv-wHQCsBXA", "kid": "py-es256-rails-ES256", "alg": "ES256", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-extras-int.json b/tests/fixtures/cross-lang/py-extras-int.json index a8eab9f..7632dcf 100644 --- a/tests/fixtures/cross-lang/py-extras-int.json +++ b/tests/fixtures/cross-lang/py-extras-int.json @@ -25,17 +25,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "oLnV-NyrlmlJnhDsqcYH6dRgh0A_W76k-dcTWKX8wpU" + "x": "rSdbpACnhv_GVb7R01lDjmO7kUUvZR6GKBYR0AhW4go" } ], "name": "Extras Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1leHRyYXMtaW50LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Im9MblYtTnlybG1sSm5oRHNxY1lINmRSZ2gwQV9XNzZrLWRjVFdLWDh3cFUifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.EEqKepCa3CQnoqICbmOnQ6L132k9v0NEqA-8_-LwH01oW5cgcL7gX0LWR6jDLQlZAqMqXYmlSqfHJU4MfEQVCw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1leHRyYXMtaW50LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InJTZGJwQUNuaHZfR1ZiN1IwMWxEam1PN2tVVXZaUjZHS0JZUjBBaFc0Z28ifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.SW6CJuYzFh5PJ_AQJ89iUINqhW7O1kZvDQo1zuhqtjtU-Gj48XI2pykZph04lBcBS8r4mVEvNrUzVxVi44hXCw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "oLnV-NyrlmlJnhDsqcYH6dRgh0A_W76k-dcTWKX8wpU", + "x": "rSdbpACnhv_GVb7R01lDjmO7kUUvZR6GKBYR0AhW4go", "kid": "py-extras-int-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-int-boundary.json b/tests/fixtures/cross-lang/py-int-boundary.json index e2f26fe..32a5c44 100644 --- a/tests/fixtures/cross-lang/py-int-boundary.json +++ b/tests/fixtures/cross-lang/py-int-boundary.json @@ -17,24 +17,22 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "4SHOZmcJU7-moS3oCHJZ4Tg5EUUDVt6SJ5ipCez2HQY" + "x": "2hWtYbSpkVrFTzo_rccrGWAYf_jrreq8wB1D_z_IZOc" } ], "name": "Int Boundary Merchant", - "extras": { - "max_safe_int": 9007199254740991, - "min_safe_int": -9007199254740991, - "small_int": 42, - "neg_small_int": -42, - "zero": 0 - }, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsibWF4X3NhZmVfaW50Ijo5MDA3MTk5MjU0NzQwOTkxLCJtaW5fc2FmZV9pbnQiOi05MDA3MTk5MjU0NzQwOTkxLCJuZWdfc21hbGxfaW50IjotNDIsInNtYWxsX2ludCI6NDIsInplcm8iOjB9LCJuYW1lIjoiSW50IEJvdW5kYXJ5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W10sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiNFNIT1ptY0pVNy1tb1Mzb0NISlo0VGc1RVVVRFZ0NlNKNWlwQ2V6MkhRWSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.IECpY2YYknqwNIR44iHDqcgW_ssf7s50NJySbSIjXU-Nx3vZYRw96oXAt5DGTnfUm5gxBykJrOOrb1hDycm4DA" + "max_safe_int": 9007199254740991, + "min_safe_int": -9007199254740991, + "small_int": 42, + "neg_small_int": -42, + "zero": 0, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktaW50LWJvdW5kYXJ5LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjJoV3RZYlNwa1ZyRlR6b19yY2NyR1dBWWZfanJyZXE4d0IxRF96X0laT2MifV0sInNtYWxsX2ludCI6NDIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTciLCJ6ZXJvIjowfQ.1qtHiT04A_A0asQx3jJWeeNEfY9lQ6haBf6JDMoaOz3NmWLg70Yvad0wStR8wQcaDDqPgi7-mmMZMnu8XxvyCQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "4SHOZmcJU7-moS3oCHJZ4Tg5EUUDVt6SJ5ipCez2HQY", + "x": "2hWtYbSpkVrFTzo_rccrGWAYf_jrreq8wB1D_z_IZOc", "kid": "py-int-boundary-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-minimal.json b/tests/fixtures/cross-lang/py-minimal.json index 802fe64..67a31e6 100644 --- a/tests/fixtures/cross-lang/py-minimal.json +++ b/tests/fixtures/cross-lang/py-minimal.json @@ -17,17 +17,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "TPmFauB427PT9-4Bw3UFpEoDlOTgqBKhO1c35oFS84s" + "x": "Jal9VgyjKMP3MguusxxOZJDOb6U7nToLMa7C3hCqu2o" } ], "name": "Minimal Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJUUG1GYXVCNDI3UFQ5LTRCdzNVRnBFb0RsT1RncUJLaE8xYzM1b0ZTODRzIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.Y2lCI49wwYviyczTZY34je4zJSWrcE1Bvba1XuBZRqYy0x_EW4pEnEpK5-Ldkb7KCxcrIGhtSCcMm1wMC6zyDg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJKYWw5Vmd5aktNUDNNZ3V1c3h4T1pKRE9iNlU3blRvTE1hN0MzaENxdTJvIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.isXdp4CcyRr9lh9yHQwjAH-MDgj6ZqUfJjLr3rj6KTBaE_nRIjR_HO4hM44uMnh8RUYm5-JCNVh8m1-lsLGxDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "TPmFauB427PT9-4Bw3UFpEoDlOTgqBKhO1c35oFS84s", + "x": "Jal9VgyjKMP3MguusxxOZJDOb6U7nToLMa7C3hCqu2o", "kid": "py-minimal-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-multikey.json b/tests/fixtures/cross-lang/py-multikey.json index c50a1a2..5333038 100644 --- a/tests/fixtures/cross-lang/py-multikey.json +++ b/tests/fixtures/cross-lang/py-multikey.json @@ -24,7 +24,7 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "d2NIlHf-SNBZEDSPgHcQBW8NURtFe3ILy5sUObcCS1A" + "x": "Jk-XKLK-B6PQDrH5muAusj5s64a_jYGScTY7sR5zwTU" }, { "kid": "py-multikey-new", @@ -32,17 +32,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "xgZThpeX4H8TUX2UZazZpI0wsjgGKn8MTZcckeTBPWk" + "x": "0k8GV3ctomf1kxJIdg8cIpFDrFdWBE6fNW1kRp5j_ew" } ], "name": "Multi-Key Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1vbGQiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiZDJOSWxIZi1TTkJaRURTUGdIY1FCVzhOVVJ0RmUzSUx5NXNVT2JjQ1MxQSJ9LHsiYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InhnWlRocGVYNEg4VFVYMlVaYXpacEkwd3NqZ0dLbjhNVFpjY2tlVEJQV2sifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.9hcCiYi0FNGHXSJ_Lywa0QBR8r7kTGFbH3DOm7TgbIcRj-YbsIQjpE65V5rqwQS6qwnfq5DJQg6c5R7SA7k4Dg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1vbGQiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiSmstWEtMSy1CNlBRRHJINW11QXVzajVzNjRhX2pZR1NjVFk3c1I1endUVSJ9LHsiYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjBrOEdWM2N0b21mMWt4SklkZzhjSXBGRHJGZFdCRTZmTlcxa1JwNWpfZXcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.5sp4yAhsPgmO6F4CvLSADiJ78rk5_KO83r1NkJYKg2bB3flvz1RdkwpodiH66wKOu8XOEDx2Xmr9_RWUb2W4DA" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "d2NIlHf-SNBZEDSPgHcQBW8NURtFe3ILy5sUObcCS1A", + "x": "Jk-XKLK-B6PQDrH5muAusj5s64a_jYGScTY7sR5zwTU", "kid": "py-multikey-old", "alg": "EdDSA", "use": "sig", @@ -50,7 +50,7 @@ }, { "crv": "Ed25519", - "x": "xgZThpeX4H8TUX2UZazZpI0wsjgGKn8MTZcckeTBPWk", + "x": "0k8GV3ctomf1kxJIdg8cIpFDrFdWBE6fNW1kRp5j_ew", "kid": "py-multikey-new", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-typed-claims.json b/tests/fixtures/cross-lang/py-typed-claims.json index 40b46f2..22daba2 100644 --- a/tests/fixtures/cross-lang/py-typed-claims.json +++ b/tests/fixtures/cross-lang/py-typed-claims.json @@ -33,17 +33,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "KtFSWARHs9TmHwRGKXUTLsIg0PfG0oo7j7wRUKQ-OT4" + "x": "wNeB1hL1l7cml2x2miyjUChAxvveRYkuuMug1XJkq64" } ], "name": "Typed Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ikt0RlNXQVJIczlUbUh3UkdLWFVUTHNJZzBQZkcwb283ajd3UlVLUS1PVDQifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.MYdFE3z36uXmleFYuF4-PdQglR7lHAEYaPbcy5MyWEwLcfQQZARPW_V3AMrm7YVHw-RjkgTYYoRoQRQeqzwPAA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IndOZUIxaEwxbDdjbWwyeDJtaXlqVUNoQXh2dmVSWWt1dU11ZzFYSmtxNjQifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.RJjoaDeclNkgVS18FQ4zS7vpoyXV3p0tO1J2YCnrqxe6qM6XQtuCyl90zJQXXFvdz41EEC9et7ZquSUviY2BDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "KtFSWARHs9TmHwRGKXUTLsIg0PfG0oo7j7wRUKQ-OT4", + "x": "wNeB1hL1l7cml2x2miyjUChAxvveRYkuuMug1XJkq64", "kid": "py-typed-claims-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-unicode.json b/tests/fixtures/cross-lang/py-unicode.json index 6483595..8a2154a 100644 --- a/tests/fixtures/cross-lang/py-unicode.json +++ b/tests/fixtures/cross-lang/py-unicode.json @@ -24,17 +24,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "rLZoFrjW3H24e5W996_q_0oLJ4vB1iP81hCN9uRW92U" + "x": "zYxsKbVzECrjQUcCCoXQRJ4_PPA5512kajrQHLUu8nE" } ], "name": "Café 日本 🍷 Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4Ijoickxab0ZyalczSDI0ZTVXOTk2X3FfMG9MSjR2QjFpUDgxaENOOXVSVzkyVSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.wNjLqaN9mHK-jLlHlVLPYB9SonwFurK0bE41_zRx-aJedCE4uUSrCoqkOOdqwq4h5yx4-tTQnqLjYkoyng3BAQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4Ijoiell4c0tiVnpFQ3JqUVVjQ0NvWFFSSjRfUFBBNTUxMmthanJRSExVdThuRSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.q50JBjCcGT3wY0CdcAatQfVlo3K6o_SnEm9UQUPYdSApQwBT8pLofmATVdMoX0NZlr6TbIMaU29EEVl8XjzuBQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "rLZoFrjW3H24e5W996_q_0oLJ4vB1iP81hCN9uRW92U", + "x": "zYxsKbVzECrjQUcCCoXQRJ4_PPA5512kajrQHLUu8nE", "kid": "py-unicode-EdDSA", "alg": "EdDSA", "use": "sig", From bbcd862ec229a7e1b6d9ee3a595ca97a59d12862 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 11:03:33 -0700 Subject: [PATCH 25/35] test(identity): assert UCPPaymentHandler omits config when caller does 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) --- tests/identity/ucp.test.ts | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/identity/ucp.test.ts b/tests/identity/ucp.test.ts index 35f0ebb..19e770e 100644 --- a/tests/identity/ucp.test.ts +++ b/tests/identity/ucp.test.ts @@ -82,6 +82,19 @@ describe('buildUCPProfile', () => { expect((profile as Record).custom_field).toBe('custom_value'); }); + // payment_handler.config is an optional TypeScript property: when the caller + // omits it the wire profile ships without the `config` key. Python's + // `UCPPaymentHandler.to_dict` omits empty configs to match this convention, + // so the same logical input produces the same canonical bytes across SDKs. + it('payment_handler omits config key when caller does not set it (cross-lang parity)', () => { + const profile = buildUCPProfile({ + ...baseInput, + payment_handlers: [{ name: 'tempo' }], + }); + expect(profile.payment_handlers).toEqual([{ name: 'tempo' }]); + expect('config' in (profile.payment_handlers[0] as object)).toBe(false); + }); + it('respects custom version override', () => { const profile = buildUCPProfile({ ...baseInput, version: '2026-12-31' }); expect(profile.version).toBe('2026-12-31'); From 6d8bd4100424bc5b388a1fd1a9c041274eb119d0 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 11:17:37 -0700 Subject: [PATCH 26/35] chore(identity): drop fixture one-shots, document UCP per-element shape 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) --- .../generate-data-driven-claims-fixture.ts | 71 ------------------ scripts/generate-int-boundary-fixture.ts | 57 -------------- scripts/generate-typed-claims-fixture.ts | 74 ------------------- src/identity/ucp.ts | 25 ++++++- 4 files changed, 22 insertions(+), 205 deletions(-) delete mode 100644 scripts/generate-data-driven-claims-fixture.ts delete mode 100644 scripts/generate-int-boundary-fixture.ts delete mode 100644 scripts/generate-typed-claims-fixture.ts diff --git a/scripts/generate-data-driven-claims-fixture.ts b/scripts/generate-data-driven-claims-fixture.ts deleted file mode 100644 index 8dcbf03..0000000 --- a/scripts/generate-data-driven-claims-fixture.ts +++ /dev/null @@ -1,71 +0,0 @@ -/** - * One-shot generator for the data-driven-claims cross-lang fixture (Node side). - * - * Writes `tests/fixtures/cross-lang/node-data-driven-claims.json`. Unlike the - * other cross-lang fixtures (which hand-craft the `agentscore-identity` - * capability), this one EXERCISES `buildUCPProfile`'s data path: it constructs - * a synthetic `AgentScoreData` with the API-shape "missing" sentinels (empty - * string for kyc_level, null for age_bracket / jurisdiction / verified_at) and - * lets the builder coalesce them. Both languages MUST emit identical canonical - * bytes for this input or cross-lang verify drifts silently in production. - */ - -import { writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { buildUCPProfile, type UCPSigningKey } from '../src/identity/ucp'; -import { - buildJWKSResponse, - generateUCPSigningKey, - signUCPProfile, -} from '../src/identity/ucp-jwks'; -import type { AgentScoreData } from '../src/core'; - -const OUT = join(__dirname, '..', 'tests', 'fixtures', 'cross-lang', 'node-data-driven-claims.json'); -const KID = 'node-data-driven-claims-EdDSA'; - -async function main(): Promise { - const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); - - const data: AgentScoreData = { - decision: 'allow', - decision_reasons: [], - resolved_operator: 'op_data_driven', - verify_url: 'https://agentscore.sh/verify/op_data_driven', - account_verification: { - // Empty string is the API's "set but unknown" shape for some columns; - // null is the shape for others. The builder must coerce both to the - // schema default identically across node and python. - kyc_level: '', - sanctions_clear: false, - age_bracket: null as unknown as string, - jurisdiction: null as unknown as string, - verified_at: null, - }, - }; - - const profile = buildUCPProfile({ - name: 'Data Driven Claims Merchant', - services: [{ type: 'rest', url: 'https://d.example.com' }], - payment_handlers: [], - signing_keys: [publicJWK as UCPSigningKey], - data, - }); - - const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); - - const fixture = { - profile: signed, - jwks: buildJWKSResponse([publicJWK]), - alg: 'EdDSA', - kid: KID, - generator: 'node', - }; - - writeFileSync(OUT, `${JSON.stringify(fixture, null, 2)}\n`); - console.warn(`wrote ${OUT}`); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/scripts/generate-int-boundary-fixture.ts b/scripts/generate-int-boundary-fixture.ts deleted file mode 100644 index f327c57..0000000 --- a/scripts/generate-int-boundary-fixture.ts +++ /dev/null @@ -1,57 +0,0 @@ -/** - * One-shot generator for the int-boundary cross-lang fixture (Node side). - * - * Writes `tests/fixtures/cross-lang/node-int-boundary.json`. The fixture - * exercises the safe-integer boundary that BOTH languages must round-trip - * identically: `Number.MAX_SAFE_INTEGER` (2^53 - 1), its negative, zero, and - * small ints. Lossy values (>2^53) are NOT in the fixture (they're rejected - * at sign time); they're unit-tested in each language's signing path. - */ - -import { writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { buildUCPProfile, type UCPSigningKey } from '../src/identity/ucp'; -import { - buildJWKSResponse, - generateUCPSigningKey, - signUCPProfile, -} from '../src/identity/ucp-jwks'; - -const OUT = join(__dirname, '..', 'tests', 'fixtures', 'cross-lang', 'node-int-boundary.json'); -const KID = 'node-int-boundary-EdDSA'; - -async function main(): Promise { - const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); - - const profile = buildUCPProfile({ - name: 'Int Boundary Merchant', - services: [{ type: 'rest', url: 'https://i.example.com' }], - payment_handlers: [], - signing_keys: [publicJWK as UCPSigningKey], - extras: { - max_safe_int: 9007199254740991, - min_safe_int: -9007199254740991, - small_int: 42, - neg_small_int: -42, - zero: 0, - }, - }); - - const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); - - const fixture = { - profile: signed, - jwks: buildJWKSResponse([publicJWK]), - alg: 'EdDSA', - kid: KID, - generator: 'node', - }; - - writeFileSync(OUT, `${JSON.stringify(fixture, null, 2)}\n`); - console.warn(`wrote ${OUT}`); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/scripts/generate-typed-claims-fixture.ts b/scripts/generate-typed-claims-fixture.ts deleted file mode 100644 index 68b6bbf..0000000 --- a/scripts/generate-typed-claims-fixture.ts +++ /dev/null @@ -1,74 +0,0 @@ -/** - * One-shot generator for the typed-claims cross-lang fixture (Node side). - * - * Writes `tests/fixtures/cross-lang/node-typed-claims.json`. Sibling to - * `generate-data-driven-claims-fixture.ts` but exercises the **typed** - * `AgentScoreData.account_verification` / `AgentScoreData.operator_verification` - * read path (no raw fallback) so cross-lang verify catches drift on the - * typed-field-only call site. Python's `build_ucp_profile` reads the typed - * fields first without consulting raw when they are present, so both - * languages must emit the identical canonical bytes for this hand-constructed - * input shape. - */ - -import { writeFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { buildUCPProfile, type UCPSigningKey } from '../src/identity/ucp'; -import { - buildJWKSResponse, - generateUCPSigningKey, - signUCPProfile, -} from '../src/identity/ucp-jwks'; -import type { AgentScoreData } from '../src/core'; - -const OUT = join(__dirname, '..', 'tests', 'fixtures', 'cross-lang', 'node-typed-claims.json'); -const KID = 'node-typed-claims-EdDSA'; - -async function main(): Promise { - const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); - - const data: AgentScoreData = { - decision: 'allow', - decision_reasons: [], - resolved_operator: 'op_typed_claims', - verify_url: 'https://agentscore.sh/verify/op_typed_claims', - operator_verification: { - level: 'enhanced', - operator_type: 'api', - verified_at: '2026-04-01T00:00:00Z', - }, - account_verification: { - kyc_level: 'enhanced', - sanctions_clear: true, - age_bracket: '21+', - jurisdiction: 'US', - verified_at: '2026-04-01T00:00:00Z', - }, - }; - - const profile = buildUCPProfile({ - name: 'Typed Claims Merchant', - services: [{ type: 'rest', url: 'https://t.example.com' }], - payment_handlers: [], - signing_keys: [publicJWK as UCPSigningKey], - data, - }); - - const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); - - const fixture = { - profile: signed, - jwks: buildJWKSResponse([publicJWK]), - alg: 'EdDSA', - kid: KID, - generator: 'node', - }; - - writeFileSync(OUT, `${JSON.stringify(fixture, null, 2)}\n`); - console.warn(`wrote ${OUT}`); -} - -main().catch((err) => { - console.error(err); - process.exit(1); -}); diff --git a/src/identity/ucp.ts b/src/identity/ucp.ts index 1e6bde5..6de0bfa 100644 --- a/src/identity/ucp.ts +++ b/src/identity/ucp.ts @@ -22,14 +22,33 @@ import type { AgentScoreData } from '../core'; +/** + * UCP per-element shape note (applies to UCPSigningKey, UCPService, UCPCapability): + * + * The Node interfaces accept canonical UCP fields plus arbitrary vendor extras + * flat in the same object via `[k: string]: unknown`. There is no separate + * `extras` slot per element. The python sibling models these as dataclasses + * with an explicit `extras: dict` field so callers can isolate canonical from + * vendor fields, and `to_dict()` rejects extras keys that collide with + * reserved canonical names. In Node, the canonical names (`kid` / `kty` for + * signing keys, `type` / `url` / `version` for services, `name` / `schema` / + * `version` for capabilities) are positional fields in the interface, so a + * caller that writes `{ name: 'checkout', name: 'attacker' }` produces a + * static type error or trivially overwrites itself in object-literal syntax; + * there is no separate map that could shadow a reserved key. The two designs + * therefore offer equivalent guarantees through different mechanisms; the + * top-level `BuildUCPProfileInput.extras` slot below DOES carry a runtime + * reserved-key guard because it is a dedicated map and can be populated by + * spreading, where the per-element shapes cannot. + */ export interface UCPSigningKey { /** JWK kid (key id). */ kid: string; - /** JWK kty (key type) — typically `EC`, `RSA`, or `OKP`. */ + /** JWK kty (key type), typically `EC`, `RSA`, or `OKP`. */ kty: string; - /** JWK alg (signing algorithm) — typically `ES256`, `RS256`, or `EdDSA`. */ + /** JWK alg (signing algorithm), typically `ES256`, `RS256`, or `EdDSA`. */ alg?: string; - /** JWK use — typically `sig`. */ + /** JWK use, typically `sig`. */ use?: string; /** JWK crv (curve) for EC / OKP keys. */ crv?: string; From 13d3e618cfbb4c8873f43d64eaa70f74edb5defb Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 05:25:16 -0700 Subject: [PATCH 27/35] =?UTF-8?q?fix(identity):=20rename=20agentscoreSchem?= =?UTF-8?q?aUrl=20=E2=86=92=20agentscore=5Fschema=5Furl=20(python=20parity?= =?UTF-8?q?)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/identity/ucp.ts | 8 +++++--- tests/identity/ucp.test.ts | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/identity/ucp.ts b/src/identity/ucp.ts index 6de0bfa..6847068 100644 --- a/src/identity/ucp.ts +++ b/src/identity/ucp.ts @@ -155,8 +155,10 @@ export interface BuildUCPProfileInput { signing_keys: UCPSigningKey[]; /** AgentScore assess data — adds an `sh.agentscore.identity` capability + claims block when present. */ data?: AgentScoreData | null; - /** Optional override for the AgentScore capability schema URL. */ - agentscoreSchemaUrl?: string; + /** Optional override for the AgentScore capability schema URL. Field is + * snake_cased for cross-language parity with `agentscore_commerce`'s + * `build_ucp_profile(agentscore_schema_url=...)`. */ + agentscore_schema_url?: string; /** Vendor-specific extras at the top level. */ extras?: Record; } @@ -220,7 +222,7 @@ export function buildUCPProfile(input: BuildUCPProfileInput): UCPProfile { baseCapabilities.push({ name: AGENTSCORE_CAPABILITY_NAME, version: AGENTSCORE_CAPABILITY_VERSION, - schema: input.agentscoreSchemaUrl ?? 'https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json', + schema: input.agentscore_schema_url ?? 'https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json', claims, }); } diff --git a/tests/identity/ucp.test.ts b/tests/identity/ucp.test.ts index 19e770e..a647912 100644 --- a/tests/identity/ucp.test.ts +++ b/tests/identity/ucp.test.ts @@ -100,11 +100,11 @@ describe('buildUCPProfile', () => { expect(profile.version).toBe('2026-12-31'); }); - it('respects agentscoreSchemaUrl override', () => { + it('respects agentscore_schema_url override', () => { const profile = buildUCPProfile({ ...baseInput, data: fullData, - agentscoreSchemaUrl: 'https://custom.example/schema.json', + agentscore_schema_url: 'https://custom.example/schema.json', }); const cap = profile.capabilities.find((c) => c.name === AGENTSCORE_UCP_CAPABILITY); expect(cap?.schema).toBe('https://custom.example/schema.json'); From e2434f0ea602b541adf2c4bfc38f3a587aef6aed Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 06:09:36 -0700 Subject: [PATCH 28/35] fix(identity)!: spec-compliant UCP profile shape (ucp envelope + map-keyed bindings) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 keyed by service name (e.g., 'dev.ucp.shopping') - capabilities: Record keyed by capability name - payment_handlers: Record 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) --- src/identity/ucp.ts | 372 ++++++++++++++++++++++--------------- src/index.ts | 7 +- tests/identity/ucp.test.ts | 177 ++++++++++++------ 3 files changed, 348 insertions(+), 208 deletions(-) diff --git a/src/identity/ucp.ts b/src/identity/ucp.ts index 6847068..8735a64 100644 --- a/src/identity/ucp.ts +++ b/src/identity/ucp.ts @@ -1,52 +1,40 @@ /** * UCP (Universal Commerce Protocol) profile builder. * - * Compose the JSON payload published at `/.well-known/ucp` per the UCP spec, with - * AgentScore identity claims attached as a capability. Returned object is the unsigned - * profile body — the merchant signs it (or wraps it in their JWKS-backed envelope) - * before publishing. + * Compose the JSON payload published at `/.well-known/ucp` per the UCP spec. + * Output shape matches the spec example: top-level `{ ucp: {...}, signing_keys: [...] }` + * envelope, with `services` / `capabilities` / `payment_handlers` as MAPs keyed by + * reverse-DNS service / capability / handler name. Verified against the live + * production reference at `https://puravidabracelets.com/.well-known/ucp` (Shopify's + * UCP integration, one of the launch reference brands). * - * Why publish: UCP is the Google-led cross-vendor standard (announced Jan 2026 at NRF - * with Shopify, Etsy, Wayfair, Target, Walmart, Adyen, Mastercard, Stripe, Visa, Amex, - * etc.). Every UCP-aware platform discovers a merchant via `/.well-known/ucp`, so - * shipping this profile means AgentScore-gated merchants are discoverable through the - * same surface every other UCP merchant uses. + * AgentScore identity claims layer over UCP via the `sh.agentscore.identity` capability + * (vendor-namespaced; UCP doesn't define KYC/sanctions/age/jurisdiction natively). The + * capability extends `dev.ucp.shopping.checkout` AND `dev.ucp.shopping.cart` (multi-parent, + * matching Shopify's `dev.shopify.catalog.storefront` pattern in the live ecosystem). * - * Spec reference: https://ucp.dev/ + * The unsigned profile body returned here is what merchants publish; pass it through + * `signUCPProfile` to attach the `agentscore-profile+jws` signature for trust-mode + * verifiers (vendor extension; UCP itself doesn't mandate profile-body signing). * - * UCP profiles do NOT carry KYC / sanctions / age / jurisdiction claims natively — - * identity in the UCP spec is "who signed this" (JWKS-backed). AgentScore claims layer - * over UCP via a custom capability so consumers who care about verified-buyer identity - * can read them; consumers who don't care just see a normal UCP profile. + * Spec reference: https://ucp.dev/ */ import type { AgentScoreData } from '../core'; /** - * UCP per-element shape note (applies to UCPSigningKey, UCPService, UCPCapability): - * - * The Node interfaces accept canonical UCP fields plus arbitrary vendor extras - * flat in the same object via `[k: string]: unknown`. There is no separate - * `extras` slot per element. The python sibling models these as dataclasses - * with an explicit `extras: dict` field so callers can isolate canonical from - * vendor fields, and `to_dict()` rejects extras keys that collide with - * reserved canonical names. In Node, the canonical names (`kid` / `kty` for - * signing keys, `type` / `url` / `version` for services, `name` / `schema` / - * `version` for capabilities) are positional fields in the interface, so a - * caller that writes `{ name: 'checkout', name: 'attacker' }` produces a - * static type error or trivially overwrites itself in object-literal syntax; - * there is no separate map that could shadow a reserved key. The two designs - * therefore offer equivalent guarantees through different mechanisms; the - * top-level `BuildUCPProfileInput.extras` slot below DOES carry a runtime - * reserved-key guard because it is a dedicated map and can be populated by - * spreading, where the per-element shapes cannot. + * UCP per-element shape note: each binding interface (`UCPServiceBinding`, + * `UCPCapabilityBinding`, `UCPPaymentHandlerBinding`) carries the canonical UCP fields + * plus arbitrary vendor extras flat on the same object via `[k: string]: unknown`. The + * python sibling models these as dataclasses with an explicit `extras: dict` field. Both + * designs offer equivalent guarantees through different mechanisms. */ export interface UCPSigningKey { /** JWK kid (key id). */ kid: string; - /** JWK kty (key type), typically `EC`, `RSA`, or `OKP`. */ + /** JWK kty (key type) — `EC`, `RSA`, or `OKP`. */ kty: string; - /** JWK alg (signing algorithm), typically `ES256`, `RS256`, or `EdDSA`. */ + /** JWK alg (signing algorithm) — `ES256`, `RS256`, or `EdDSA`. */ alg?: string; /** JWK use, typically `sig`. */ use?: string; @@ -57,18 +45,10 @@ export interface UCPSigningKey { } /** - * Construct a UCPSigningKey from a public JWK dict (e.g. the `publicJWK` - * returned by `generateUCPSigningKey()`). Validates the JWK has required - * fields (`kid`, `kty`) and rejects symmetric (`oct`) keys, which can't - * publicly verify a JWS in trust-mode UCP. - * - * Symmetric to Python's `UCPSigningKey.from_jwk(public_jwk)` classmethod. - * - * Example: - * ```ts - * const { publicJWK } = await generateUCPSigningKey({ kid: 'merchant-2026-05' }); - * const profile = buildUCPProfile({ ..., signing_keys: [ucpSigningKeyFromJWK(publicJWK)] }); - * ``` + * Construct a UCPSigningKey from a public JWK dict (e.g. the `publicJWK` returned by + * `generateUCPSigningKey()`). Validates required fields and rejects symmetric keys that + * can't publicly verify a JWS in trust-mode UCP. Symmetric to Python's + * `UCPSigningKey.from_jwk(public_jwk)` classmethod. */ export function ucpSigningKeyFromJWK(jwk: Record): UCPSigningKey { if (!jwk || typeof jwk !== 'object') { @@ -88,117 +68,215 @@ export function ucpSigningKeyFromJWK(jwk: Record): UCPSigningKe return jwk as unknown as UCPSigningKey; } -export interface UCPService { - /** Transport binding — `rest` / `mcp` / `a2a` / `embedded`. */ - type: string; - /** Service URL (or path for embedded). */ - url?: string; - /** Optional version pin. */ - version?: string; - /** Vendor-specific extras for the binding. */ +/** Transport binding — keyed under a service name (e.g., `dev.ucp.shopping`). */ +export interface UCPServiceBinding { + /** Spec version, YYYY-MM-DD per UCP convention. REQUIRED. */ + version: string; + /** URL to human-readable specification. REQUIRED. */ + spec: string; + /** Transport — `rest` / `mcp` / `a2a` / `embedded`. REQUIRED. */ + transport: 'rest' | 'mcp' | 'a2a' | 'embedded'; + /** Endpoint URL — required for rest/mcp; A2A points at the agent-card.json URL. */ + endpoint?: string; + /** URL to JSON Schema — required for rest/mcp/embedded per spec. */ + schema?: string; + /** Optional id for entity-instance disambiguation. */ + id?: string; + /** Entity-specific config. */ + config?: Record; + /** Vendor-specific extras. */ [k: string]: unknown; } -export interface UCPCapability { - /** Capability name — `checkout`, `catalog`, `sh.agentscore.identity`, etc. */ - name: string; - /** URL of the JSON Schema describing this capability's payload. */ - schema?: string; - /** Capability version — semver or date-stamp per UCP convention. */ - version?: string; - /** Vendor-specific extras for the capability. */ +/** Capability binding — keyed under a capability name (e.g., `dev.ucp.shopping.checkout`). */ +export interface UCPCapabilityBinding { + /** Capability version, YYYY-MM-DD. REQUIRED. */ + version: string; + /** URL to human-readable specification. REQUIRED. */ + spec: string; + /** URL to JSON Schema. REQUIRED. */ + schema: string; + /** Optional id for entity-instance disambiguation. */ + id?: string; + /** Entity-specific config (feature flags, callback URLs, etc). */ + config?: Record; + /** Parent capability(ies) extended — single string or array for multi-parent. */ + extends?: string | string[]; + /** Optional version requirements per UCP §6.5. */ + requires?: { + protocol?: { min: string; max?: string }; + capabilities?: Record; + }; + /** Vendor-specific extras (e.g., AgentScore claims block on `sh.agentscore.identity`). */ [k: string]: unknown; } -export interface UCPPaymentHandler { - /** Handler name — `stripe`, `tempo`, `x402-base`, `solana`, etc. */ - name: string; - /** Handler config — recipient address, profile id, etc. */ +/** Payment handler binding — keyed under a handler reverse-DNS name (e.g., `com.google.pay`). */ +export interface UCPPaymentHandlerBinding { + /** Handler instance id (short, human-readable, e.g., `gpay`, `tempo`, `x402`). REQUIRED. */ + id: string; + /** Handler spec version, YYYY-MM-DD. REQUIRED. */ + version: string; + /** URL to handler spec. REQUIRED. */ + spec: string; + /** URL to handler config schema. REQUIRED. */ + schema: string; + /** Available instruments — type + per-type constraints (cards, wallets, etc.). */ + available_instruments?: Array<{ type: string; constraints?: Record; [k: string]: unknown }>; + /** Handler config — gateway IDs, merchant IDs, public keys, etc. */ config?: Record; + /** Vendor-specific extras. */ + [k: string]: unknown; } -export interface UCPProfile { - /** UCP spec version (date-stamped). */ +/** UCP body — nested under the `ucp` key of the published profile. */ +export interface UCPProfileBody { + /** UCP spec version (YYYY-MM-DD). */ version: string; - /** URL of the UCP spec. */ - spec: string; - /** URL of this profile's JSON schema. */ - schema?: string; - /** Display name of the merchant / agent surface. */ + /** Display name for the merchant / agent surface. */ name?: string; - /** Service bindings — REST, MCP, A2A, embedded transports. */ - services: UCPService[]; - /** Capabilities offered (with schema URLs). */ - capabilities: UCPCapability[]; - /** Payment handlers offered — typically the rails the merchant accepts. */ - payment_handlers: UCPPaymentHandler[]; - /** JWKS — REQUIRED by spec. The merchant signs requests with a private key whose - * public counterpart is listed here. Verifiers fetch this profile, find the kid, and - * validate signatures. */ + /** Services — keyed by service name (e.g., `dev.ucp.shopping`). Each value is an + * array of transport bindings (one merchant typically advertises multiple transports + * under one service name). */ + services: Record; + /** Capabilities — keyed by capability name (e.g., `dev.ucp.shopping.checkout`). */ + capabilities: Record; + /** Payment handlers — keyed by handler reverse-DNS name (e.g., `com.google.pay`). */ + payment_handlers: Record; + /** Optional `supported_versions` map linking historical version-specific profile URLs. + * Pattern: `{ "2026-01-23": "https://merchant/.well-known/ucp/2026-01-23", ... }`. */ + supported_versions?: Record; + /** Vendor-specific extras inside the `ucp` envelope. */ + [k: string]: unknown; +} + +/** Full UCP profile body as published at `/.well-known/ucp`. Top-level shape: + * `{ ucp: {...}, signing_keys: [...], signature?: "..." }`. */ +export interface UCPProfile { + /** UCP body. ALL UCP-spec fields nest here per spec. */ + ucp: UCPProfileBody; + /** JWKS — public keys at the OUTER level per UCP spec. Verifiers fetch this profile, + * match the kid from a JWS / RFC 9421 signature header against this list, and validate. */ signing_keys: UCPSigningKey[]; - /** Vendor-specific extras at the top level. */ + /** Set when JWS-signed via `signUCPProfile` — JWS Compact Serialization with detached + * payload (header..signature; payload is the canonicalized body minus this field). */ + signature?: string; + /** Top-level vendor-specific extras (outside the `ucp` envelope). */ [k: string]: unknown; } export interface BuildUCPProfileInput { - /** UCP spec version. Default `"2026-04-17"` (current at time of writing). */ + /** UCP spec version. Default `'2026-04-17'`. */ version?: string; /** Display name for the merchant / agent surface. */ name?: string; - /** Service transport bindings. At minimum, the agent's primary REST endpoint. */ - services: UCPService[]; - /** Capabilities offered. AgentScore identity (vendor-namespaced as `sh.agentscore.identity`) is auto-added as a capability when `data` is provided. */ - capabilities?: UCPCapability[]; - /** Payment handlers — rails the merchant accepts. */ - payment_handlers?: UCPPaymentHandler[]; - /** JWKS — public keys the merchant signs requests with. REQUIRED by spec. */ + /** Services map, keyed by service name. UCP-shopping merchants typically advertise + * bindings under `'dev.ucp.shopping'`. */ + services?: Record; + /** Capabilities map, keyed by capability name. The `sh.agentscore.identity` capability + * is auto-added when `data` is provided. */ + capabilities?: Record; + /** Payment handlers map, keyed by handler reverse-DNS name. */ + payment_handlers?: Record; + /** JWKS — public keys the merchant signs with. REQUIRED by spec. */ signing_keys: UCPSigningKey[]; - /** AgentScore assess data — adds an `sh.agentscore.identity` capability + claims block when present. */ + /** AgentScore assess data — adds an `sh.agentscore.identity` capability + claims + * block when present. */ data?: AgentScoreData | null; - /** Optional override for the AgentScore capability schema URL. Field is - * snake_cased for cross-language parity with `agentscore_commerce`'s - * `build_ucp_profile(agentscore_schema_url=...)`. */ + /** Optional override for the AgentScore capability schema URL. Field is snake_cased + * for cross-language parity with the Python sibling. */ agentscore_schema_url?: string; - /** Vendor-specific extras at the top level. */ + /** Optional override for the AgentScore capability spec URL. */ + agentscore_spec_url?: string; + /** `supported_versions` map at the profile root. Pattern matches Pura Vida's + * production profile (`{ "": "/.well-known/ucp/" }`). */ + supported_versions?: Record; + /** Vendor-specific extras at the OUTER level (alongside `ucp` + `signing_keys`). */ extras?: Record; + /** Vendor-specific extras INSIDE the `ucp` envelope (alongside `version`, `services`, etc.). */ + ucp_extras?: Record; } const DEFAULT_VERSION = '2026-04-17'; -const SPEC_URL = 'https://ucp.dev/'; // Reverse-DNS namespacing per UCP convention (`^[a-z][a-z0-9]*(?:\.[a-z][a-z0-9_]*)+$`). -// The bare `agentscore-identity` form fails the spec regex; vendor-namespacing under the -// `sh.agentscore` authority is honest about the capability being our extension, not a -// UCP-canonical slot. +// The bare `agentscore-identity` form fails the spec regex; vendor-namespacing under +// `sh.agentscore` is honest about the capability being our extension, not UCP-canonical. const AGENTSCORE_CAPABILITY_NAME = 'sh.agentscore.identity'; const AGENTSCORE_CAPABILITY_VERSION = '1'; +const AGENTSCORE_DEFAULT_SPEC_URL = 'https://agentscore.sh/specification/identity'; +const AGENTSCORE_DEFAULT_SCHEMA_URL = 'https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json'; +// Multi-parent extension — `sh.agentscore.identity` carries claims relevant at both +// checkout-build (compliance gate) and cart-build (price-gate eligibility, jurisdiction- +// restricted items in cart) time. Mirrors the multi-parent convention in the live +// ecosystem (Shopify's `dev.shopify.catalog.storefront` extends both `catalog.search` +// and `catalog.lookup`; UCP-canonical `dev.ucp.shopping.discount` extends both checkout +// and cart). +const AGENTSCORE_EXTENDS = ['dev.ucp.shopping.checkout', 'dev.ucp.shopping.cart']; + +const RESERVED_TOP_LEVEL = new Set([ + 'ucp', + 'signing_keys', + 'signature', + '__proto__', + 'constructor', + 'prototype', +]); +const RESERVED_UCP_FIELDS = new Set([ + 'version', + 'name', + 'services', + 'capabilities', + 'payment_handlers', + 'supported_versions', + '__proto__', + 'constructor', + 'prototype', +]); /** - * Compose a UCP profile body for `/.well-known/ucp` publication. Merges AgentScore - * identity claims into the `capabilities` array as an `sh.agentscore.identity` - * capability so UCP-aware consumers can discover verified-buyer claims alongside the - * standard UCP transport metadata. + * Compose a UCP profile body for `/.well-known/ucp` publication. Returns the spec- + * compliant shape: `{ ucp: { version, services, capabilities, payment_handlers, ... }, + * signing_keys: [...] }`. Pass through `signUCPProfile` to attach a JWS signature for + * trust-mode verifiers. + * + * Auto-injects `sh.agentscore.identity` as a vendor capability extending both + * `dev.ucp.shopping.checkout` and `dev.ucp.shopping.cart` when `data` carries a + * resolved operator. Verifiers that recognize the AgentScore namespace can parse + * the `claims` block; vanilla UCP agents see a normal extension capability. * * Example: * ```ts * import { buildUCPProfile } from '@agent-score/commerce'; * - * app.get('/.well-known/ucp', async (c) => { - * const data = getAgentScoreData(c); - * return c.json(buildUCPProfile({ - * name: 'Example Merchant', - * services: [{ type: 'rest', url: 'https://agents.example.com' }], - * payment_handlers: [ - * { name: 'tempo', config: { recipient: TEMPO_ADDR } }, - * { name: 'stripe', config: { profile_id: STRIPE_PROFILE_ID } }, + * const profile = buildUCPProfile({ + * name: 'Example Merchant', + * services: { + * 'dev.ucp.shopping': [ + * { version: '2026-04-08', spec: 'https://ucp.dev/2026-04-08/specification/overview', + * transport: 'mcp', endpoint: 'https://merchant.example/api/ucp/mcp', + * schema: 'https://ucp.dev/services/shopping/openrpc.json' }, * ], - * signing_keys: [{ kid: 'merchant-2026-04', kty: 'EC', alg: 'ES256', crv: 'P-256', x: '...', y: '...' }], - * data, - * })); + * }, + * payment_handlers: { + * 'sh.agentscore.payment.tempo': [{ + * id: 'tempo', + * version: '2026-04-08', + * spec: 'https://agentscore.sh/specification/payment-handlers/tempo', + * schema: 'https://agentscore.sh/schemas/payment-handlers/tempo.json', + * config: { recipient: TEMPO_ADDR }, + * }], + * }, + * signing_keys: [signingKey], * }); * ``` */ export function buildUCPProfile(input: BuildUCPProfileInput): UCPProfile { - const baseCapabilities: UCPCapability[] = [...(input.capabilities ?? [])]; + // Deep-clone the capabilities map so we can safely mutate (auto-add the AgentScore + // identity capability) without altering the caller's input. + const capabilities: Record = {}; + for (const [name, bindings] of Object.entries(input.capabilities ?? {})) { + capabilities[name] = [...bindings]; + } if (input.data) { const operatorId = input.data.resolved_operator; @@ -207,8 +285,8 @@ export function buildUCPProfile(input: BuildUCPProfileInput): UCPProfile { const accountVerification = input.data.account_verification; // `||` (not `??`) coerces both null/undefined AND empty string to the default, // matching the python sibling. The API can return `account_verification` with - // either null or `""` for un-set fields depending on the row state, and a - // profile signed in one language must verify in the other across both shapes. + // either null or `""` for un-set fields; profiles signed in one language must + // verify in the other across both shapes. const claims: Record = { operator_id: operatorId, kyc_level: accountVerification?.kyc_level || operatorVerification?.level || 'none', @@ -219,45 +297,47 @@ export function buildUCPProfile(input: BuildUCPProfileInput): UCPProfile { verify_url: input.data.verify_url ?? null, issuer: 'https://agentscore.sh', }; - baseCapabilities.push({ - name: AGENTSCORE_CAPABILITY_NAME, + const agentscoreBinding: UCPCapabilityBinding = { version: AGENTSCORE_CAPABILITY_VERSION, - schema: input.agentscore_schema_url ?? 'https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json', + spec: input.agentscore_spec_url ?? AGENTSCORE_DEFAULT_SPEC_URL, + schema: input.agentscore_schema_url ?? AGENTSCORE_DEFAULT_SCHEMA_URL, + extends: AGENTSCORE_EXTENDS, + // `claims` is our vendor extra on the binding; allowed per spec via the + // `[k: string]: unknown` index signature on UCPCapabilityBinding. claims, - }); + }; + const existing = capabilities[AGENTSCORE_CAPABILITY_NAME]; + if (existing) existing.push(agentscoreBinding); + else capabilities[AGENTSCORE_CAPABILITY_NAME] = [agentscoreBinding]; } } - const profile: UCPProfile = { + const ucp: UCPProfileBody = { version: input.version ?? DEFAULT_VERSION, - spec: SPEC_URL, - services: input.services, - capabilities: baseCapabilities, - payment_handlers: input.payment_handlers ?? [], - signing_keys: input.signing_keys, + services: input.services ?? {}, + capabilities, + payment_handlers: input.payment_handlers ?? {}, }; + if (input.name !== undefined) ucp.name = input.name; + if (input.supported_versions !== undefined) ucp.supported_versions = input.supported_versions; + if (input.ucp_extras) { + for (const k of Object.keys(input.ucp_extras)) { + if (RESERVED_UCP_FIELDS.has(k)) { + throw new Error(`buildUCPProfile: ucp_extras key "${k}" collides with a reserved \`ucp\` field; rejected.`); + } + } + Object.assign(ucp, input.ucp_extras); + } - if (input.name !== undefined) profile.name = input.name; + const profile: UCPProfile = { + ucp, + signing_keys: input.signing_keys, + }; if (input.extras) { - // Reserved-field collisions are rejected so a careless `extras: { signing_keys: [...] }` - // can't silently destroy the explicit field. `__proto__`, `constructor`, and `prototype` - // are reserved so vendor extras can't slip prototype-pollution payloads into the canonical - // body and surprise downstream consumers. - const RESERVED = new Set([ - 'version', - 'spec', - 'services', - 'capabilities', - 'payment_handlers', - 'signing_keys', - 'name', - 'signature', - '__proto__', - 'constructor', - 'prototype', - ]); + // `__proto__`, `constructor`, `prototype` reserved so vendor extras can't slip + // prototype-pollution payloads into the canonical body. for (const k of Object.keys(input.extras)) { - if (RESERVED.has(k)) { + if (RESERVED_TOP_LEVEL.has(k)) { throw new Error(`buildUCPProfile: extras key "${k}" collides with a reserved profile field; rejected.`); } } diff --git a/src/index.ts b/src/index.ts index 084ac95..d40ac5d 100644 --- a/src/index.ts +++ b/src/index.ts @@ -34,10 +34,11 @@ export { AGENTSCORE_UCP_CAPABILITY, buildUCPProfile, type BuildUCPProfileInput, - type UCPCapability, - type UCPPaymentHandler, + type UCPCapabilityBinding, + type UCPPaymentHandlerBinding, type UCPProfile, - type UCPService, + type UCPProfileBody, + type UCPServiceBinding, type UCPSigningKey, ucpSigningKeyFromJWK, } from './identity/ucp'; diff --git a/tests/identity/ucp.test.ts b/tests/identity/ucp.test.ts index a647912..e6bdc9a 100644 --- a/tests/identity/ucp.test.ts +++ b/tests/identity/ucp.test.ts @@ -1,5 +1,11 @@ import { describe, expect, it } from 'vitest'; -import { AGENTSCORE_UCP_CAPABILITY, buildUCPProfile } from '../../src/identity/ucp'; +import { + AGENTSCORE_UCP_CAPABILITY, + buildUCPProfile, + type UCPCapabilityBinding, + type UCPPaymentHandlerBinding, + type UCPServiceBinding, +} from '../../src/identity/ucp'; import type { AgentScoreData } from '../../src/core'; const fullData: AgentScoreData = { @@ -16,29 +22,48 @@ const fullData: AgentScoreData = { }, }; +const sampleServiceBinding: UCPServiceBinding = { + version: '2026-04-08', + spec: 'https://ucp.dev/2026-04-08/specification/overview', + transport: 'mcp', + endpoint: 'https://agents.example/api/ucp/mcp', + schema: 'https://ucp.dev/services/shopping/openrpc.json', +}; + const baseInput = { - services: [{ type: 'rest', url: 'https://agents.example' }], + services: { 'dev.ucp.shopping': [sampleServiceBinding] }, signing_keys: [{ kid: 'me-2026', kty: 'EC', alg: 'ES256', crv: 'P-256', x: 'x', y: 'y' }], }; -describe('buildUCPProfile', () => { - it('emits a base profile with required fields when no AgentScore data', () => { +const agentscoreCap = (profile: ReturnType): UCPCapabilityBinding | undefined => { + return profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY]?.[0]; +}; + +describe('buildUCPProfile (spec-compliant shape)', () => { + it('emits the spec envelope with `ucp` body + outer `signing_keys`', () => { const profile = buildUCPProfile(baseInput); - expect(profile.spec).toBe('https://ucp.dev/'); - expect(profile.version).toMatch(/^\d{4}-\d{2}-\d{2}$/); - expect(profile.services).toEqual(baseInput.services); + expect(profile.ucp).toBeDefined(); expect(profile.signing_keys).toEqual(baseInput.signing_keys); - expect(profile.capabilities).toEqual([]); - expect(profile.payment_handlers).toEqual([]); + expect(profile.ucp.version).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(profile.ucp.services).toEqual(baseInput.services); + expect(profile.ucp.capabilities).toEqual({}); + expect(profile.ucp.payment_handlers).toEqual({}); + // No top-level `spec` field per UCP spec — spec lives per-binding. + expect((profile as Record).spec).toBeUndefined(); + // No `version` at top level either; lives under `ucp`. + expect((profile as Record).version).toBeUndefined(); }); it('appends sh.agentscore.identity capability when data carries a resolved operator', () => { const profile = buildUCPProfile({ ...baseInput, data: fullData }); - const cap = profile.capabilities.find((c) => c.name === AGENTSCORE_UCP_CAPABILITY); + const cap = agentscoreCap(profile); expect(cap).toBeDefined(); expect(cap?.version).toBe('1'); - expect(cap?.name).toBe('sh.agentscore.identity'); + expect(cap?.spec).toContain('agentscore.sh'); expect(cap?.schema).toContain('sh-agentscore-identity-v1.json'); + // Multi-parent extends — matches Shopify's dev.shopify.catalog.storefront pattern + // and UCP-canonical dev.ucp.shopping.discount (extends [checkout, cart]). + expect(cap?.extends).toEqual(['dev.ucp.shopping.checkout', 'dev.ucp.shopping.cart']); const claims = (cap as Record).claims as Record; expect(claims.operator_id).toBe('op_abc'); expect(claims.kyc_level).toBe('enhanced'); @@ -54,87 +79,125 @@ describe('buildUCPProfile', () => { ...baseInput, data: { decision: null, decision_reasons: [] }, }); - expect(profile.capabilities.find((c) => c.name === AGENTSCORE_UCP_CAPABILITY)).toBeUndefined(); + expect(agentscoreCap(profile)).toBeUndefined(); + expect(AGENTSCORE_UCP_CAPABILITY in profile.ucp.capabilities).toBe(false); }); - it('preserves caller-supplied capabilities and appends agentscore at end', () => { + it('preserves caller-supplied capabilities and merges agentscore in alongside', () => { + const checkoutBinding: UCPCapabilityBinding = { + version: '2026-04-08', + spec: 'https://ucp.dev/2026-04-08/specification/checkout', + schema: 'https://ucp.dev/2026-04-08/schemas/shopping/checkout.json', + }; const profile = buildUCPProfile({ ...baseInput, - capabilities: [{ name: 'checkout', version: '2' }], + capabilities: { 'dev.ucp.shopping.checkout': [checkoutBinding] }, data: fullData, }); - expect(profile.capabilities[0]?.name).toBe('checkout'); - expect(profile.capabilities[1]?.name).toBe(AGENTSCORE_UCP_CAPABILITY); + expect(profile.ucp.capabilities['dev.ucp.shopping.checkout']?.[0]?.version).toBe('2026-04-08'); + expect(agentscoreCap(profile)?.version).toBe('1'); }); - it('passes through name + payment_handlers + extras', () => { + it('passes through name + payment_handlers + extras + ucp_extras', () => { + const tempoHandler: UCPPaymentHandlerBinding = { + id: 'tempo', + version: '2026-04-08', + spec: 'https://agentscore.sh/specification/payment-handlers/tempo', + schema: 'https://agentscore.sh/schemas/payment-handlers/tempo.json', + config: { recipient: '0xtempo' }, + }; const profile = buildUCPProfile({ ...baseInput, name: 'Example Merchant', - payment_handlers: [ - { name: 'tempo', config: { recipient: '0xtempo' } }, - { name: 'stripe', config: { profile_id: 'prof_x' } }, - ], - extras: { custom_field: 'custom_value' }, + payment_handlers: { 'sh.agentscore.payment.tempo': [tempoHandler] }, + extras: { custom_top_level: 'top_value' }, + ucp_extras: { custom_ucp_field: 'ucp_value' }, }); - expect(profile.name).toBe('Example Merchant'); - expect(profile.payment_handlers).toHaveLength(2); - expect((profile as Record).custom_field).toBe('custom_value'); + expect(profile.ucp.name).toBe('Example Merchant'); + expect(profile.ucp.payment_handlers['sh.agentscore.payment.tempo']?.[0]?.id).toBe('tempo'); + expect((profile as Record).custom_top_level).toBe('top_value'); + expect((profile.ucp as Record).custom_ucp_field).toBe('ucp_value'); }); - // payment_handler.config is an optional TypeScript property: when the caller - // omits it the wire profile ships without the `config` key. Python's - // `UCPPaymentHandler.to_dict` omits empty configs to match this convention, - // so the same logical input produces the same canonical bytes across SDKs. - it('payment_handler omits config key when caller does not set it (cross-lang parity)', () => { + it('payment_handler binding omits config when caller does not set it (cross-lang parity)', () => { + const tempoHandlerNoConfig: UCPPaymentHandlerBinding = { + id: 'tempo', + version: '2026-04-08', + spec: 'https://agentscore.sh/specification/payment-handlers/tempo', + schema: 'https://agentscore.sh/schemas/payment-handlers/tempo.json', + }; const profile = buildUCPProfile({ ...baseInput, - payment_handlers: [{ name: 'tempo' }], + payment_handlers: { 'sh.agentscore.payment.tempo': [tempoHandlerNoConfig] }, }); - expect(profile.payment_handlers).toEqual([{ name: 'tempo' }]); - expect('config' in (profile.payment_handlers[0] as object)).toBe(false); + const handler = profile.ucp.payment_handlers['sh.agentscore.payment.tempo']?.[0]; + expect(handler).toBeDefined(); + expect('config' in (handler as object)).toBe(false); }); - it('respects custom version override', () => { + it('respects custom version override under ucp.version', () => { const profile = buildUCPProfile({ ...baseInput, version: '2026-12-31' }); - expect(profile.version).toBe('2026-12-31'); + expect(profile.ucp.version).toBe('2026-12-31'); }); - it('respects agentscore_schema_url override', () => { + it('respects agentscore_schema_url override on the auto-injected capability', () => { const profile = buildUCPProfile({ ...baseInput, data: fullData, agentscore_schema_url: 'https://custom.example/schema.json', }); - const cap = profile.capabilities.find((c) => c.name === AGENTSCORE_UCP_CAPABILITY); - expect(cap?.schema).toBe('https://custom.example/schema.json'); + expect(agentscoreCap(profile)?.schema).toBe('https://custom.example/schema.json'); + }); + + it('respects agentscore_spec_url override on the auto-injected capability', () => { + const profile = buildUCPProfile({ + ...baseInput, + data: fullData, + agentscore_spec_url: 'https://custom.example/spec', + }); + expect(agentscoreCap(profile)?.spec).toBe('https://custom.example/spec'); }); + it('emits supported_versions map under ucp body when supplied', () => { + const profile = buildUCPProfile({ + ...baseInput, + supported_versions: { + '2026-04-08': 'https://merchant.example/.well-known/ucp/2026-04-08', + '2026-01-23': 'https://merchant.example/.well-known/ucp/2026-01-23', + }, + }); + expect(profile.ucp.supported_versions?.['2026-04-08']).toContain('/2026-04-08'); + }); + + it.each([['ucp'], ['signing_keys'], ['signature'], ['__proto__'], ['constructor'], ['prototype']])( + 'rejects extras key "%s" as a reserved top-level collision', + (k) => { + expect(() => buildUCPProfile({ ...baseInput, extras: { [k]: 'attacker' } })).toThrow( + /collides with a reserved profile field/, + ); + }, + ); + it.each([ ['version'], - ['spec'], + ['name'], ['services'], ['capabilities'], ['payment_handlers'], - ['signing_keys'], - ['name'], - ['signature'], + ['supported_versions'], ['__proto__'], ['constructor'], ['prototype'], - ])('rejects extras key "%s" as a reserved-field collision', (k) => { - expect(() => - buildUCPProfile({ - ...baseInput, - extras: { [k]: 'attacker' }, - }), - ).toThrow(/collides with a reserved profile field/); + ])('rejects ucp_extras key "%s" as a reserved ucp-field collision', (k) => { + expect(() => buildUCPProfile({ ...baseInput, ucp_extras: { [k]: 'attacker' } })).toThrow( + /collides with a reserved `ucp` field/, + ); }); - // Empty-string and null normalization: the API can emit - // `account_verification` with either null or `""` for un-set fields, and the - // node + python siblings must produce the SAME canonical claims block for - // either shape so a profile signed in one language verifies in the other. + // Empty-string and null normalization: the API can emit `account_verification` with + // either null or `""` for un-set fields, and the node + python siblings must produce + // the SAME canonical claims block for either shape so a profile signed in one + // language verifies in the other. describe('account_verification missing-value normalization (cross-lang parity)', () => { const baseDataWithOp = { decision: 'allow', @@ -142,16 +205,12 @@ describe('buildUCPProfile', () => { resolved_operator: 'op_abc', }; - const claimsOf = (av: AgentScoreData['account_verification']) => { + const claimsOf = (av: AgentScoreData['account_verification']): Record => { const profile = buildUCPProfile({ ...baseInput, data: { ...baseDataWithOp, account_verification: av } as AgentScoreData, }); - const cap = profile.capabilities.find((c) => c.name === AGENTSCORE_UCP_CAPABILITY) as Record< - string, - unknown - >; - return cap.claims as Record; + return (agentscoreCap(profile) as Record).claims as Record; }; it('coerces empty-string kyc_level to "none"', () => { From 1b927c8305b1155067777a798b3d3f69ce80503a Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 06:16:00 -0700 Subject: [PATCH 29/35] docs: update README + signed-ucp-merchant example for new spec-compliant shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- README.md | 27 +++++++++++++++++++++++++-- examples/signed-ucp-merchant.ts | 22 ++++++++++++++++++++-- 2 files changed, 45 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index a954cf3..2a64a4e 100644 --- a/README.md +++ b/README.md @@ -196,10 +196,33 @@ import { buildA2AAgentCard, buildUCPProfile } from "@agent-score/commerce"; const card = buildA2AAgentCard({ name, url, capabilities, data: assess }); // Google Universal Commerce Protocol; publish at /.well-known/ucp -const profile = buildUCPProfile({ name, services, payment_handlers, signing_keys, data: assess }); +// Output shape: { ucp: { version, services, capabilities, payment_handlers, +// name?, supported_versions? }, signing_keys: [...], signature?: "..." } +// — services / capabilities / payment_handlers are MAPS keyed by reverse-DNS +// service / capability / handler name. Verified against the live Pura Vida +// reference at puravidabracelets.com/.well-known/ucp. +const profile = buildUCPProfile({ + name, + services: { + 'dev.ucp.shopping': [ + { version: '2026-04-08', spec: 'https://ucp.dev/2026-04-08/specification/overview', + transport: 'mcp', endpoint: 'https://merchant.example/api/ucp/mcp', + schema: 'https://ucp.dev/services/shopping/openrpc.json' }, + ], + }, + payment_handlers: { + 'sh.agentscore.payment.tempo': [{ + id: 'tempo', version: '2026-04-08', + spec: 'https://agentscore.sh/specification/payment-handlers/tempo', + schema: 'https://agentscore.sh/schemas/payment-handlers/tempo.json', + config: { recipient: TEMPO_ADDR }, + }], + }, + signing_keys, data: assess, +}); ``` -UCP §6 trust-mode requires profiles to carry a JWS signature backed by a JWKS at `/.well-known/jwks.json`. Sign + verify via the optional `jose` peer dep (tested against jose v5.x; pin `jose@^5`): +UCP §6 doesn't mandate profile-body JWS signing — Pura Vida and other Shopify-backed UCP merchants ship unsigned. AgentScore's `agentscore-profile+jws` is a vendor extension for trust-mode verifiers (Visa AP2 pilots, regulated-commerce verifiers) that opt into auditable profiles. Sign + verify via the optional `jose` peer dep (tested against jose v5.x; pin `jose@^5`): ```typescript import { buildJWKSResponse, generateUCPSigningKey, signUCPProfile, verifyUCPProfile, UCPVerificationError } from "@agent-score/commerce"; diff --git a/examples/signed-ucp-merchant.ts b/examples/signed-ucp-merchant.ts index d67d9d9..1fcb47d 100644 --- a/examples/signed-ucp-merchant.ts +++ b/examples/signed-ucp-merchant.ts @@ -78,8 +78,26 @@ app.get('/.well-known/ucp', async (c) => { const key = await loadSigningKey(); const profile = buildUCPProfile({ name: 'My Agent Service', - services: [{ type: 'rest', url: 'https://agents.example.com' }], - payment_handlers: [{ name: 'tempo', config: { recipient: '0xfeedface' } }], + services: { + 'dev.ucp.shopping': [ + { + version: '2026-04-08', + spec: 'https://ucp.dev/2026-04-08/specification/overview', + transport: 'mcp', + endpoint: 'https://agents.example.com/api/ucp/mcp', + schema: 'https://ucp.dev/services/shopping/openrpc.json', + }, + ], + }, + payment_handlers: { + 'sh.agentscore.payment.tempo': [{ + id: 'tempo', + version: '2026-04-08', + spec: 'https://agentscore.sh/specification/payment-handlers/tempo', + schema: 'https://agentscore.sh/schemas/payment-handlers/tempo.json', + config: { recipient: '0xfeedface' }, + }], + }, signing_keys: [ucpSigningKeyFromJWK(key.publicJWK as Record)], }); const signed = await signUCPProfile(profile, { From fcbace18afce84fe6d98859d763d01bae77a05d8 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 06:25:01 -0700 Subject: [PATCH 30/35] fix(identity): regenerate cross-lang fixture corpus for spec-compliant shape MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- examples/README.md | 2 +- examples/signed-ucp-merchant.ts | 13 +- scripts/regenerate-cross-lang-fixtures.ts | 185 ++++++++++++------ .../fixtures/cross-lang/node-capability.json | 71 ++++--- .../cross-lang/node-data-driven-claims.json | 72 ++++--- .../fixtures/cross-lang/node-emoji-keys.json | 50 +++-- .../fixtures/cross-lang/node-es256-rails.json | 86 ++++---- .../fixtures/cross-lang/node-extras-int.json | 55 +++--- .../cross-lang/node-int-boundary.json | 34 ++-- tests/fixtures/cross-lang/node-minimal.json | 34 ++-- tests/fixtures/cross-lang/node-multikey.json | 57 +++--- .../cross-lang/node-typed-claims.json | 72 ++++--- tests/fixtures/cross-lang/node-unicode.json | 53 +++-- tests/fixtures/cross-lang/py-capability.json | 71 ++++--- .../cross-lang/py-data-driven-claims.json | 72 ++++--- tests/fixtures/cross-lang/py-emoji-keys.json | 50 +++-- tests/fixtures/cross-lang/py-es256-rails.json | 86 ++++---- tests/fixtures/cross-lang/py-extras-int.json | 55 +++--- .../fixtures/cross-lang/py-int-boundary.json | 34 ++-- tests/fixtures/cross-lang/py-minimal.json | 34 ++-- tests/fixtures/cross-lang/py-multikey.json | 57 +++--- .../fixtures/cross-lang/py-typed-claims.json | 72 ++++--- tests/fixtures/cross-lang/py-unicode.json | 53 +++-- 23 files changed, 829 insertions(+), 539 deletions(-) diff --git a/examples/README.md b/examples/README.md index 81c296c..6a8964c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -11,7 +11,7 @@ Runnable, copy-pasteable example integrations covering the most common merchant | [`variable-cost-merchant.ts`](./variable-cost-merchant.ts) | Pay-per-actual-usage (LLM, transcode, etc.) | Same use case on **two protocols**: x402 upto (Permit2 authorize-max → Settlement-Overrides settle-actual) AND MPP tempo session (channel + SSE + mid-stream vouchers). Vendor offers both so agents pick whichever their wallet supports. | | [`compliance-merchant.ts`](./compliance-merchant.ts) | Regulated-goods merchant (wine, cannabis, etc.) | Full compliance gate (KYC + sanctions + age + jurisdiction) + custom `onDenied` composing commerce helpers: `verificationAgentInstructions`, `isFixableDenial`, `buildContactSupportNextSteps`, `denialReasonToBody`/`denialReasonStatus`, `buildSignerMismatchBody`. Shows how vendors write only the business-specific branches and let commerce handle the rest. | | [`per-product-policy-merchant.ts`](./per-product-policy-merchant.ts) | Multi-product merchant with mixed compliance needs | One product hard-gates KYC + 21 + US-state allowlist (wine), one is anonymous (merch, ships anywhere), a third uses `enforcement: 'soft'` to request KYC as a fraud signal but accept anonymous sales (stamps `identity_status: 'unverified'` on the order). Uses `PolicyBlock`, `policyToGateOptions`, `runGateWithEnforcement`, `shippingCountryAllowed`, `shippingStateAllowed`. | -| [`signed-ucp-merchant.ts`](./signed-ucp-merchant.ts) | Signed UCP profile + JWKS endpoint | Wires `/.well-known/ucp` (signed envelope) + `/.well-known/jwks.json` against a persistent signing key. UCP §6 trust-mode requires the JWS signature; this example shows ephemeral-for-dev / env-JWK-for-prod, key rotation, and `Cache-Control` posture on the JWKS endpoint. Uses `generateUCPSigningKey`, `signUCPProfile`, `buildJWKSResponse`, `ucpSigningKeyFromJWK`. | +| [`signed-ucp-merchant.ts`](./signed-ucp-merchant.ts) | Signed UCP profile + JWKS endpoint | Wires `/.well-known/ucp` (signed envelope) + `/.well-known/jwks.json` against a persistent signing key. 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 profiles; UCP §6 itself does NOT mandate signing — Pura Vida and other Shopify-backed UCP merchants ship unsigned. This example shows ephemeral-for-dev / env-JWK-for-prod, key rotation, and `Cache-Control` posture on the JWKS endpoint. Uses `generateUCPSigningKey`, `signUCPProfile`, `buildJWKSResponse`, `ucpSigningKeyFromJWK`. | ## How to use diff --git a/examples/signed-ucp-merchant.ts b/examples/signed-ucp-merchant.ts index 1fcb47d..ba4c2a1 100644 --- a/examples/signed-ucp-merchant.ts +++ b/examples/signed-ucp-merchant.ts @@ -1,10 +1,15 @@ /** * Signed UCP profile example — `/.well-known/ucp` + `/.well-known/jwks.json`. * - * UCP §6 trust-mode verification (Google AI Mode, Gemini commerce) requires the - * profile to carry a JWS signature and the merchant to publish a JWKS endpoint - * verifiers can fetch the public key from. This example wires both routes against - * a persistent signing key (env-loaded for prod, ephemeral for dev). + * AgentScore's `agentscore-profile+jws` is a vendor extension layered on top of + * the UCP profile for trust-mode verifiers (Visa AP2 pilots, regulated-commerce + * verifiers) that opt into auditable cryptographic provenance. UCP §6 itself + * does NOT mandate profile-body signing — Pura Vida and other Shopify-backed + * UCP merchants ship unsigned in production today, and live UCP-aware agents + * (Google AI Mode, Gemini commerce, Microsoft Copilot, Perplexity) accept + * unsigned profiles. This example wires both routes against a persistent + * signing key (env-loaded for prod, ephemeral for dev) for verifiers that DO + * opt into the signed envelope. * * Run: `bun examples/signed-ucp-merchant.ts` (port 3010). * diff --git a/scripts/regenerate-cross-lang-fixtures.ts b/scripts/regenerate-cross-lang-fixtures.ts index 044035a..4a9ca28 100644 --- a/scripts/regenerate-cross-lang-fixtures.ts +++ b/scripts/regenerate-cross-lang-fixtures.ts @@ -1,21 +1,30 @@ /** * Regenerate the full cross-lang fixture corpus (Node side). * - * Writes all `node-*.json` fixtures under `tests/fixtures/cross-lang/`. Used - * after a canonicalization-relevant change (typ rename, capability-name - * rename, schema-URL rename, key-sort tweak, etc.) where every JWS in the - * corpus needs to be re-signed. + * Writes all `node-*.json` fixtures under `tests/fixtures/cross-lang/`. Used after + * a canonicalization-relevant change (typ rename, capability-name rename, schema-URL + * rename, key-sort tweak, profile-shape change) where every JWS in the corpus needs + * to be re-signed. * - * Each scenario hand-crafts the profile body, signs with a fresh keypair, and - * writes the `{ profile, jwks, alg, kid, generator }` envelope. Cross-lang - * verify in `tests/identity/cross-lang.test.ts` (and the python sibling) - * pulls these in alongside the `py-*` fixtures generated by the python - * sibling. + * Each scenario hand-crafts the profile body using the spec-compliant input shape + * (`services` / `capabilities` / `payment_handlers` as MAPS keyed by reverse-DNS + * service / capability / handler name), signs with a fresh keypair, and writes the + * `{ profile, jwks, alg, kid, generator }` envelope. Cross-lang verify in + * `tests/identity/cross-lang.test.ts` (and the python sibling) pulls these in + * alongside the `py-*` fixtures generated by the python sibling. + * + * Run: `bun run scripts/regenerate-cross-lang-fixtures.ts` */ import { writeFileSync } from 'node:fs'; import { join } from 'node:path'; -import { buildUCPProfile, type UCPSigningKey } from '../src/identity/ucp'; +import { + buildUCPProfile, + type UCPCapabilityBinding, + type UCPPaymentHandlerBinding, + type UCPServiceBinding, + type UCPSigningKey, +} from '../src/identity/ucp'; import { buildJWKSResponse, generateUCPSigningKey, @@ -40,17 +49,68 @@ function writeFixture(name: string, env: FixtureEnvelope): void { console.warn(`wrote ${out}`); } +// Spec-compliant binding helpers — each scenario uses these (or variants) so the +// fixtures cover the full set of canonical UCP fields per binding type. +function shopServiceMcp(host: string): UCPServiceBinding { + return { + version: '2026-04-08', + spec: 'https://ucp.dev/2026-04-08/specification/overview', + transport: 'mcp', + endpoint: `${host}/api/ucp/mcp`, + schema: 'https://ucp.dev/services/shopping/openrpc.json', + }; +} + +function shopServiceA2A(host: string): UCPServiceBinding { + return { + version: '2026-04-08', + spec: 'https://ucp.dev/2026-04-08/specification/overview', + transport: 'a2a', + endpoint: `${host}/.well-known/agent-card.json`, + }; +} + +function tempoHandler(config?: Record): UCPPaymentHandlerBinding { + const out: UCPPaymentHandlerBinding = { + id: 'tempo', + version: '2026-04-08', + spec: 'https://agentscore.sh/specification/payment-handlers/tempo', + schema: 'https://agentscore.sh/schemas/payment-handlers/tempo.json', + }; + if (config) out.config = config; + return out; +} + +function x402Handler(networks: string[]): UCPPaymentHandlerBinding { + return { + id: 'x402', + version: '2026-04-08', + spec: 'https://agentscore.sh/specification/payment-handlers/x402', + schema: 'https://agentscore.sh/schemas/payment-handlers/x402.json', + config: { networks }, + }; +} + +function stripeHandler(config: Record): UCPPaymentHandlerBinding { + return { + id: 'stripe', + version: '2026-04-08', + spec: 'https://agentscore.sh/specification/payment-handlers/stripe-spt', + schema: 'https://agentscore.sh/schemas/payment-handlers/stripe-spt.json', + config, + }; +} + async function main(): Promise { // ------------------------------------------------------------------------- - // node-minimal + // node-minimal — empty maps; just metadata + signing keys // ------------------------------------------------------------------------- { const KID = 'node-minimal-EdDSA'; const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); const profile = buildUCPProfile({ name: 'Minimal Merchant', - services: [{ type: 'rest', url: 'https://m.example.com' }], - payment_handlers: [], + services: { 'dev.ucp.shopping': [shopServiceMcp('https://m.example.com')] }, signing_keys: [publicJWK as UCPSigningKey], }); const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); @@ -64,21 +124,23 @@ async function main(): Promise { } // ------------------------------------------------------------------------- - // node-es256-rails — multi-service + multi-rail + ES256 signing key + // node-es256-rails — multi-transport service + multi-rail + ES256 signing key // ------------------------------------------------------------------------- { const KID = 'node-es256-rails-ES256'; const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID, alg: 'ES256' }); const profile = buildUCPProfile({ name: 'ES256 Merchant', - services: [ - { type: 'rest', url: 'https://a.example.com' }, - { type: 'a2a', url: 'https://a.example.com/agent-card.json' }, - ], - payment_handlers: [ - { name: 'tempo', config: { rail: 'tempo-mainnet', chain_id: 4217 } }, - { name: 'x402', config: { networks: ['base-8453'] } }, - ], + services: { + 'dev.ucp.shopping': [ + shopServiceMcp('https://a.example.com'), + shopServiceA2A('https://a.example.com'), + ], + }, + payment_handlers: { + 'sh.agentscore.payment.tempo': [tempoHandler({ rail: 'tempo-mainnet', chain_id: 4217 })], + 'sh.agentscore.payment.x402': [x402Handler(['base-8453'])], + }, signing_keys: [publicJWK as UCPSigningKey], }); const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID, alg: 'ES256' }); @@ -99,8 +161,10 @@ async function main(): Promise { const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); const profile = buildUCPProfile({ name: 'Extras Merchant', - services: [{ type: 'rest', url: 'https://e.example.com' }], - payment_handlers: [{ name: 'stripe', config: { profile_id: 'abc', count: 7 } }], + services: { 'dev.ucp.shopping': [shopServiceMcp('https://e.example.com')] }, + payment_handlers: { + 'sh.agentscore.payment.stripe-spt': [stripeHandler({ profile_id: 'abc', count: 7 })], + }, signing_keys: [publicJWK as UCPSigningKey], }); const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); @@ -114,28 +178,25 @@ async function main(): Promise { } // ------------------------------------------------------------------------- - // node-capability — a hand-crafted vendor capability (renamed to - // sh.agentscore.identity to match the new namespace; the in-fixture name is - // independent of the SDK's auto-injection but consistency keeps the corpus - // honest about what callers should publish). + // node-capability — hand-crafted vendor capability under sh.agentscore.identity // ------------------------------------------------------------------------- { const KID = 'node-capability-EdDSA'; const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); + const customCapability: UCPCapabilityBinding = { + version: '1', + spec: 'https://agentscore.sh/specification/identity', + schema: 'https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json', + // `extras` flat on the binding — kyc_required is a vendor field on this binding. + kyc_required: true, + }; const profile = buildUCPProfile({ name: 'Capability Merchant', - services: [{ type: 'rest', url: 'https://c.example.com' }], - capabilities: [ - { - name: 'sh.agentscore.identity', - schema: 'https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json', - version: '1', - kyc_required: true, - }, - ], - payment_handlers: [ - { name: 'tempo', config: { rail: 'tempo-mainnet', chain_id: 4217 } }, - ], + services: { 'dev.ucp.shopping': [shopServiceMcp('https://c.example.com')] }, + capabilities: { 'sh.agentscore.identity': [customCapability] }, + payment_handlers: { + 'sh.agentscore.payment.tempo': [tempoHandler({ rail: 'tempo-mainnet', chain_id: 4217 })], + }, signing_keys: [publicJWK as UCPSigningKey], }); const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); @@ -149,15 +210,17 @@ async function main(): Promise { } // ------------------------------------------------------------------------- - // node-unicode — multi-byte UTF-8 in name / url / config + // node-unicode — multi-byte UTF-8 in name / endpoint / config // ------------------------------------------------------------------------- { const KID = 'node-unicode-EdDSA'; const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); const profile = buildUCPProfile({ name: 'Café 日本 🍷 Merchant', - services: [{ type: 'rest', url: 'https://日本.example.com' }], - payment_handlers: [{ name: 'tempo', config: { note: 'メモ' } }], + services: { 'dev.ucp.shopping': [shopServiceMcp('https://日本.example.com')] }, + payment_handlers: { + 'sh.agentscore.payment.tempo': [tempoHandler({ note: 'メモ' })], + }, signing_keys: [publicJWK as UCPSigningKey], }); const signed = await signUCPProfile(profile, { signingKey: privateKey, kid: KID }); @@ -178,8 +241,10 @@ async function main(): Promise { const newKey = await generateUCPSigningKey({ kid: 'node-multikey-new' }); const profile = buildUCPProfile({ name: 'Multi-Key Merchant', - services: [{ type: 'rest', url: 'https://mk.example.com' }], - payment_handlers: [{ name: 'tempo', config: { rail: 'tempo-mainnet' } }], + services: { 'dev.ucp.shopping': [shopServiceMcp('https://mk.example.com')] }, + payment_handlers: { + 'sh.agentscore.payment.tempo': [tempoHandler({ rail: 'tempo-mainnet' })], + }, signing_keys: [oldKey.publicJWK as UCPSigningKey, newKey.publicJWK as UCPSigningKey], }); const signed = await signUCPProfile(profile, { signingKey: newKey.privateKey, kid: 'node-multikey-new' }); @@ -193,21 +258,24 @@ async function main(): Promise { } // ------------------------------------------------------------------------- - // node-emoji-keys — extras with non-ASCII object keys (BMP private use, CJK - // compatibility, supplementary plane). Exercises codepoint-vs-UTF-16 sort. + // node-emoji-keys — extras with non-ASCII object keys (BMP private use, + // CJK compatibility, supplementary plane). Exercises codepoint-vs-UTF-16 sort. + // Lives at top-level `extras` (outside the `ucp` envelope). // ------------------------------------------------------------------------- { const KID = 'node-emoji-keys-EdDSA'; const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); const profile = buildUCPProfile({ name: 'Emoji Keys Merchant', - services: [{ type: 'rest', url: 'https://emoji.example.com' }], - payment_handlers: [{ name: 'tempo', config: {} }], + services: { 'dev.ucp.shopping': [shopServiceMcp('https://emoji.example.com')] }, + payment_handlers: { + 'sh.agentscore.payment.tempo': [tempoHandler()], + }, signing_keys: [publicJWK as UCPSigningKey], extras: { a: 1, '豈': 2, - '': 3, + '': 3, '🍷': 4, }, }); @@ -222,15 +290,14 @@ async function main(): Promise { } // ------------------------------------------------------------------------- - // node-int-boundary — exercises Number.MAX_SAFE_INTEGER round-trip + // node-int-boundary — exercises Number.MAX_SAFE_INTEGER round-trip via extras // ------------------------------------------------------------------------- { const KID = 'node-int-boundary-EdDSA'; const { privateKey, publicJWK } = await generateUCPSigningKey({ kid: KID }); const profile = buildUCPProfile({ name: 'Int Boundary Merchant', - services: [{ type: 'rest', url: 'https://i.example.com' }], - payment_handlers: [], + services: { 'dev.ucp.shopping': [shopServiceMcp('https://i.example.com')] }, signing_keys: [publicJWK as UCPSigningKey], extras: { max_safe_int: 9007199254740991, @@ -251,7 +318,7 @@ async function main(): Promise { } // ------------------------------------------------------------------------- - // node-data-driven-claims — exercises the buildUCPProfile data path with + // node-data-driven-claims — exercises buildUCPProfile data path with // API-shape "missing" sentinels (empty string + null). Both languages MUST // emit identical canonical bytes for this input. // ------------------------------------------------------------------------- @@ -273,8 +340,7 @@ async function main(): Promise { }; const profile = buildUCPProfile({ name: 'Data Driven Claims Merchant', - services: [{ type: 'rest', url: 'https://d.example.com' }], - payment_handlers: [], + services: { 'dev.ucp.shopping': [shopServiceMcp('https://d.example.com')] }, signing_keys: [publicJWK as UCPSigningKey], data, }); @@ -289,8 +355,8 @@ async function main(): Promise { } // ------------------------------------------------------------------------- - // node-typed-claims — exercises the typed AssessResult fields (no raw - // fallback). Cross-lang parity check for the typed-field-only call site. + // node-typed-claims — exercises typed AssessResult fields (no raw fallback). + // Cross-lang parity check for the typed-field-only call site. // ------------------------------------------------------------------------- { const KID = 'node-typed-claims-EdDSA'; @@ -315,8 +381,7 @@ async function main(): Promise { }; const profile = buildUCPProfile({ name: 'Typed Claims Merchant', - services: [{ type: 'rest', url: 'https://t.example.com' }], - payment_handlers: [], + services: { 'dev.ucp.shopping': [shopServiceMcp('https://t.example.com')] }, signing_keys: [publicJWK as UCPSigningKey], data, }); diff --git a/tests/fixtures/cross-lang/node-capability.json b/tests/fixtures/cross-lang/node-capability.json index f148b37..a06e437 100644 --- a/tests/fixtures/cross-lang/node-capability.json +++ b/tests/fixtures/cross-lang/node-capability.json @@ -1,30 +1,44 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://c.example.com" - } - ], - "capabilities": [ - { - "name": "sh.agentscore.identity", - "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", - "version": "1", - "kyc_required": true - } - ], - "payment_handlers": [ - { - "name": "tempo", - "config": { - "rail": "tempo-mainnet", - "chain_id": 4217 - } - } - ], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://c.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "1", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "kyc_required": true + } + ] + }, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "rail": "tempo-mainnet", + "chain_id": 4217 + } + } + ] + }, + "name": "Capability Merchant" + }, "signing_keys": [ { "kid": "node-capability-EdDSA", @@ -32,11 +46,10 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "AeclvTjS8f6B3AwW9kO4yjbZCEShPVIBiNFGFR4ZZp4" + "x": "IuEDuQu_5--c_GVEaY4x0xjGbKro965U5VGyRY8TxpI" } ], - "name": "Capability Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwidmVyc2lvbiI6IjEifV0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiQWVjbHZUalM4ZjZCM0F3VzlrTzR5amJaQ0VTaFBWSUJpTkZHRlI0WlpwNCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.3ThDzMfTI4znrd0200TO0r-vTK2rS_w9BV6_PD0yyKcXvvu_dEqZVOs9R4kZRLJlPpnmoO8YpKg65qzcp5SwDA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtY2FwYWJpbGl0eS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJJdUVEdVF1XzUtLWNfR1ZFYVk0eDB4akdiS3JvOTY1VTVWR3lSWThUeHBJIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIxIn1dfSwibmFtZSI6IkNhcGFiaWxpdHkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9jLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.s36lpaOS-eGdTC0agCpLU_JxDLNO6nM5YjOTxJb6JoYVYzWBaflJCkWxwN6bDgdgDh-lPSY7_l7X0636TjpzCA" }, "jwks": { "keys": [ @@ -46,7 +59,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "AeclvTjS8f6B3AwW9kO4yjbZCEShPVIBiNFGFR4ZZp4" + "x": "IuEDuQu_5--c_GVEaY4x0xjGbKro965U5VGyRY8TxpI" } ] }, diff --git a/tests/fixtures/cross-lang/node-data-driven-claims.json b/tests/fixtures/cross-lang/node-data-driven-claims.json index 287e64a..c59119e 100644 --- a/tests/fixtures/cross-lang/node-data-driven-claims.json +++ b/tests/fixtures/cross-lang/node-data-driven-claims.json @@ -1,31 +1,44 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://d.example.com" - } - ], - "capabilities": [ - { - "name": "sh.agentscore.identity", - "version": "1", - "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", - "claims": { - "operator_id": "op_data_driven", - "kyc_level": "none", - "sanctions_clear": false, - "age_bracket": "unknown", - "jurisdiction": "", - "verified_at": null, - "verify_url": "https://agentscore.sh/verify/op_data_driven", - "issuer": "https://agentscore.sh" - } - } - ], - "payment_handlers": [], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://d.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "1", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "extends": [ + "dev.ucp.shopping.checkout", + "dev.ucp.shopping.cart" + ], + "claims": { + "operator_id": "op_data_driven", + "kyc_level": "none", + "sanctions_clear": false, + "age_bracket": "unknown", + "jurisdiction": "", + "verified_at": null, + "verify_url": "https://agentscore.sh/verify/op_data_driven", + "issuer": "https://agentscore.sh" + } + } + ] + }, + "payment_handlers": {}, + "name": "Data Driven Claims Merchant" + }, "signing_keys": [ { "kid": "node-data-driven-claims-EdDSA", @@ -33,11 +46,10 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "_-gSp0gvGWvi1K8l3CY5F_jVGRSnogFBxUwwUiz_wcw" + "x": "t9ul3BiA3r0fugZcbcEcyARb8SAH_-4dalE3sjaVMKc" } ], - "name": "Data Driven Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1kYXRhLWRyaXZlbi1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiXy1nU3AwZ3ZHV3ZpMUs4bDNDWTVGX2pWR1JTbm9nRkJ4VXd3VWl6X3djdyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.iDuLf2JRjIr-Dx2siH9gft6X7UUsY1X3uZAa1cSuED33hlNePVK5j-oOOn0c66DrFVeGfCgBrlpG0KZ3InVvCA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InQ5dWwzQmlBM3IwZnVnWmNiY0VjeUFSYjhTQUhfLTRkYWxFM3NqYVZNS2MifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnsic2guYWdlbnRzY29yZS5pZGVudGl0eSI6W3siY2xhaW1zIjp7ImFnZV9icmFja2V0IjoidW5rbm93biIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IiIsImt5Y19sZXZlbCI6Im5vbmUiLCJvcGVyYXRvcl9pZCI6Im9wX2RhdGFfZHJpdmVuIiwic2FuY3Rpb25zX2NsZWFyIjpmYWxzZSwidmVyaWZpZWRfYXQiOm51bGwsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX2RhdGFfZHJpdmVuIn0sImV4dGVuZHMiOlsiZGV2LnVjcC5zaG9wcGluZy5jaGVja291dCIsImRldi51Y3Auc2hvcHBpbmcuY2FydCJdLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL2lkZW50aXR5IiwidmVyc2lvbiI6IjEifV19LCJuYW1lIjoiRGF0YSBEcml2ZW4gQ2xhaW1zIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6e30sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9kLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.8MSGbttC6ITB1vEYr0Wq8kSRniYAV25gMT7jahMGKIJfcE-rBGTukPFpXzpBWUNkSWOW4ihkOvTA5Wxws4NjDA" }, "jwks": { "keys": [ @@ -47,7 +59,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "_-gSp0gvGWvi1K8l3CY5F_jVGRSnogFBxUwwUiz_wcw" + "x": "t9ul3BiA3r0fugZcbcEcyARb8SAH_-4dalE3sjaVMKc" } ] }, diff --git a/tests/fixtures/cross-lang/node-emoji-keys.json b/tests/fixtures/cross-lang/node-emoji-keys.json index c1507dc..ca325f0 100644 --- a/tests/fixtures/cross-lang/node-emoji-keys.json +++ b/tests/fixtures/cross-lang/node-emoji-keys.json @@ -1,20 +1,31 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://emoji.example.com" - } - ], - "capabilities": [], - "payment_handlers": [ - { - "name": "tempo", - "config": {} - } - ], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://emoji.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json" + } + ] + }, + "name": "Emoji Keys Merchant" + }, "signing_keys": [ { "kid": "node-emoji-keys-EdDSA", @@ -22,15 +33,14 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "c3dtoA-lzWibbsG7II88-F90FpkjTaBejCYYpNzCLKw" + "x": "O5o3d9qQsgo-eDXV9rnt-saHwzpiitL4kTcVxGr6mjE" } ], - "name": "Emoji Keys Merchant", "a": 1, "豈": 2, - "": 3, + "": 3, "🍷": 4, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZW1vamkta2V5cy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJhIjoxLCJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWVtb2ppLWtleXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiYzNkdG9BLWx6V2liYnNHN0lJODgtRjkwRnBralRhQmVqQ1lZcE56Q0xLdyJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsIuixiCI6Miwi7oCAIjozLCLwn423Ijo0fQ.mH8PvKgGWxaUZMBIAu7ePxjpWaP9RM970enSZkSlLUKTURgmoWCkvxqWvwm4eIiQ2q-OK3UMOw7_I1qO2xEBCQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZW1vamkta2V5cy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyIiOjMsImEiOjEsInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ik81bzNkOXFRc2dvLWVEWFY5cm50LXNhSHd6cGlpdEw0a1RjVnhHcjZtakUifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnt9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifSwi6LGIIjoyLCLwn423Ijo0fQ.a-34-eGa5zJtMxXiefamLIcm4UM_Wix1XpHcJRXcM8Fs1Lx3ErLxLl-pdgyveDP1DVel7FmaSXJJuANSRvB4Bw" }, "jwks": { "keys": [ @@ -40,7 +50,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "c3dtoA-lzWibbsG7II88-F90FpkjTaBejCYYpNzCLKw" + "x": "O5o3d9qQsgo-eDXV9rnt-saHwzpiitL4kTcVxGr6mjE" } ] }, diff --git a/tests/fixtures/cross-lang/node-es256-rails.json b/tests/fixtures/cross-lang/node-es256-rails.json index f8e67ca..c8bd844 100644 --- a/tests/fixtures/cross-lang/node-es256-rails.json +++ b/tests/fixtures/cross-lang/node-es256-rails.json @@ -1,35 +1,54 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://a.example.com" + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://a.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + }, + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "a2a", + "endpoint": "https://a.example.com/.well-known/agent-card.json" + } + ] }, - { - "type": "a2a", - "url": "https://a.example.com/agent-card.json" - } - ], - "capabilities": [], - "payment_handlers": [ - { - "name": "tempo", - "config": { - "rail": "tempo-mainnet", - "chain_id": 4217 - } + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "rail": "tempo-mainnet", + "chain_id": 4217 + } + } + ], + "sh.agentscore.payment.x402": [ + { + "id": "x402", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/x402", + "schema": "https://agentscore.sh/schemas/payment-handlers/x402.json", + "config": { + "networks": [ + "base-8453" + ] + } + } + ] }, - { - "name": "x402", - "config": { - "networks": [ - "base-8453" - ] - } - } - ], + "name": "ES256 Merchant" + }, "signing_keys": [ { "kid": "node-es256-rails-ES256", @@ -37,12 +56,11 @@ "use": "sig", "crv": "P-256", "kty": "EC", - "x": "_Hq8UqyZbxKGSySRkLkNNigGoBOs9O49vbV6NEPPFfw", - "y": "1NIEwISSuJ8qbASd6QBCFooBPsphl4m4-zYM56bm-Dg" + "x": "YJlpUMxCjw_uFVaklMcPBroRAAyWRFBb6hogNbBzwqc", + "y": "RPRH4k6hBTqEX0-Wf9s2y3VAFcwtYDnZz53Y-3G-Vl8" } ], - "name": "ES256 Merchant", - "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJrdHkiOiJFQyIsInVzZSI6InNpZyIsIngiOiJfSHE4VXF5WmJ4S0dTeVNSa0xrTk5pZ0dvQk9zOU80OXZiVjZORVBQRmZ3IiwieSI6IjFOSUV3SVNTdUo4cWJBU2Q2UUJDRm9vQlBzcGhsNG00LXpZTTU2Ym0tRGcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.Pt-f7cc8KNxAuLM4vlCIt69qCoUlb5SnzQjSncvX-qsMwlwKCKwNNe9n0oRoZ75qQg8v1PZN5RWMwPmhJxIeNA" + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6Im5vZGUtZXMyNTYtcmFpbHMtRVMyNTYiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVTMjU2IiwiY3J2IjoiUC0yNTYiLCJraWQiOiJub2RlLWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiWUpscFVNeENqd191RlZha2xNY1BCcm9SQUF5V1JGQmI2aG9nTmJCendxYyIsInkiOiJSUFJINGs2aEJUcUVYMC1XZjlzMnkzVkFGY3d0WURuWno1M1ktM0ctVmw4In1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkVTMjU2IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJjaGFpbl9pZCI6NDIxNywicmFpbCI6InRlbXBvLW1haW5uZXQifSwiaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV0sInNoLmFnZW50c2NvcmUucGF5bWVudC54NDAyIjpbeyJjb25maWciOnsibmV0d29ya3MiOlsiYmFzZS04NDUzIl19LCJpZCI6Ing0MDIiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3g0MDIuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3g0MDIiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9hLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifSx7ImVuZHBvaW50IjoiaHR0cHM6Ly9hLmV4YW1wbGUuY29tLy53ZWxsLWtub3duL2FnZW50LWNhcmQuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoiYTJhIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.lhet7Dek3XSboG8lxyoGEc4-6kEQqwkxXbR2qqKGdKlB4aoXmHrN0hpQZSzzfqKpjwN_I7VgZiKZOteTqhKrMQ" }, "jwks": { "keys": [ @@ -52,8 +70,8 @@ "use": "sig", "crv": "P-256", "kty": "EC", - "x": "_Hq8UqyZbxKGSySRkLkNNigGoBOs9O49vbV6NEPPFfw", - "y": "1NIEwISSuJ8qbASd6QBCFooBPsphl4m4-zYM56bm-Dg" + "x": "YJlpUMxCjw_uFVaklMcPBroRAAyWRFBb6hogNbBzwqc", + "y": "RPRH4k6hBTqEX0-Wf9s2y3VAFcwtYDnZz53Y-3G-Vl8" } ] }, diff --git a/tests/fixtures/cross-lang/node-extras-int.json b/tests/fixtures/cross-lang/node-extras-int.json index e43a6ec..60f17ec 100644 --- a/tests/fixtures/cross-lang/node-extras-int.json +++ b/tests/fixtures/cross-lang/node-extras-int.json @@ -1,23 +1,35 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://e.example.com" - } - ], - "capabilities": [], - "payment_handlers": [ - { - "name": "stripe", - "config": { - "profile_id": "abc", - "count": 7 - } - } - ], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://e.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.stripe-spt": [ + { + "id": "stripe", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/stripe-spt", + "schema": "https://agentscore.sh/schemas/payment-handlers/stripe-spt.json", + "config": { + "profile_id": "abc", + "count": 7 + } + } + ] + }, + "name": "Extras Merchant" + }, "signing_keys": [ { "kid": "node-extras-int-EdDSA", @@ -25,11 +37,10 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "q8TPukNcTGlAQITtxzuMx-VPo7b0u78TZ6l7tPLZ1Lk" + "x": "LwGeYhxjsedo9kllWo8uRdHZnf9teSPjEGLJrhF9o0M" } ], - "name": "Extras Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoicThUUHVrTmNUR2xBUUlUdHh6dU14LVZQbzdiMHU3OFRaNmw3dFBMWjFMayJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.9KMviIIukuWTKrLYyrLzyWpSLterso4-TWMe6-i_IPnZ1DkVEeo09ql73NF1dcBk2E8bNetcJ2o603JyLD5pCQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtZXh0cmFzLWludC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJMd0dlWWh4anNlZG85a2xsV284dVJkSFpuZjl0ZVNQakVHTEpyaEY5bzBNIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkV4dHJhcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnN0cmlwZS1zcHQiOlt7ImNvbmZpZyI6eyJjb3VudCI6NywicHJvZmlsZV9pZCI6ImFiYyJ9LCJpZCI6InN0cmlwZSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvc3RyaXBlLXNwdC5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvc3RyaXBlLXNwdCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL2UuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In19.s6sBki-bBhRuZNZiv7s7NO3NpLDfhoXhZXKcK2uitVGiBh9nANv-pi8L-nAIBte8jN_DFoeqtWQJiAK188XqBg" }, "jwks": { "keys": [ @@ -39,7 +50,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "q8TPukNcTGlAQITtxzuMx-VPo7b0u78TZ6l7tPLZ1Lk" + "x": "LwGeYhxjsedo9kllWo8uRdHZnf9teSPjEGLJrhF9o0M" } ] }, diff --git a/tests/fixtures/cross-lang/node-int-boundary.json b/tests/fixtures/cross-lang/node-int-boundary.json index 2a6df84..74dd819 100644 --- a/tests/fixtures/cross-lang/node-int-boundary.json +++ b/tests/fixtures/cross-lang/node-int-boundary.json @@ -1,15 +1,22 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://i.example.com" - } - ], - "capabilities": [], - "payment_handlers": [], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://i.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": {}, + "name": "Int Boundary Merchant" + }, "signing_keys": [ { "kid": "node-int-boundary-EdDSA", @@ -17,16 +24,15 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "Szf46vxQ_9bY6fp12Tzs2jxUtDtRnPSLAI2FeaMkhk8" + "x": "vmNTcQKo5jUIpTVnWRSkLu-s7cUoNO_OfPJTctAOhR4" } ], - "name": "Int Boundary Merchant", "max_safe_int": 9007199254740991, "min_safe_int": -9007199254740991, "small_int": 42, "neg_small_int": -42, "zero": 0, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaW50LWJvdW5kYXJ5LUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiU3pmNDZ2eFFfOWJZNmZwMTJUenMyanhVdER0Um5QU0xBSTJGZWFNa2hrOCJ9XSwic21hbGxfaW50Ijo0Miwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyIsInplcm8iOjB9.DvkIZ_rn0utUn4LQhLsIFeoA9iIhpEb3Gk3_Q93LOLaHg5kuw226m35IFODJV2WwkJtjmJ6-Ib829V_-7iF8Bg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtaW50LWJvdW5kYXJ5LUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5lZ19zbWFsbF9pbnQiOi00Miwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLWludC1ib3VuZGFyeS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJ2bU5UY1FLbzVqVUlwVFZuV1JTa0x1LXM3Y1VvTk9fT2ZQSlRjdEFPaFI0In1dLCJzbWFsbF9pbnQiOjQyLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkludCBCb3VuZGFyeSBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vaS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifSwiemVybyI6MH0.iIsGfdlC2ZqMh3ouvz86u4QmGS0d-JR9KyTcUNoMTnqbt0P63PBJ7lXCoZ64DY4XtFJ83sPzSrOIzvdsbrOvBQ" }, "jwks": { "keys": [ @@ -36,7 +42,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "Szf46vxQ_9bY6fp12Tzs2jxUtDtRnPSLAI2FeaMkhk8" + "x": "vmNTcQKo5jUIpTVnWRSkLu-s7cUoNO_OfPJTctAOhR4" } ] }, diff --git a/tests/fixtures/cross-lang/node-minimal.json b/tests/fixtures/cross-lang/node-minimal.json index 9f840e2..32eef05 100644 --- a/tests/fixtures/cross-lang/node-minimal.json +++ b/tests/fixtures/cross-lang/node-minimal.json @@ -1,15 +1,22 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://m.example.com" - } - ], - "capabilities": [], - "payment_handlers": [], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://m.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": {}, + "name": "Minimal Merchant" + }, "signing_keys": [ { "kid": "node-minimal-EdDSA", @@ -17,11 +24,10 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "2Jgxm4RvhN3zQ-tmfPw3e-kqm80FGByMyjmswNnjl0I" + "x": "69RWgrarCEN0sSH5FfkJ2-miQQNRpXYh0wt9kviqzqk" } ], - "name": "Minimal Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS1taW5pbWFsLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjJKZ3htNFJ2aE4zelEtdG1mUHczZS1rcW04MEZHQnlNeWptc3dObmpsMEkifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.hcmlPgS0XaPdSe9kPFhaMbIvmNPzEaBJY9jW_ZYW7kDMftsaBt4wwF-SocM8z-dcpos4kNGsPAWKEzGHnYAaAg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI2OVJXZ3JhckNFTjBzU0g1RmZrSjItbWlRUU5ScFhZaDB3dDlrdmlxenFrIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6Ik1pbmltYWwgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7fSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL20uZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In19.ei7hxM6v-gnxAkgG4NiWLwzhd9wOxg3lO9ZTFVEuSBaAho0n_GaQayO99ibjQgqa2yUa1J9PcGh3woMh7cQcAA" }, "jwks": { "keys": [ @@ -31,7 +37,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "2Jgxm4RvhN3zQ-tmfPw3e-kqm80FGByMyjmswNnjl0I" + "x": "69RWgrarCEN0sSH5FfkJ2-miQQNRpXYh0wt9kviqzqk" } ] }, diff --git a/tests/fixtures/cross-lang/node-multikey.json b/tests/fixtures/cross-lang/node-multikey.json index b218774..5027299 100644 --- a/tests/fixtures/cross-lang/node-multikey.json +++ b/tests/fixtures/cross-lang/node-multikey.json @@ -1,22 +1,34 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://mk.example.com" - } - ], - "capabilities": [], - "payment_handlers": [ - { - "name": "tempo", - "config": { - "rail": "tempo-mainnet" - } - } - ], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://mk.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "rail": "tempo-mainnet" + } + } + ] + }, + "name": "Multi-Key Merchant" + }, "signing_keys": [ { "kid": "node-multikey-old", @@ -24,7 +36,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "OegF87KiSAOfa6Wd3qJKCNPmoFKHBxPaf9YqgvlTQvY" + "x": "dh_cI_8_Z79h3t5i72fKw89EwpeJiA2ELN1SnS_OgdQ" }, { "kid": "node-multikey-new", @@ -32,11 +44,10 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "mrwPS0Dm8qiH2sFDtECXMgiRa5u3GS1h5UCG4NqxwxY" + "x": "oxGfu9h6LckqvQ0eVkovSzUwCdGo8xLkPcq8siUoh7M" } ], - "name": "Multi-Key Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJPZWdGODdLaVNBT2ZhNldkM3FKS0NOUG1vRktIQnhQYWY5WXFndmxUUXZZIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJub2RlLW11bHRpa2V5LW5ldyIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJtcndQUzBEbThxaUgyc0ZEdEVDWE1naVJhNXUzR1MxaDVVQ0c0TnF4d3hZIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.oc_dpoITPPuL0oft-Lu0rEa7Hm10LNTe8RYqMpPbaA0xtJcQzRyJMiQgJ3sWOiXSRst6CzeuO_von36ansqWCA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtbXVsdGlrZXktb2xkIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImRoX2NJXzhfWjc5aDN0NWk3MmZLdzg5RXdwZUppQTJFTE4xU25TX09nZFEifSx7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Im94R2Z1OWg2TGNrcXZRMGVWa292U3pVd0NkR284eExrUGNxOHNpVW9oN00ifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnt9LCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.fEq5VVrBtuwEYJGcpHuaTCVQWmS6LvcOdtS-reZGyLFosCrmok9eU86w9m79aO6k0u_CXOC_n90TvbFfuKINCA" }, "jwks": { "keys": [ @@ -46,7 +57,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "OegF87KiSAOfa6Wd3qJKCNPmoFKHBxPaf9YqgvlTQvY" + "x": "dh_cI_8_Z79h3t5i72fKw89EwpeJiA2ELN1SnS_OgdQ" }, { "kid": "node-multikey-new", @@ -54,7 +65,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "mrwPS0Dm8qiH2sFDtECXMgiRa5u3GS1h5UCG4NqxwxY" + "x": "oxGfu9h6LckqvQ0eVkovSzUwCdGo8xLkPcq8siUoh7M" } ] }, diff --git a/tests/fixtures/cross-lang/node-typed-claims.json b/tests/fixtures/cross-lang/node-typed-claims.json index 22b0c25..d754f4c 100644 --- a/tests/fixtures/cross-lang/node-typed-claims.json +++ b/tests/fixtures/cross-lang/node-typed-claims.json @@ -1,31 +1,44 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://t.example.com" - } - ], - "capabilities": [ - { - "name": "sh.agentscore.identity", - "version": "1", - "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", - "claims": { - "operator_id": "op_typed_claims", - "kyc_level": "enhanced", - "sanctions_clear": true, - "age_bracket": "21+", - "jurisdiction": "US", - "verified_at": "2026-04-01T00:00:00Z", - "verify_url": "https://agentscore.sh/verify/op_typed_claims", - "issuer": "https://agentscore.sh" - } - } - ], - "payment_handlers": [], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://t.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "1", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "extends": [ + "dev.ucp.shopping.checkout", + "dev.ucp.shopping.cart" + ], + "claims": { + "operator_id": "op_typed_claims", + "kyc_level": "enhanced", + "sanctions_clear": true, + "age_bracket": "21+", + "jurisdiction": "US", + "verified_at": "2026-04-01T00:00:00Z", + "verify_url": "https://agentscore.sh/verify/op_typed_claims", + "issuer": "https://agentscore.sh" + } + } + ] + }, + "payment_handlers": {}, + "name": "Typed Claims Merchant" + }, "signing_keys": [ { "kid": "node-typed-claims-EdDSA", @@ -33,11 +46,10 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "ZdOtA_ss6CHrTAhGqci7hIFTL8027NfXNMypu_tCKE4" + "x": "6JcesuEfiy104P6W8zOsruWkL7Ju7RLXMyR2F3fQ4xM" } ], - "name": "Typed Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoibm9kZS10eXBlZC1jbGFpbXMtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiWmRPdEFfc3M2Q0hyVEFoR3FjaTdoSUZUTDgwMjdOZlhOTXlwdV90Q0tFNCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.KtuEN5n05QAQHkmvcxpYzfXfivsGwSfAx_tODt4nb7qhEOpyqBVGKwwJbX2NEy5D0TpEjyoFp_E6OUAmDkgcBg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjZKY2VzdUVmaXkxMDRQNlc4ek9zcnVXa0w3SnU3UkxYTXlSMkYzZlE0eE0ifV0sInVjcCI6eyJjYXBhYmlsaXRpZXMiOnsic2guYWdlbnRzY29yZS5pZGVudGl0eSI6W3siY2xhaW1zIjp7ImFnZV9icmFja2V0IjoiMjErIiwiaXNzdWVyIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoIiwianVyaXNkaWN0aW9uIjoiVVMiLCJreWNfbGV2ZWwiOiJlbmhhbmNlZCIsIm9wZXJhdG9yX2lkIjoib3BfdHlwZWRfY2xhaW1zIiwic2FuY3Rpb25zX2NsZWFyIjp0cnVlLCJ2ZXJpZmllZF9hdCI6IjIwMjYtMDQtMDFUMDA6MDA6MDBaIiwidmVyaWZ5X3VybCI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC92ZXJpZnkvb3BfdHlwZWRfY2xhaW1zIn0sImV4dGVuZHMiOlsiZGV2LnVjcC5zaG9wcGluZy5jaGVja291dCIsImRldi51Y3Auc2hvcHBpbmcuY2FydCJdLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL2lkZW50aXR5IiwidmVyc2lvbiI6IjEifV19LCJuYW1lIjoiVHlwZWQgQ2xhaW1zIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6e30sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly90LmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.MzebNr-eOGPHe84z8ARhjFHmSLju7AvwgsSu4KY_tmg5R_T6xYrx7tZXbYOCfiSaZoFmpIJcXPakit4c-yxMAA" }, "jwks": { "keys": [ @@ -47,7 +59,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "ZdOtA_ss6CHrTAhGqci7hIFTL8027NfXNMypu_tCKE4" + "x": "6JcesuEfiy104P6W8zOsruWkL7Ju7RLXMyR2F3fQ4xM" } ] }, diff --git a/tests/fixtures/cross-lang/node-unicode.json b/tests/fixtures/cross-lang/node-unicode.json index 77b7f2f..89fb48b 100644 --- a/tests/fixtures/cross-lang/node-unicode.json +++ b/tests/fixtures/cross-lang/node-unicode.json @@ -1,22 +1,34 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://日本.example.com" - } - ], - "capabilities": [], - "payment_handlers": [ - { - "name": "tempo", - "config": { - "note": "メモ" - } - } - ], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://日本.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "note": "メモ" + } + } + ] + }, + "name": "Café 日本 🍷 Merchant" + }, "signing_keys": [ { "kid": "node-unicode-EdDSA", @@ -24,11 +36,10 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "4bSDSdklqdRpcUwMmerxArvPlqFwGDUH13wqNQVRfHc" + "x": "At1k1YXploco8YrjdagqC9HYxCnN7ommm4MWIRUp5AY" } ], - "name": "Café 日本 🍷 Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiI0YlNEU2RrbHFkUnBjVXdNbWVyeEFydlBscUZ3R0RVSDEzd3FOUVZSZkhjIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.sopMGjSMti21_96dyk8cbrkv6tIDStW-lc74IbVhnakgovuGAunvSIMRzqvAXAweYksBrvvuuAVpoSjBXH8fCQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6Im5vZGUtdW5pY29kZS1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJBdDFrMVlYcGxvY284WXJqZGFncUM5SFl4Q25ON29tbW00TVdJUlVwNUFZIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkNhZsOpIOaXpeacrCDwn423IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJub3RlIjoi44Oh44OiIn0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL-aXpeacrC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.BKhv1J0LSZ-PQBySoMQXTAx-OalhQZSiiCXaWSHjA6HbeCvz4aw-os3p-FlAgfDoiChxRKeGfN-n-LcYIv4yAQ" }, "jwks": { "keys": [ @@ -38,7 +49,7 @@ "use": "sig", "crv": "Ed25519", "kty": "OKP", - "x": "4bSDSdklqdRpcUwMmerxArvPlqFwGDUH13wqNQVRfHc" + "x": "At1k1YXploco8YrjdagqC9HYxCnN7ommm4MWIRUp5AY" } ] }, diff --git a/tests/fixtures/cross-lang/py-capability.json b/tests/fixtures/cross-lang/py-capability.json index b1c3c6c..af06dd8 100644 --- a/tests/fixtures/cross-lang/py-capability.json +++ b/tests/fixtures/cross-lang/py-capability.json @@ -1,30 +1,44 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://c.example.com" - } - ], - "capabilities": [ - { - "name": "sh.agentscore.identity", - "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", - "version": "1", - "kyc_required": true - } - ], - "payment_handlers": [ - { - "name": "tempo", - "config": { - "rail": "tempo-mainnet", - "chain_id": 4217 - } - } - ], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://c.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "1", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "kyc_required": true + } + ] + }, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "rail": "tempo-mainnet", + "chain_id": 4217 + } + } + ] + }, + "name": "Capability Merchant" + }, "signing_keys": [ { "kid": "py-capability-EdDSA", @@ -32,17 +46,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "TMBp_r4E06Pdu0h-53QW7ncMNSYwPXLem6kMjY-3We8" + "x": "TikhC4jSghoLfPC6j9KBytlHrgyFvZVVm5OUjG7bYCM" } ], - "name": "Capability Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwidmVyc2lvbiI6IjEifV0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1jYXBhYmlsaXR5LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IlRNQnBfcjRFMDZQZHUwaC01M1FXN25jTU5TWXdQWExlbTZrTWpZLTNXZTgifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.tFu690kDz0E2Iy45Y0MgpUS3G2ocaBgeWFUwwPQSjsXXroi1T4GFZROrA_6MntaKb07CfjLgWoMh9z8Cl3ecBA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiVGlraEM0alNnaG9MZlBDNmo5S0J5dGxIcmd5RnZaVlZtNU9Vakc3YllDTSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6eyJzaC5hZ2VudHNjb3JlLmlkZW50aXR5IjpbeyJreWNfcmVxdWlyZWQiOnRydWUsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vaWRlbnRpdHkiLCJ2ZXJzaW9uIjoiMSJ9XX0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJjaGFpbl9pZCI6NDIxNywicmFpbCI6InRlbXBvLW1haW5uZXQifSwiaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0._31-NgZEBmN2c8qyxQvOaEBrhycJ6MULjhfN3sgVp5UqiUduGp66XHQC0HI4Ni6W7CzNx2-ktZWdLWD0clPdDg" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "TMBp_r4E06Pdu0h-53QW7ncMNSYwPXLem6kMjY-3We8", + "x": "TikhC4jSghoLfPC6j9KBytlHrgyFvZVVm5OUjG7bYCM", "kid": "py-capability-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-data-driven-claims.json b/tests/fixtures/cross-lang/py-data-driven-claims.json index 8153869..5e3bb79 100644 --- a/tests/fixtures/cross-lang/py-data-driven-claims.json +++ b/tests/fixtures/cross-lang/py-data-driven-claims.json @@ -1,31 +1,44 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://d.example.com" - } - ], - "capabilities": [ - { - "name": "sh.agentscore.identity", - "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", - "version": "1", - "claims": { - "operator_id": "op_data_driven", - "kyc_level": "none", - "sanctions_clear": false, - "age_bracket": "unknown", - "jurisdiction": "", - "verified_at": null, - "verify_url": "https://agentscore.sh/verify/op_data_driven", - "issuer": "https://agentscore.sh" - } - } - ], - "payment_handlers": [], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://d.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "1", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "extends": [ + "dev.ucp.shopping.checkout", + "dev.ucp.shopping.cart" + ], + "claims": { + "operator_id": "op_data_driven", + "kyc_level": "none", + "sanctions_clear": false, + "age_bracket": "unknown", + "jurisdiction": "", + "verified_at": null, + "verify_url": "https://agentscore.sh/verify/op_data_driven", + "issuer": "https://agentscore.sh" + } + } + ] + }, + "payment_handlers": {}, + "name": "Data Driven Claims Merchant" + }, "signing_keys": [ { "kid": "py-data-driven-claims-EdDSA", @@ -33,17 +46,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "DAaVG_-gxUtcNYUOizP4YJNRwhHhnDWn-stsD7jPO4w" + "x": "g_RzTBbrZ0krF4_f4Rtm__flo_1RH2sxiTF9dLltpC8" } ], - "name": "Data Driven Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IkRBYVZHXy1neFV0Y05ZVU9pelA0WUpOUndoSGhuRFduLXN0c0Q3alBPNHcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.Bo9sH1eHbuXfl6XMp43smJDrGd6KFFoEMejcmgAYTScOiBzgRk9bs7s7YgNSjM4QXnZ-2YXtrI58d1n7tu-4BA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJnX1J6VEJiclowa3JGNF9mNFJ0bV9fZmxvXzFSSDJzeGlURjlkTGx0cEM4In1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJleHRlbmRzIjpbImRldi51Y3Auc2hvcHBpbmcuY2hlY2tvdXQiLCJkZXYudWNwLnNob3BwaW5nLmNhcnQiXSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIxIn1dfSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vZC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.X7Xdu_60_sT2XpwD9SqF7Lpuf5OGlbG_t_sxaY1xf7rQID5fSR-4BEdB0Dppq04nuaedhcqGUeyTMZDfHe8YDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "DAaVG_-gxUtcNYUOizP4YJNRwhHhnDWn-stsD7jPO4w", + "x": "g_RzTBbrZ0krF4_f4Rtm__flo_1RH2sxiTF9dLltpC8", "kid": "py-data-driven-claims-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-emoji-keys.json b/tests/fixtures/cross-lang/py-emoji-keys.json index 8502a8e..209540d 100644 --- a/tests/fixtures/cross-lang/py-emoji-keys.json +++ b/tests/fixtures/cross-lang/py-emoji-keys.json @@ -1,20 +1,31 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://emoji.example.com" - } - ], - "capabilities": [], - "payment_handlers": [ - { - "name": "tempo", - "config": {} - } - ], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://emoji.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json" + } + ] + }, + "name": "Emoji Keys Merchant" + }, "signing_keys": [ { "kid": "py-emoji-keys-EdDSA", @@ -22,21 +33,20 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "rI2JPvoFJRRLd3EbGgoiKut82R3us1TTAIpwhp97BSY" + "x": "t9o2BRiSJvI4c7a3KlzCqzKS1evXIyngTwB2GBxtZec" } ], - "name": "Emoji Keys Merchant", "a": 1, "豈": 2, - "": 3, + "": 3, "🍷": 4, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJhIjoxLCJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InJJMkpQdm9GSlJSTGQzRWJHZ29pS3V0ODJSM3VzMVRUQUlwd2hwOTdCU1kifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTciLCLosYgiOjIsIu6AgCI6Mywi8J-NtyI6NH0.1zOHof4cekmU2iphy-mp5DQHS66klN45KOrJ87bKsQYnO6_o2cL6iCEH6GOuQ9qBFTBUfzOWapK1hCAIX3vOAQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyIiOjMsImEiOjEsInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZW1vamkta2V5cy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJ0OW8yQlJpU0p2STRjN2EzS2x6Q3F6S1MxZXZYSXluZ1R3QjJHQnh0WmVjIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkVtb2ppIEtleXMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vZW1vamkuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In0sIuixiCI6Miwi8J-NtyI6NH0.JFxAqyuCgvA0HNAl2giJeb4MbHDuW5h7jBjGcrQxcSDCiCgXjzhaUSWJXjiB7GeHcL7CDMg3kj79VQ4Rsr1-Bw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "rI2JPvoFJRRLd3EbGgoiKut82R3us1TTAIpwhp97BSY", + "x": "t9o2BRiSJvI4c7a3KlzCqzKS1evXIyngTwB2GBxtZec", "kid": "py-emoji-keys-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-es256-rails.json b/tests/fixtures/cross-lang/py-es256-rails.json index 7694b1e..4c9ffa9 100644 --- a/tests/fixtures/cross-lang/py-es256-rails.json +++ b/tests/fixtures/cross-lang/py-es256-rails.json @@ -1,35 +1,54 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://a.example.com" + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://a.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + }, + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "a2a", + "endpoint": "https://a.example.com/.well-known/agent-card.json" + } + ] }, - { - "type": "a2a", - "url": "https://a.example.com/agent-card.json" - } - ], - "capabilities": [], - "payment_handlers": [ - { - "name": "tempo", - "config": { - "rail": "tempo-mainnet", - "chain_id": 4217 - } + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "rail": "tempo-mainnet", + "chain_id": 4217 + } + } + ], + "sh.agentscore.payment.x402": [ + { + "id": "x402", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/x402", + "schema": "https://agentscore.sh/schemas/payment-handlers/x402.json", + "config": { + "networks": [ + "base-8453" + ] + } + } + ] }, - { - "name": "x402", - "config": { - "networks": [ - "base-8453" - ] - } - } - ], + "name": "ES256 Merchant" + }, "signing_keys": [ { "kid": "py-es256-rails-ES256", @@ -37,19 +56,18 @@ "alg": "ES256", "use": "sig", "crv": "P-256", - "x": "_2HJ2FjbKQFI-uVpxIY8ZIS3htpNb-92IGfIY8kpQhk", - "y": "0KlKsjssFxnAPoozOr2CvYXbuoXih_IzMv-wHQCsBXA" + "x": "NFS5qrSPV5sDQ5hHVag2zFqOSpTO6NBL-Hqf9EjBOco", + "y": "wtbEwX6TxEFid1IJvIwxkVfNic3Q_xEOq7j54Kje7aY" } ], - "name": "ES256 Merchant", - "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiXzJISjJGamJLUUZJLXVWcHhJWThaSVMzaHRwTmItOTJJR2ZJWThrcFFoayIsInkiOiIwS2xLc2pzc0Z4bkFQb296T3IyQ3ZZWGJ1b1hpaF9Jek12LXdIUUNzQlhBIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.MmqXHAzDEAPjH6wmOvwDq-yyq-0TCyMXVDwZlgryCN0O2E1sDS4jVeOb69F05uWdiPrLBP-HpJGTxruvHJre-g" + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVTMjU2IiwiY3J2IjoiUC0yNTYiLCJraWQiOiJweS1lczI1Ni1yYWlscy1FUzI1NiIsImt0eSI6IkVDIiwidXNlIjoic2lnIiwieCI6Ik5GUzVxclNQVjVzRFE1aEhWYWcyekZxT1NwVE82TkJMLUhxZjlFakJPY28iLCJ5Ijoid3RiRXdYNlR4RUZpZDFJSnZJd3hrVmZOaWMzUV94RU9xN2o1NEtqZTdhWSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJFUzI1NiBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dLCJzaC5hZ2VudHNjb3JlLnBheW1lbnQueDQwMiI6W3siY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwiaWQiOiJ4NDAyIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy94NDAyLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy94NDAyIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In0seyJlbmRwb2ludCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS8ud2VsbC1rbm93bi9hZ2VudC1jYXJkLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6ImEyYSIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.Vo7XPWeW37oSI5Eub7oUVwb3ODY3g70PeNgYLODGQ9L2nrf-5K7yinG2QwHEh5GtIMq7fXp5fiQVk1KtFnL4Wg" }, "jwks": { "keys": [ { "crv": "P-256", - "x": "_2HJ2FjbKQFI-uVpxIY8ZIS3htpNb-92IGfIY8kpQhk", - "y": "0KlKsjssFxnAPoozOr2CvYXbuoXih_IzMv-wHQCsBXA", + "x": "NFS5qrSPV5sDQ5hHVag2zFqOSpTO6NBL-Hqf9EjBOco", + "y": "wtbEwX6TxEFid1IJvIwxkVfNic3Q_xEOq7j54Kje7aY", "kid": "py-es256-rails-ES256", "alg": "ES256", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-extras-int.json b/tests/fixtures/cross-lang/py-extras-int.json index 7632dcf..f1380bd 100644 --- a/tests/fixtures/cross-lang/py-extras-int.json +++ b/tests/fixtures/cross-lang/py-extras-int.json @@ -1,23 +1,35 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://e.example.com" - } - ], - "capabilities": [], - "payment_handlers": [ - { - "name": "stripe", - "config": { - "profile_id": "abc", - "count": 7 - } - } - ], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://e.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.stripe-spt": [ + { + "id": "stripe", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/stripe-spt", + "schema": "https://agentscore.sh/schemas/payment-handlers/stripe-spt.json", + "config": { + "profile_id": "abc", + "count": 7 + } + } + ] + }, + "name": "Extras Merchant" + }, "signing_keys": [ { "kid": "py-extras-int-EdDSA", @@ -25,17 +37,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "rSdbpACnhv_GVb7R01lDjmO7kUUvZR6GKBYR0AhW4go" + "x": "El2ke55St-sfq6gYs6wYJyJX7TIw3-spyA1hlMiNhpM" } ], - "name": "Extras Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1leHRyYXMtaW50LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InJTZGJwQUNuaHZfR1ZiN1IwMWxEam1PN2tVVXZaUjZHS0JZUjBBaFc0Z28ifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.SW6CJuYzFh5PJ_AQJ89iUINqhW7O1kZvDQo1zuhqtjtU-Gj48XI2pykZph04lBcBS8r4mVEvNrUzVxVi44hXCw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiRWwya2U1NVN0LXNmcTZnWXM2d1lKeUpYN1RJdzMtc3B5QTFobE1pTmhwTSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJFeHRyYXMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC5zdHJpcGUtc3B0IjpbeyJjb25maWciOnsiY291bnQiOjcsInByb2ZpbGVfaWQiOiJhYmMifSwiaWQiOiJzdHJpcGUiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3N0cmlwZS1zcHQuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3N0cmlwZS1zcHQiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9lLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.0DAtQpZ-9e8U3cmpzTHwWFZq2LmmchY6mz-rhRxybkNX4YDlqpPLcfAig7ybMzdo_O7afJ9QDNYfDERmCGtVDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "rSdbpACnhv_GVb7R01lDjmO7kUUvZR6GKBYR0AhW4go", + "x": "El2ke55St-sfq6gYs6wYJyJX7TIw3-spyA1hlMiNhpM", "kid": "py-extras-int-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-int-boundary.json b/tests/fixtures/cross-lang/py-int-boundary.json index 32a5c44..9c91acc 100644 --- a/tests/fixtures/cross-lang/py-int-boundary.json +++ b/tests/fixtures/cross-lang/py-int-boundary.json @@ -1,15 +1,22 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://i.example.com" - } - ], - "capabilities": [], - "payment_handlers": [], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://i.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": {}, + "name": "Int Boundary Merchant" + }, "signing_keys": [ { "kid": "py-int-boundary-EdDSA", @@ -17,22 +24,21 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "2hWtYbSpkVrFTzo_rccrGWAYf_jrreq8wB1D_z_IZOc" + "x": "b5OlULxsP0xpS8IkLF4tRaiB1u6yODPxsQJJYv1iB6s" } ], - "name": "Int Boundary Merchant", "max_safe_int": 9007199254740991, "min_safe_int": -9007199254740991, "small_int": 42, "neg_small_int": -42, "zero": 0, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktaW50LWJvdW5kYXJ5LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjJoV3RZYlNwa1ZyRlR6b19yY2NyR1dBWWZfanJyZXE4d0IxRF96X0laT2MifV0sInNtYWxsX2ludCI6NDIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTciLCJ6ZXJvIjowfQ.1qtHiT04A_A0asQx3jJWeeNEfY9lQ6haBf6JDMoaOz3NmWLg70Yvad0wStR8wQcaDDqPgi7-mmMZMnu8XxvyCQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5lZ19zbWFsbF9pbnQiOi00Miwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiYjVPbFVMeHNQMHhwUzhJa0xGNHRSYWlCMXU2eU9EUHhzUUpKWXYxaUI2cyJ9XSwic21hbGxfaW50Ijo0MiwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7fSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL2kuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In0sInplcm8iOjB9.PsM9i8EXGN5eNPJI6_6Efk8P-aE-gQQvmXpNCr1vTFMtsjvUrwPO974mweqhbyogrdfm47UkAhJZ2tkGQ26YDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "2hWtYbSpkVrFTzo_rccrGWAYf_jrreq8wB1D_z_IZOc", + "x": "b5OlULxsP0xpS8IkLF4tRaiB1u6yODPxsQJJYv1iB6s", "kid": "py-int-boundary-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-minimal.json b/tests/fixtures/cross-lang/py-minimal.json index 67a31e6..0e83bfc 100644 --- a/tests/fixtures/cross-lang/py-minimal.json +++ b/tests/fixtures/cross-lang/py-minimal.json @@ -1,15 +1,22 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://m.example.com" - } - ], - "capabilities": [], - "payment_handlers": [], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://m.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": {}, + "name": "Minimal Merchant" + }, "signing_keys": [ { "kid": "py-minimal-EdDSA", @@ -17,17 +24,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "Jal9VgyjKMP3MguusxxOZJDOb6U7nToLMa7C3hCqu2o" + "x": "dZ6PLK4BfgrHTuRA0klbkcl6iHAXhyX3ACjRefxb8IA" } ], - "name": "Minimal Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJKYWw5Vmd5aktNUDNNZ3V1c3h4T1pKRE9iNlU3blRvTE1hN0MzaENxdTJvIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.isXdp4CcyRr9lh9yHQwjAH-MDgj6ZqUfJjLr3rj6KTBaE_nRIjR_HO4hM44uMnh8RUYm5-JCNVh8m1-lsLGxDQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiZFo2UExLNEJmZ3JIVHVSQTBrbGJrY2w2aUhBWGh5WDNBQ2pSZWZ4YjhJQSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJNaW5pbWFsIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6e30sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9tLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.axue3k1ojtSWw0pZJbuDmx-HBt6DZTwtbD3DiHKwrrP3YSWjdlp_FBfBMT0jA-oQ6HqfdQ4fO9vuRAAIpBepCw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "Jal9VgyjKMP3MguusxxOZJDOb6U7nToLMa7C3hCqu2o", + "x": "dZ6PLK4BfgrHTuRA0klbkcl6iHAXhyX3ACjRefxb8IA", "kid": "py-minimal-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-multikey.json b/tests/fixtures/cross-lang/py-multikey.json index 5333038..514eaba 100644 --- a/tests/fixtures/cross-lang/py-multikey.json +++ b/tests/fixtures/cross-lang/py-multikey.json @@ -1,22 +1,34 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://mk.example.com" - } - ], - "capabilities": [], - "payment_handlers": [ - { - "name": "tempo", - "config": { - "rail": "tempo-mainnet" - } - } - ], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://mk.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "rail": "tempo-mainnet" + } + } + ] + }, + "name": "Multi-Key Merchant" + }, "signing_keys": [ { "kid": "py-multikey-old", @@ -24,7 +36,7 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "Jk-XKLK-B6PQDrH5muAusj5s64a_jYGScTY7sR5zwTU" + "x": "lW7nqnsPzl7FVllMcMjTSHmAqaMVeBMJk4mEwgfY5Vo" }, { "kid": "py-multikey-new", @@ -32,17 +44,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "0k8GV3ctomf1kxJIdg8cIpFDrFdWBE6fNW1kRp5j_ew" + "x": "Kmwcte5hHWi17aQjekr9Zdw6fsBQl237_jllIAJBMnk" } ], - "name": "Multi-Key Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1vbGQiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiSmstWEtMSy1CNlBRRHJINW11QXVzajVzNjRhX2pZR1NjVFk3c1I1endUVSJ9LHsiYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjBrOEdWM2N0b21mMWt4SklkZzhjSXBGRHJGZFdCRTZmTlcxa1JwNWpfZXcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.5sp4yAhsPgmO6F4CvLSADiJ78rk5_KO83r1NkJYKg2bB3flvz1RdkwpodiH66wKOu8XOEDx2Xmr9_RWUb2W4DA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJsVzducW5zUHpsN0ZWbGxNY01qVFNIbUFxYU1WZUJNSms0bUV3Z2ZZNVZvIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1uZXciLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiS213Y3RlNWhIV2kxN2FRamVrcjlaZHc2ZnNCUWwyMzdfamxsSUFKQk1uayJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJNdWx0aS1LZXkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL21rLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.gBimQYPBcvQFutbEzKeJrLzjrkgqyClkbRuSVOaRAfzAvUsxZ5Zse1WmqhadHzv5DUZohfBiWUHjj96kToOPDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "Jk-XKLK-B6PQDrH5muAusj5s64a_jYGScTY7sR5zwTU", + "x": "lW7nqnsPzl7FVllMcMjTSHmAqaMVeBMJk4mEwgfY5Vo", "kid": "py-multikey-old", "alg": "EdDSA", "use": "sig", @@ -50,7 +61,7 @@ }, { "crv": "Ed25519", - "x": "0k8GV3ctomf1kxJIdg8cIpFDrFdWBE6fNW1kRp5j_ew", + "x": "Kmwcte5hHWi17aQjekr9Zdw6fsBQl237_jllIAJBMnk", "kid": "py-multikey-new", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-typed-claims.json b/tests/fixtures/cross-lang/py-typed-claims.json index 22daba2..a486c17 100644 --- a/tests/fixtures/cross-lang/py-typed-claims.json +++ b/tests/fixtures/cross-lang/py-typed-claims.json @@ -1,31 +1,44 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://t.example.com" - } - ], - "capabilities": [ - { - "name": "sh.agentscore.identity", - "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", - "version": "1", - "claims": { - "operator_id": "op_typed_claims", - "kyc_level": "enhanced", - "sanctions_clear": true, - "age_bracket": "21+", - "jurisdiction": "US", - "verified_at": "2026-04-01T00:00:00Z", - "verify_url": "https://agentscore.sh/verify/op_typed_claims", - "issuer": "https://agentscore.sh" - } - } - ], - "payment_handlers": [], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://t.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": { + "sh.agentscore.identity": [ + { + "version": "1", + "spec": "https://agentscore.sh/specification/identity", + "schema": "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", + "extends": [ + "dev.ucp.shopping.checkout", + "dev.ucp.shopping.cart" + ], + "claims": { + "operator_id": "op_typed_claims", + "kyc_level": "enhanced", + "sanctions_clear": true, + "age_bracket": "21+", + "jurisdiction": "US", + "verified_at": "2026-04-01T00:00:00Z", + "verify_url": "https://agentscore.sh/verify/op_typed_claims", + "issuer": "https://agentscore.sh" + } + } + ] + }, + "payment_handlers": {}, + "name": "Typed Claims Merchant" + }, "signing_keys": [ { "kid": "py-typed-claims-EdDSA", @@ -33,17 +46,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "wNeB1hL1l7cml2x2miyjUChAxvveRYkuuMug1XJkq64" + "x": "clSTIoRWvV4whYX40RYSSPGfcj2mL3YW-IkgYYM6SLQ" } ], - "name": "Typed Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IndOZUIxaEwxbDdjbWwyeDJtaXlqVUNoQXh2dmVSWWt1dU11ZzFYSmtxNjQifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.RJjoaDeclNkgVS18FQ4zS7vpoyXV3p0tO1J2YCnrqxe6qM6XQtuCyl90zJQXXFvdz41EEC9et7ZquSUviY2BDQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJjbFNUSW9SV3ZWNHdoWVg0MFJZU1NQR2ZjajJtTDNZVy1Ja2dZWU02U0xRIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJleHRlbmRzIjpbImRldi51Y3Auc2hvcHBpbmcuY2hlY2tvdXQiLCJkZXYudWNwLnNob3BwaW5nLmNhcnQiXSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIxIn1dfSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vdC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.0BQic1wyTNOk4TVcs2dJ6iRARokGtSjzMzbP9myAlMdF8zpnXfWAzZ6MwUsmH10eK7PQtRrj5D-St4_xxw6SBw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "wNeB1hL1l7cml2x2miyjUChAxvveRYkuuMug1XJkq64", + "x": "clSTIoRWvV4whYX40RYSSPGfcj2mL3YW-IkgYYM6SLQ", "kid": "py-typed-claims-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/fixtures/cross-lang/py-unicode.json b/tests/fixtures/cross-lang/py-unicode.json index 8a2154a..8d27413 100644 --- a/tests/fixtures/cross-lang/py-unicode.json +++ b/tests/fixtures/cross-lang/py-unicode.json @@ -1,22 +1,34 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://日本.example.com" - } - ], - "capabilities": [], - "payment_handlers": [ - { - "name": "tempo", - "config": { - "note": "メモ" - } - } - ], + "ucp": { + "version": "2026-04-17", + "services": { + "dev.ucp.shopping": [ + { + "version": "2026-04-08", + "spec": "https://ucp.dev/2026-04-08/specification/overview", + "transport": "mcp", + "endpoint": "https://日本.example.com/api/ucp/mcp", + "schema": "https://ucp.dev/services/shopping/openrpc.json" + } + ] + }, + "capabilities": {}, + "payment_handlers": { + "sh.agentscore.payment.tempo": [ + { + "id": "tempo", + "version": "2026-04-08", + "spec": "https://agentscore.sh/specification/payment-handlers/tempo", + "schema": "https://agentscore.sh/schemas/payment-handlers/tempo.json", + "config": { + "note": "メモ" + } + } + ] + }, + "name": "Café 日本 🍷 Merchant" + }, "signing_keys": [ { "kid": "py-unicode-EdDSA", @@ -24,17 +36,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "zYxsKbVzECrjQUcCCoXQRJ4_PPA5512kajrQHLUu8nE" + "x": "Rk_x9yyAht9Xy_mKxxmdh0kyr12andlLUGHY2xh8-3w" } ], - "name": "Café 日本 🍷 Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4Ijoiell4c0tiVnpFQ3JqUVVjQ0NvWFFSSjRfUFBBNTUxMmthanJRSExVdThuRSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.q50JBjCcGT3wY0CdcAatQfVlo3K6o_SnEm9UQUPYdSApQwBT8pLofmATVdMoX0NZlr6TbIMaU29EEVl8XjzuBQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiUmtfeDl5eUFodDlYeV9tS3h4bWRoMGt5cjEyYW5kbExVR0hZMnhoOC0zdyJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJDYWbDqSDml6XmnKwg8J-NtyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly_ml6XmnKwuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In19.fraS8Y7ecHdldvmqwIdCzvSlBqi2GvYatX4UmSnR0jBnKDY8qxQnfYErAbJQ8ywXnP8Ztsp7PvbaRd90GIZ0CQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "zYxsKbVzECrjQUcCCoXQRJ4_PPA5512kajrQHLUu8nE", + "x": "Rk_x9yyAht9Xy_mKxxmdh0kyr12andlLUGHY2xh8-3w", "kid": "py-unicode-EdDSA", "alg": "EdDSA", "use": "sig", From 56aa9ae50a61739b9b09e1927881171d267299e2 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 06:29:15 -0700 Subject: [PATCH 31/35] docs(claude): clarify signed-ucp-merchant example is vendor-extension, not UCP-required MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- CLAUDE.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CLAUDE.md b/CLAUDE.md index 829de41..5b3fcea 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,7 +47,7 @@ Peer-dep pattern: payment/x402/mppx/stripe modules `dynamic import` at runtime, | `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`. | -| `signed-ucp-merchant.ts` | Signed UCP profile (`/.well-known/ucp`) + JWKS endpoint (`/.well-known/jwks.json`) for UCP §6 trust-mode verifiers. Wires ephemeral-for-dev / env-JWK-for-prod signing, kid rotation, and `Cache-Control` posture. Uses `generateUCPSigningKey`, `signUCPProfile`, `buildJWKSResponse`, `ucpSigningKeyFromJWK`, `UCPVerificationError`. | +| `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 From 27914952b4f296587c078b8324b1fa395bd87e4a Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 06:42:15 -0700 Subject: [PATCH 32/35] fix(examples): auto-detect alg from JWK shape (parity with python sibling) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- examples/signed-ucp-merchant.ts | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/examples/signed-ucp-merchant.ts b/examples/signed-ucp-merchant.ts index ba4c2a1..90768ee 100644 --- a/examples/signed-ucp-merchant.ts +++ b/examples/signed-ucp-merchant.ts @@ -59,11 +59,19 @@ function loadSigningKey(): Promise { `Failed to parse UCP_SIGNING_KEY_JWK_PRIVATE as JSON: ${err instanceof Error ? err.message : String(err)}`, ); } - const privateKey = (await importJWK(jwk, ALG)) as CryptoKey; - // Re-export to derive a clean public-only JWK (drops `d` + any RSA private fields). + // Detect alg from JWK shape (parity with python sibling); ignore env ALG if it conflicts. + let effectiveAlg: 'EdDSA' | 'ES256'; + if (jwk.kty === 'OKP' && jwk.crv === 'Ed25519') { + effectiveAlg = 'EdDSA'; + } else if (jwk.kty === 'EC' && jwk.crv === 'P-256') { + effectiveAlg = 'ES256'; + } else { + throw new Error(`Unsupported env JWK: kty=${jwk.kty} crv=${jwk.crv}`); + } + const privateKey = (await importJWK(jwk, effectiveAlg)) as CryptoKey; const publicJWK = (await exportJWK(privateKey)) as JWK; publicJWK.kid = jwk.kid ?? KID; - publicJWK.alg = ALG; + publicJWK.alg = effectiveAlg; publicJWK.use = 'sig'; delete (publicJWK as Record).d; return { privateKey, publicJWK } as GeneratedUCPKey; From ac601c3764aea945b0120813e3868854c70f6fea Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 07:06:31 -0700 Subject: [PATCH 33/35] feat(a2a): add UCP extension declaration to A2A agent card builder MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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) --- src/identity/a2a.ts | 38 ++++++++++++++++++++++++++++++ src/index.ts | 3 +++ tests/identity/a2a.test.ts | 47 +++++++++++++++++++++++++++++++++++++- 3 files changed, 87 insertions(+), 1 deletion(-) diff --git a/src/identity/a2a.ts b/src/identity/a2a.ts index 9d126e4..d312711 100644 --- a/src/identity/a2a.ts +++ b/src/identity/a2a.ts @@ -23,6 +23,38 @@ export interface A2AAgentCardCapabilities { skills?: string[]; } +/** Per A2A v1.0: an entry in the card's top-level `extensions` array. UCP support + * is declared this way (UCP §A2A binding requires `https://ucp.dev/2026-04-08/specification/reference`). */ +export interface A2AAgentCardExtension { + /** Canonical extension URI — for UCP, `https://ucp.dev/2026-04-08/specification/reference`. */ + uri: string; + /** Extension-specific params. UCP places `{ capabilities: { "": [{ version: "..." }, ...] } }` here. */ + params?: Record; +} + +/** Canonical UCP A2A extension URI — verifiers look for this exact URI in `extensions[]` + * to detect UCP support on the agent card. Pinned to the 2026-04-08 spec snapshot. */ +export const UCP_A2A_EXTENSION_URI = 'https://ucp.dev/2026-04-08/specification/reference'; + +/** Build the canonical UCP entry for an A2A agent card's `extensions[]` array. + * + * Per UCP §A2A binding: "Businesses supporting UCP must advertise the extension and + * any optional capabilities in their A2A Agent Card to allow platforms to activate + * the extension." Pass the `capabilities` map keyed by reverse-DNS service/capability + * name (e.g. `dev.ucp.shopping.checkout`), each value a list of `{ version }` records. + * Pass `{}` (or omit) when you serve UCP at the discovery layer but have no formal + * capability bindings yet — vendors that haven't implemented checkout/cart/etc. should + * declare the extension URI without claiming capabilities they don't service. + */ +export function ucpA2AExtension( + capabilities: Record> = {}, +): A2AAgentCardExtension { + return { + uri: UCP_A2A_EXTENSION_URI, + params: { capabilities }, + }; +} + export interface A2AAgentCardIdentity { /** Issuer of the identity claims — always `"https://agentscore.sh"` for the AgentScore-issued card. */ issuer: string; @@ -55,6 +87,8 @@ export interface A2AAgentCard { url?: string; /** Agent capabilities — endpoints + skills. */ capabilities?: A2AAgentCardCapabilities; + /** A2A v1.0 extensions array. Use `ucpA2AExtension()` to add the UCP entry. */ + extensions?: A2AAgentCardExtension[]; /** AgentScore identity claims. Empty `null` when no identity is available (pre-KYC). */ identity: A2AAgentCardIdentity | null; /** Vendor-specific extras merged at the top level. */ @@ -70,6 +104,9 @@ export interface BuildA2AAgentCardInput { url?: string; /** Capabilities — endpoints exposed + skill tags. */ capabilities?: A2AAgentCardCapabilities; + /** A2A v1.0 extensions to declare on the card. Build the UCP entry with + * `ucpA2AExtension()`. Other A2A extensions can be added the same way. */ + extensions?: A2AAgentCardExtension[]; /** AgentScore assess data — what `getAgentScoreData(c)` returns or what `assess()` returned directly. * Pass `null` to emit a card with no identity claims (publishable but unverified). */ data?: AgentScoreData | null; @@ -148,6 +185,7 @@ export function buildA2AAgentCard(input: BuildA2AAgentCardInput): A2AAgentCard { if (input.description !== undefined) card.description = input.description; if (input.url !== undefined) card.url = input.url; if (input.capabilities !== undefined) card.capabilities = input.capabilities; + if (input.extensions !== undefined) card.extensions = input.extensions; if (input.extras !== undefined) card.extras = input.extras; return card; } diff --git a/src/index.ts b/src/index.ts index d40ac5d..7cd12ba 100644 --- a/src/index.ts +++ b/src/index.ts @@ -25,8 +25,11 @@ export { export { denialReasonToBody } from './_response'; export { buildA2AAgentCard, + ucpA2AExtension, + UCP_A2A_EXTENSION_URI, type A2AAgentCard, type A2AAgentCardCapabilities, + type A2AAgentCardExtension, type A2AAgentCardIdentity, type BuildA2AAgentCardInput, } from './identity/a2a'; diff --git a/tests/identity/a2a.test.ts b/tests/identity/a2a.test.ts index 03656ae..66f0073 100644 --- a/tests/identity/a2a.test.ts +++ b/tests/identity/a2a.test.ts @@ -1,5 +1,9 @@ import { describe, expect, it } from 'vitest'; -import { buildA2AAgentCard } from '../../src/identity/a2a'; +import { + UCP_A2A_EXTENSION_URI, + buildA2AAgentCard, + ucpA2AExtension, +} from '../../src/identity/a2a'; import type { AgentScoreData } from '../../src/core'; const fullData: AgentScoreData = { @@ -129,3 +133,44 @@ describe('buildA2AAgentCard', () => { expect(card.identity?.verify_url).toBe('https://from-data.example/verify'); }); }); + +describe('UCP A2A extension', () => { + it('exports the canonical UCP A2A extension URI pinned to 2026-04-08', () => { + expect(UCP_A2A_EXTENSION_URI).toBe('https://ucp.dev/2026-04-08/specification/reference'); + }); + + it('ucpA2AExtension() with no args produces empty-capabilities entry', () => { + const ext = ucpA2AExtension(); + expect(ext.uri).toBe(UCP_A2A_EXTENSION_URI); + expect(ext.params).toEqual({ capabilities: {} }); + }); + + it('ucpA2AExtension(map) wraps the capabilities map under params.capabilities', () => { + const ext = ucpA2AExtension({ + 'dev.ucp.shopping.checkout': [{ version: '2026-04-08' }], + 'dev.ucp.shopping.cart': [{ version: '2026-04-08' }], + }); + expect(ext.params).toEqual({ + capabilities: { + 'dev.ucp.shopping.checkout': [{ version: '2026-04-08' }], + 'dev.ucp.shopping.cart': [{ version: '2026-04-08' }], + }, + }); + }); + + it('buildA2AAgentCard emits extensions[] when passed', () => { + const card = buildA2AAgentCard({ + name: 'X', + data: null, + extensions: [ucpA2AExtension()], + }); + expect(card.extensions).toHaveLength(1); + expect(card.extensions?.[0]?.uri).toBe(UCP_A2A_EXTENSION_URI); + expect(card.extensions?.[0]?.params).toEqual({ capabilities: {} }); + }); + + it('buildA2AAgentCard omits extensions[] when not passed', () => { + const card = buildA2AAgentCard({ name: 'X', data: null }); + expect(card.extensions).toBeUndefined(); + }); +}); From 495a2f5d1fa9b17f28a5f08c3a21ee03d029eba8 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 07:16:27 -0700 Subject: [PATCH 34/35] fix(identity): cross-lang parity tightening from spec audit 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) --- README.md | 9 ++++++--- src/identity/a2a.ts | 2 +- src/identity/ucp.ts | 3 +++ tests/identity/a2a.test.ts | 7 +++++++ tests/identity/ucp-signing.test.ts | 8 ++++++++ 5 files changed, 25 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 2a64a4e..23d5377 100644 --- a/README.md +++ b/README.md @@ -190,10 +190,13 @@ return new Response(JSON.stringify(responseBody), { status: 402, headers }); ### Identity publishing (cross-vendor standards) ```typescript -import { buildA2AAgentCard, buildUCPProfile } from "@agent-score/commerce"; +import { buildA2AAgentCard, buildUCPProfile, ucpA2AExtension } from "@agent-score/commerce"; -// Google A2A v1.0 Signed Agent Card; publish at /.well-known/agent-card.json -const card = buildA2AAgentCard({ name, url, capabilities, data: assess }); +// Google A2A v1.0 Signed Agent Card; publish at /.well-known/agent-card.json. +// Per UCP §A2A binding, the card MUST declare the canonical UCP extension URI; +// pass `ucpA2AExtension()` with empty capabilities until you bind formal UCP +// capabilities (dev.ucp.shopping.checkout, etc.). +const card = buildA2AAgentCard({ name, url, capabilities, extensions: [ucpA2AExtension()], data: assess }); // Google Universal Commerce Protocol; publish at /.well-known/ucp // Output shape: { ucp: { version, services, capabilities, payment_handlers, diff --git a/src/identity/a2a.ts b/src/identity/a2a.ts index d312711..4b358f4 100644 --- a/src/identity/a2a.ts +++ b/src/identity/a2a.ts @@ -185,7 +185,7 @@ export function buildA2AAgentCard(input: BuildA2AAgentCardInput): A2AAgentCard { if (input.description !== undefined) card.description = input.description; if (input.url !== undefined) card.url = input.url; if (input.capabilities !== undefined) card.capabilities = input.capabilities; - if (input.extensions !== undefined) card.extensions = input.extensions; + if (input.extensions && input.extensions.length > 0) card.extensions = input.extensions; if (input.extras !== undefined) card.extras = input.extras; return card; } diff --git a/src/identity/ucp.ts b/src/identity/ucp.ts index 8735a64..f034621 100644 --- a/src/identity/ucp.ts +++ b/src/identity/ucp.ts @@ -65,6 +65,9 @@ export function ucpSigningKeyFromJWK(jwk: Record): UCPSigningKe `ucpSigningKeyFromJWK: kty=${JSON.stringify(jwk.kty)} is not a supported asymmetric key type (expected OKP, EC, or RSA). Symmetric \`oct\` keys are rejected because they cannot publicly verify a JWS in the trust-mode UCP flow.`, ); } + if ((jwk.kty === 'EC' || jwk.kty === 'OKP') && (typeof jwk.crv !== 'string' || !jwk.crv)) { + throw new Error(`ucpSigningKeyFromJWK: kty=${jwk.kty} requires a non-empty \`crv\` field (e.g., "P-256" for EC, "Ed25519" for OKP).`); + } return jwk as unknown as UCPSigningKey; } diff --git a/tests/identity/a2a.test.ts b/tests/identity/a2a.test.ts index 66f0073..869882b 100644 --- a/tests/identity/a2a.test.ts +++ b/tests/identity/a2a.test.ts @@ -173,4 +173,11 @@ describe('UCP A2A extension', () => { const card = buildA2AAgentCard({ name: 'X', data: null }); expect(card.extensions).toBeUndefined(); }); + + it('buildA2AAgentCard omits extensions[] when passed an empty array (parity with python)', () => { + // Python's to_dict skips `extensions` when empty; node skips the same way so + // cross-language profiles canonicalize to identical bytes when both omit. + const card = buildA2AAgentCard({ name: 'X', data: null, extensions: [] }); + expect(card.extensions).toBeUndefined(); + }); }); diff --git a/tests/identity/ucp-signing.test.ts b/tests/identity/ucp-signing.test.ts index 5de7946..e9ddac0 100644 --- a/tests/identity/ucp-signing.test.ts +++ b/tests/identity/ucp-signing.test.ts @@ -446,6 +446,14 @@ describe('ucpSigningKeyFromJWK', () => { expect(() => ucpSigningKeyFromJWK('string' as never)).toThrow(); expect(() => ucpSigningKeyFromJWK(42 as never)).toThrow(); }); + + it('rejects EC JWK missing crv', () => { + expect(() => ucpSigningKeyFromJWK({ kid: 'k', kty: 'EC' })).toThrow(/crv/); + }); + + it('rejects OKP JWK with empty crv', () => { + expect(() => ucpSigningKeyFromJWK({ kid: 'k', kty: 'OKP', crv: '' })).toThrow(/crv/); + }); }); describe('UCP signing — JCS-incompatible value rejection', () => { From 4315fdecaca9de823cc26343697f0e62502053fd Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 12:04:58 -0700 Subject: [PATCH 35/35] chore(deps): bump mppx 0.6.16 -> 0.6.17 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) --- bun.lock | 10 +++++++--- package.json | 2 +- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/bun.lock b/bun.lock index 2ec7788..33bff2c 100644 --- a/bun.lock +++ b/bun.lock @@ -27,7 +27,7 @@ "hono": "^4.12.18", "jose": "^5.9.0", "lefthook": "^2.1.6", - "mppx": "^0.6.16", + "mppx": "^0.6.17", "tsup": "^8.5.1", "typescript": "^6.0.3", "typescript-eslint": "^8.59.2", @@ -932,7 +932,7 @@ "mlly": ["mlly@1.8.2", "", { "dependencies": { "acorn": "^8.16.0", "pathe": "^2.0.3", "pkg-types": "^1.3.1", "ufo": "^1.6.3" } }, "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA=="], - "mppx": ["mppx@0.6.16", "", { "dependencies": { "incur": "^0.4.5", "ox": "0.14.20", "zod": "^4.3.6" }, "peerDependencies": { "@modelcontextprotocol/sdk": ">=1.25.0", "elysia": ">=1", "express": ">=5", "hono": ">=4.12.16", "viem": ">=2.47.5" }, "optionalPeers": ["@modelcontextprotocol/sdk", "elysia", "express", "hono"], "bin": { "mppx": "dist/bin.js", "mppx.src": "src/bin.ts" } }, "sha512-gByr5oM0vfbJqh3S7e3WLLjE3pjniZShv2kyFdzl3aav1ejH8rkjVqKMJ92nuQWkDw+2QZgzYD5PcbnqzUPZjA=="], + "mppx": ["mppx@0.6.17", "", { "dependencies": { "incur": "^0.4.5", "ox": "0.14.18", "zod": "^4.3.6" }, "peerDependencies": { "@modelcontextprotocol/sdk": ">=1.25.0", "elysia": ">=1", "express": ">=5", "hono": ">=4.12.18", "viem": ">=2.47.5" }, "optionalPeers": ["@modelcontextprotocol/sdk", "elysia", "express", "hono"], "bin": { "mppx": "./dist/bin.js", "mppx.src": "./src/bin.ts" } }, "sha512-qGATWL6BFE0J1rsZj/pXWapKRKlqgZvZNui/2hGHeifVoftxYcBesHLs0NNplFF5mPfVSAOqbemoz358YOUlPQ=="], "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], @@ -974,7 +974,7 @@ "own-keys": ["own-keys@1.0.1", "", { "dependencies": { "get-intrinsic": "^1.2.6", "object-keys": "^1.1.1", "safe-push-apply": "^1.0.0" } }, "sha512-qFOyK5PjiWZd+QQIh+1jhdb9LpxTF0qs7Pm8o5QHYZ0M3vKqSqzsZaEB6oWlxZ+q2sJBMI/Ktgd2N5ZwQoRHfg=="], - "ox": ["ox@0.14.20", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-rby38C3nDn8eQkf29Zgw4hkCZJ64Qqi0zRPWL8ENUQ7JVuoITqrVtwWQgM/He19SCMUEc7hS/Sjw0jIOSLJhOw=="], + "ox": ["ox@0.14.18", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-1Irk/tvMsw7xJDuCTT/u9azSjz0YX9hrYFgJOacIuFwibaW2zZBXAMrpzegndYb5o8GLpxB6/0qro4/c40q6VQ=="], "p-limit": ["p-limit@3.1.0", "", { "dependencies": { "yocto-queue": "^0.1.0" } }, "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ=="], @@ -1322,6 +1322,8 @@ "viem/abitype": ["abitype@1.2.3", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-Ofer5QUnuUdTFsBRwARMoWKOH1ND5ehwYhJ3OJ/BQO+StkwQjHw0XyVh4vDttzHB7QOFhPHa/o413PJ82gU/Tg=="], + "viem/ox": ["ox@0.14.20", "", { "dependencies": { "@adraffy/ens-normalize": "^1.11.0", "@noble/ciphers": "^1.3.0", "@noble/curves": "1.9.1", "@noble/hashes": "^1.8.0", "@scure/bip32": "^1.7.0", "@scure/bip39": "^1.6.0", "abitype": "^1.2.3", "eventemitter3": "5.0.1" }, "peerDependencies": { "typescript": ">=5.4.0" }, "optionalPeers": ["typescript"] }, "sha512-rby38C3nDn8eQkf29Zgw4hkCZJ64Qqi0zRPWL8ENUQ7JVuoITqrVtwWQgM/He19SCMUEc7hS/Sjw0jIOSLJhOw=="], + "vitest/tinyexec": ["tinyexec@1.1.1", "", {}, "sha512-VKS/ZaQhhkKFMANmAOhhXVoIfBXblQxGX1myCQ2faQrfmobMftXeJPcZGp0gS07ocvGJWDLZGyOZDadDBqYIJg=="], "@coinbase/cdp-sdk/@solana/kit/@solana/accounts": ["@solana/accounts@5.5.1", "", { "dependencies": { "@solana/addresses": "5.5.1", "@solana/codecs-core": "5.5.1", "@solana/codecs-strings": "5.5.1", "@solana/errors": "5.5.1", "@solana/rpc-spec": "5.5.1", "@solana/rpc-types": "5.5.1" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-TfOY9xixg5rizABuLVuZ9XI2x2tmWUC/OoN556xwfDlhBHBjKfszicYYOyD6nbFmwTGYarCmyGIdteXxTXIdhQ=="], @@ -1402,6 +1404,8 @@ "typescript-eslint/@typescript-eslint/eslint-plugin/ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + "viem/ox/abitype": ["abitype@1.2.4", "", { "peerDependencies": { "typescript": ">=5.0.4", "zod": "^3.22.0 || ^4.0.0" }, "optionalPeers": ["typescript", "zod"] }, "sha512-dpKH+N27vRjarMVTFFkeY445VTKftzGWpL0FiT7xmVmzQRKazZexzC5uHG0f6XKsVLAuUlndnbGau6lRejClxg=="], + "@coinbase/cdp-sdk/@solana/kit/@solana/accounts/@solana/codecs-core": ["@solana/codecs-core@5.5.1", "", { "dependencies": { "@solana/errors": "5.5.1" }, "peerDependencies": { "typescript": "^5.0.0" }, "optionalPeers": ["typescript"] }, "sha512-TgBt//bbKBct0t6/MpA8ElaOA3sa8eYVvR7LGslCZ84WiAwwjCY0lW/lOYsFHJQzwREMdUyuEyy5YWBKtdh8Rw=="], "@coinbase/cdp-sdk/@solana/kit/@solana/accounts/@solana/codecs-strings": ["@solana/codecs-strings@5.5.1", "", { "dependencies": { "@solana/codecs-core": "5.5.1", "@solana/codecs-numbers": "5.5.1", "@solana/errors": "5.5.1" }, "peerDependencies": { "fastestsmallesttextencoderdecoder": "^1.0.22", "typescript": "^5.0.0" }, "optionalPeers": ["fastestsmallesttextencoderdecoder", "typescript"] }, "sha512-7klX4AhfHYA+uKKC/nxRGP2MntbYQCR3N6+v7bk1W/rSxYuhNmt+FN8aoThSZtWIKwN6BEyR1167ka8Co1+E7A=="], diff --git a/package.json b/package.json index 1e848f6..62e27ba 100644 --- a/package.json +++ b/package.json @@ -178,7 +178,7 @@ "hono": "^4.12.18", "jose": "^5.9.0", "lefthook": "^2.1.6", - "mppx": "^0.6.16", + "mppx": "^0.6.17", "tsup": "^8.5.1", "typescript": "^6.0.3", "typescript-eslint": "^8.59.2",