From 0884d10625171912804c916327069215fc683173 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 10:58:44 -0700 Subject: [PATCH 01/37] feat(identity): UCP profile signing helpers (joserfc optional extra) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add four helpers for UCP §6 trust-mode profiles: generate_ucp_signing_key, sign_ucp_profile, verify_ucp_profile, build_jwks_response. 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 @agent-score/commerce Node SDK: profiles signed by either flavor verify in the other. Bumps to 1.3.7. joserfc is an optional extra (`pip install agentscore-commerce[ucp]`); 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 | 17 +- agentscore_commerce/discovery/robots_tag.py | 1 + agentscore_commerce/identity/__init__.py | 12 ++ agentscore_commerce/identity/ucp_jwks.py | 224 ++++++++++++++++++++ pyproject.toml | 4 +- tests/test_ucp_jwks.py | 139 ++++++++++++ uv.lock | 22 +- 7 files changed, 415 insertions(+), 4 deletions(-) create mode 100644 agentscore_commerce/identity/ucp_jwks.py create mode 100644 tests/test_ucp_jwks.py diff --git a/README.md b/README.md index 33cd918..25ceb70 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ pip install 'agentscore-commerce[fastapi,x402,coinbase]' | `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware: KYC, sanctions, age, jurisdiction. `AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic), `get_assess_data(...)`, `capture_wallet(...)`, `verify_wallet_signer_match(...)`. | | `agentscore_commerce.identity` (package level) | Re-exports the denial helpers: `denial_reason_status`, `denial_reason_to_body`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `verification_agent_instructions`, `is_fixable_denial`, `FIXABLE_DENIAL_REASONS`. Also re-exports the per-product policy helpers: `PolicyBlock`, `GateResult`, `EnforcementMode`, `IdentityStatus`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed` — for multi-product merchants where each product carries its own compliance config (hard gate vs soft vs none, per-product shipping allowlists). | | `agentscore_commerce.payment` | `networks`, `USDC`, `rails` registries; `payment_directive`, `build_payment_directive`, `www_authenticate_header`, `payment_required_header`, `alias_amount_fields` (v1↔v2 amount field shim — emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlement_override_header`, `dispatch_settlement_by_network`, `extract_payment_signer` (returns `PaymentSigner({address, network})`), `register_x402_schemes_v1_v2`; drop-in x402 helpers: `validate_x402_network_config` (boot-time guard), `verify_x402_request` (parse + validate inbound X-Payment), `process_x402_settle` (verify-then-settle with one call), `classify_x402_settle_result` (maps the tagged settle result to a recommended HTTP status / code / next_steps so merchants get a controlled envelope without coupling to facilitator-specific error text). | -| `agentscore_commerce.discovery` | `is_discovery_probe_request`, `build_discovery_probe_response` (with optional `x402_sample` for x402-aware crawlers — `awal x402 details` etc.), `sample_x402_accept_for_network` (USDC sample-accept builder for known CAIP-2 networks), `build_well_known_mpp`, `build_llms_txt` + `llms_txt_identity_section` + `llms_txt_payment_section` (compact + verbose modes), `build_skill_md` (Claude-Skill-compatible `/skill.md` agent-discovery manifest — strictly agent-facing data only, no internal posture), `agentscore_openapi_snippets`, `build_bazaar_discovery_payload`, `NoindexNonDiscoveryMiddleware` (ASGI 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 `is_discovery_path` + `DEFAULT_DISCOVERY_PATHS` for non-ASGI frameworks). | +| `agentscore_commerce.discovery` | `is_discovery_probe_request`, `build_discovery_probe_response` (with optional `x402_sample` for x402-aware crawlers — `awal x402 details` etc.), `sample_x402_accept_for_network` (USDC sample-accept builder for known CAIP-2 networks), `build_well_known_mpp`, `build_llms_txt` + `llms_txt_identity_section` + `llms_txt_payment_section` (compact + verbose modes), `build_skill_md` (Claude-Skill-compatible `/skill.md` agent-discovery manifest — strictly agent-facing data only, no internal posture), `agentscore_openapi_snippets`, `build_bazaar_discovery_payload`, `NoindexNonDiscoveryMiddleware` (ASGI 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 `is_discovery_path` + `DEFAULT_DISCOVERY_PATHS` for non-ASGI frameworks). | | `agentscore_commerce.challenge` | `build_402_body`, `build_accepted_methods`, `build_identity_metadata`, `build_how_to_pay`, `build_agent_instructions` (auto-emits per-rail `compatible_clients` — smoke-verified CLIs the agent should use; vendor override supported), `build_pricing_block` (cents → dollar-string with optional shipping/tax), `first_encounter_agent_memory` (cross-merchant hint, returns the canonical block or `None` based on a per-merchant first-seen flag), `OrderReceipt` (dataclass for the post-settlement 200 response shape); `respond_402` — drop-in 402 emit that preserves pympp's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `build_validation_error` — 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. | | `agentscore_commerce.stripe_multichain` | `create_multichain_payment_intent`, `get_deposit_address`, `simulate_crypto_deposit`; `create_pi_cache` (TTL'd PI / deposit-address cache, Redis-backed when `redis_url` set, in-memory otherwise), `simulate_deposit_if_test_mode` (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`. | | `agentscore_commerce.api` | Everything from `agentscore-py` re-exported in one place: `AgentScore` + `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` + `is_agentscore_test_address`. **Don't add `agentscore-py` as a separate dep** — the two can drift versions and cause subtle type mismatches. | @@ -202,6 +202,21 @@ profile = build_ucp_profile( ) ``` +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 `joserfc` extra (`pip install agentscore-commerce[ucp]`): + +```python +from agentscore_commerce.identity import ( + build_jwks_response, + generate_ucp_signing_key, + sign_ucp_profile, + verify_ucp_profile, +) + +key = generate_ucp_signing_key(kid="merchant-2026-05") +signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=key.public_jwk["kid"], alg="EdDSA") +jwks = build_jwks_response([key.public_jwk]) +``` + ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol with no identity-publishing surface — ACP merchants integrate via the existing `build_402_body` + `build_payment_headers` + Stripe SPT rail. ## Stripe multichain diff --git a/agentscore_commerce/discovery/robots_tag.py b/agentscore_commerce/discovery/robots_tag.py index 96b1857..cdf0763 100644 --- a/agentscore_commerce/discovery/robots_tag.py +++ b/agentscore_commerce/discovery/robots_tag.py @@ -27,6 +27,7 @@ "/.well-known/x402", "/.well-known/agent-card.json", "/.well-known/ucp", + "/.well-known/jwks.json", "/favicon.png", "/favicon.ico", } diff --git a/agentscore_commerce/identity/__init__.py b/agentscore_commerce/identity/__init__.py index b4f22fb..42f1c27 100644 --- a/agentscore_commerce/identity/__init__.py +++ b/agentscore_commerce/identity/__init__.py @@ -54,6 +54,13 @@ UCPSigningKey, build_ucp_profile, ) +from agentscore_commerce.identity.ucp_jwks import ( + GeneratedUCPKey, + build_jwks_response, + generate_ucp_signing_key, + sign_ucp_profile, + verify_ucp_profile, +) # ASGI middleware is the default import (re-exported as CreateSessionOnMissing too). @@ -94,6 +101,7 @@ def _load_asgi_middleware() -> tuple[Any, Any]: "EnforcementMode", "GateClient", "GateResult", + "GeneratedUCPKey", "Grade", "Identity", "IdentityStatus", @@ -111,14 +119,18 @@ def _load_asgi_middleware() -> tuple[Any, Any]: "build_agent_memory_hint", "build_contact_support_next_steps", "build_gate_from_policy", + "build_jwks_response", "build_signer_mismatch_body", "build_ucp_profile", "denial_reason_status", "denial_reason_to_body", "extract_x402_signer", + "generate_ucp_signing_key", "is_fixable_denial", "run_gate_with_enforcement", "shipping_country_allowed", "shipping_state_allowed", + "sign_ucp_profile", "verification_agent_instructions", + "verify_ucp_profile", ] diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py new file mode 100644 index 0000000..78514a3 --- /dev/null +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -0,0 +1,224 @@ +"""UCP profile signing helpers (JWKS + JWS) — Python sibling of node-commerce. + +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: + +* :func:`generate_ucp_signing_key` — generate an Ed25519 (or ES256) keypair +* :func:`sign_ucp_profile` — sign a profile, returning a JWS-attached envelope +* :func:`verify_ucp_profile` — verify a signed profile against a JWKS +* :func:`build_jwks_response` — assemble a JWKS document for ``/.well-known/jwks.json`` + +Implementation rides on ``joserfc`` (optional extra). Install via +``pip install agentscore-commerce[ucp]``. Merchants who don't sign their profile +(development) skip this module entirely; the unsigned :func:`build_ucp_profile` +path still works. + +Cross-language API parity with ``@agent-score/commerce`` Node SDK — same canonical +body, same JWS Compact Serialization, same key-resolution semantics. Profiles +signed by Node verify in Python and vice versa. +""" + +from __future__ import annotations + +import json +from dataclasses import dataclass +from typing import Any, Literal, cast + +_JOSE_INSTALL_HINT = ( + "Install the optional dependency: `pip install agentscore-commerce[ucp]` (or `uv pip install joserfc`)." +) + + +def _load_joserfc() -> Any: + """Lazy-import joserfc so the optional dep isn't required for non-signing flows.""" + try: + import joserfc # type: ignore[import-not-found] + + return joserfc + except ImportError as exc: + msg = f"UCP signing requires the `joserfc` library, an optional dependency. {_JOSE_INSTALL_HINT}" + raise ImportError(msg) from exc + + +@dataclass +class GeneratedUCPKey: + """Output of :func:`generate_ucp_signing_key`. + + * ``private_key`` is the joserfc Key object — pass to :func:`sign_ucp_profile`. + Never publish. + * ``public_jwk`` is the JWK dict — publish at ``/.well-known/jwks.json`` and + inline in the UCP profile's ``signing_keys[]``. + """ + + private_key: Any + public_jwk: dict[str, Any] + + +def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDSA") -> GeneratedUCPKey: + """Generate an Ed25519 (default) or ES256 keypair for signing UCP profiles. + + The ``private_key`` is a joserfc ``Key`` — store it securely (env var, KMS, secret + manager) and pass to :func:`sign_ucp_profile`. + + The ``public_jwk`` is a dict you publish at ``/.well-known/jwks.json`` and inline + in the UCP profile's ``signing_keys[]`` array. + + Example:: + + from agentscore_commerce.identity.ucp_jwks import generate_ucp_signing_key + + key = generate_ucp_signing_key(kid='merchant-2026-05') + # key.private_key — persist securely + # key.public_jwk — publish at /.well-known/jwks.json + """ + joserfc = _load_joserfc() + + if alg == "EdDSA": + from joserfc.jwk import OKPKey # type: ignore[import-not-found] + + priv = OKPKey.generate_key(crv="Ed25519", parameters={"kid": kid, "alg": alg, "use": "sig"}) + elif alg == "ES256": + from joserfc.jwk import ECKey # type: ignore[import-not-found] + + priv = ECKey.generate_key(crv="P-256", parameters={"kid": kid, "alg": alg, "use": "sig"}) + else: + msg = f"Unsupported UCP signing algorithm: {alg!r}. Use 'EdDSA' or 'ES256'." + raise ValueError(msg) + + public_jwk = priv.as_dict(private=False) + # Ensure kid/alg/use are present in the exported dict (joserfc preserves params). + public_jwk.setdefault("kid", kid) + public_jwk.setdefault("alg", alg) + public_jwk.setdefault("use", "sig") + + # Quiet unused-import warning when only one branch executes. + _ = joserfc + + return GeneratedUCPKey(private_key=priv, public_jwk=public_jwk) + + +def _canonicalize_profile(profile: dict[str, Any]) -> bytes: + """Canonicalize a UCP profile body for signing. + + Removes the ``signature`` field (if present), sorts keys lexicographically at every + nesting level, returns UTF-8 JSON bytes. Cross-language byte-identical with the + Node ``stableStringify`` output. + + UCP §6.2: "the JSON-serialized profile body, with ``signature`` removed and keys + ordered lexicographically at every nesting level." + """ + stripped = {k: v for k, v in profile.items() if k != "signature"} + # ``ensure_ascii=False`` so non-ASCII characters travel as UTF-8 (matches Node's + # JSON.stringify default). ``sort_keys=True`` sorts keys at every level. Compact + # separators avoid whitespace drift. + return json.dumps(stripped, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + + +def sign_ucp_profile( + profile: dict[str, Any], + *, + signing_key: Any, + kid: str, + alg: Literal["EdDSA", "ES256"] = "EdDSA", +) -> dict[str, Any]: + """Sign a UCP profile, returning a new dict 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. + + Example:: + + profile = build_ucp_profile(..., signing_keys=[UCPSigningKey(**key.public_jwk)]) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid='merchant-2026-05') + """ + joserfc = _load_joserfc() + from joserfc import jws # type: ignore[import-not-found] + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + canonical_body = _canonicalize_profile(profile) + header = {"alg": alg, "kid": kid, "typ": "ucp-profile+jws"} + # joserfc treats EdDSA as "not recommended" by default; UCP §6 explicitly accepts + # both EdDSA and ES256, so allow both. + registry = JWSRegistry(algorithms=["EdDSA", "ES256"]) + signature = jws.serialize_compact(header, canonical_body, signing_key, registry=registry) + + _ = joserfc + + return {**profile, "signature": signature} + + +def verify_ucp_profile( + signed_profile: dict[str, Any], + jwks: dict[str, Any], +) -> bool: + """Verify a signed UCP profile against a JWKS. + + Returns ``True`` when the JWS validates against a matching key in ``jwks`` AND the + signed payload matches the canonical body of the profile-as-presented. Raises on + signature mismatch, missing key, or canonicalization drift. + + Example:: + + ok = verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + """ + joserfc = _load_joserfc() + from joserfc import jws # type: ignore[import-not-found] + from joserfc.jwk import KeySet # type: ignore[import-not-found] + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + sig = signed_profile.get("signature") + if not sig: + msg = "UCP profile has no `signature` field; expected JWS Compact Serialization." + raise ValueError(msg) + + stripped = {k: v for k, v in signed_profile.items() if k != "signature"} + expected_payload = _canonicalize_profile(stripped) + + # joserfc's KeySetSerialization type is a precise TypedDict; in practice the helper + # accepts a plain dict-of-keys at runtime, so cast at the boundary. + key_set = KeySet.import_key_set(cast("Any", jwks)) + registry = JWSRegistry(algorithms=["EdDSA", "ES256"]) + obj = jws.deserialize_compact(sig, key_set, registry=registry) + + # Compare the bytes that were actually signed against the canonical body of the + # profile we received. ``deserialize_compact`` 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. + if obj.payload != expected_payload: + msg = "UCP profile body does not match the signed payload (tampered or non-canonical)." + raise ValueError(msg) + + _ = joserfc + return True + + +def build_jwks_response(keys: list[dict[str, Any]]) -> dict[str, Any]: + """Build a JWKS document for ``/.well-known/jwks.json``. + + Example:: + + from agentscore_commerce.identity.ucp_jwks import build_jwks_response + + @app.get('/.well-known/jwks.json') + async def jwks(): + return build_jwks_response([key.public_jwk]) + """ + return {"keys": keys} + + +__all__ = [ + "GeneratedUCPKey", + "build_jwks_response", + "generate_ucp_signing_key", + "sign_ucp_profile", + "verify_ucp_profile", +] diff --git a/pyproject.toml b/pyproject.toml index ca31255..32922dc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agentscore-commerce" -version = "1.3.6" +version = "1.3.7" description = "Agent commerce SDK for Python — identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agent commerce." readme = "README.md" license = "MIT" @@ -43,6 +43,7 @@ stripe = ["stripe>=11.0.0"] x402 = ["x402[evm,fastapi]>=2.9,<3"] mppx = ["pympp[server,tempo,stripe]>=0.6,<1"] coinbase = ["cdp-sdk>=1.0,<2"] +ucp = ["joserfc>=1.0.0,<2"] [project.urls] Homepage = "https://agentscore.sh" @@ -70,6 +71,7 @@ dev = [ "stripe>=11.0.0", "lefthook>=2.1.6", "cdp-sdk>=1.0,<2", + "joserfc>=1.0.0,<2", ] [tool.ty.src] diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py new file mode 100644 index 0000000..27b3eca --- /dev/null +++ b/tests/test_ucp_jwks.py @@ -0,0 +1,139 @@ +"""Tests for UCP profile signing helpers (cross-language parity with node-commerce).""" + +from __future__ import annotations + +import pytest + +from agentscore_commerce.identity.ucp_jwks import ( + build_jwks_response, + generate_ucp_signing_key, + sign_ucp_profile, + verify_ucp_profile, +) + + +def _base_profile(signing_keys: list[dict]) -> dict: + return { + "version": "2026-04-17", + "spec": "https://ucp.dev/", + "name": "Test Merchant", + "services": [{"type": "rest", "url": "https://agents.example.com"}], + "capabilities": [], + "payment_handlers": [{"name": "tempo", "config": {"recipient": "0x1234"}}], + "signing_keys": signing_keys, + } + + +class TestGenerateUCPSigningKey: + def test_generates_eddsa_keypair_by_default(self) -> None: + key = generate_ucp_signing_key(kid="test-key-1") + assert key.private_key is not None + assert key.public_jwk["kid"] == "test-key-1" + assert key.public_jwk["alg"] == "EdDSA" + assert key.public_jwk["use"] == "sig" + assert key.public_jwk["kty"] == "OKP" + assert key.public_jwk["crv"] == "Ed25519" + assert isinstance(key.public_jwk.get("x"), str) + # private parts must NOT be in the exported public JWK + assert "d" not in key.public_jwk + + def test_generates_es256_keypair(self) -> None: + key = generate_ucp_signing_key(kid="es256-key", alg="ES256") + assert key.public_jwk["alg"] == "ES256" + assert key.public_jwk["kty"] == "EC" + assert key.public_jwk["crv"] == "P-256" + assert isinstance(key.public_jwk.get("x"), str) + assert isinstance(key.public_jwk.get("y"), str) + assert "d" not in key.public_jwk + + def test_distinct_kid_and_material(self) -> None: + a = generate_ucp_signing_key(kid="a") + b = generate_ucp_signing_key(kid="b") + assert a.public_jwk["kid"] == "a" + assert b.public_jwk["kid"] == "b" + assert a.public_jwk["x"] != b.public_jwk["x"] + + def test_unsupported_alg_raises(self) -> None: + with pytest.raises(ValueError, match="Unsupported UCP signing algorithm"): + generate_ucp_signing_key(kid="bad", alg="RS256") # type: ignore[arg-type] + + +class TestSignAndVerifyRoundTrip: + def test_eddsa_sign_verify_round_trip(self) -> None: + key = generate_ucp_signing_key(kid="merchant-2026-05") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="merchant-2026-05") + + assert "signature" in signed + assert isinstance(signed["signature"], str) + # JWS Compact has 3 segments separated by dots + assert len(signed["signature"].split(".")) == 3 + + ok = verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert ok is True + + def test_es256_sign_verify_round_trip(self) -> None: + key = generate_ucp_signing_key(kid="es256-key", alg="ES256") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="es256-key", alg="ES256") + ok = verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert ok is True + + def test_multi_key_jwks_resolves_by_kid(self) -> None: + old_key = generate_ucp_signing_key(kid="old-key") + new_key = generate_ucp_signing_key(kid="new-key") + profile = _base_profile([old_key.public_jwk, new_key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=new_key.private_key, kid="new-key") + ok = verify_ucp_profile(signed, build_jwks_response([old_key.public_jwk, new_key.public_jwk])) + assert ok is True + + def test_rejects_tampered_profile_body(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + + tampered = {**signed, "name": "Different Name"} + with pytest.raises(ValueError, match="does not match the signed payload"): + verify_ucp_profile(tampered, build_jwks_response([key.public_jwk])) + + def test_rejects_when_jwks_missing_signing_key(self) -> None: + signer = generate_ucp_signing_key(kid="signer") + other = generate_ucp_signing_key(kid="other") + profile = _base_profile([signer.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="signer") + + from joserfc.errors import InvalidKeyIdError + + with pytest.raises(InvalidKeyIdError): + verify_ucp_profile(signed, build_jwks_response([other.public_jwk])) + + def test_rejects_profile_without_signature(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + with pytest.raises(ValueError, match="no `signature` field"): + verify_ucp_profile(profile, build_jwks_response([key.public_jwk])) + + +class TestCanonicalization: + def test_key_order_in_json_does_not_affect_verification(self) -> None: + import json + + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + + # Round-trip through JSON (loses original key order) + reordered = json.loads(json.dumps(signed)) + ok = verify_ucp_profile(reordered, build_jwks_response([key.public_jwk])) + assert ok is True + + +class TestBuildJWKSResponse: + def test_wraps_keys_in_keys_array(self) -> None: + k1 = {"kid": "a", "kty": "OKP", "crv": "Ed25519", "x": "xxx", "use": "sig", "alg": "EdDSA"} + k2 = {"kid": "b", "kty": "EC", "crv": "P-256", "x": "xxx", "y": "yyy", "use": "sig", "alg": "ES256"} + jwks = build_jwks_response([k1, k2]) + assert jwks == {"keys": [k1, k2]} + + def test_handles_empty_key_set(self) -> None: + assert build_jwks_response([]) == {"keys": []} diff --git a/uv.lock b/uv.lock index b949243..2e9f988 100644 --- a/uv.lock +++ b/uv.lock @@ -10,7 +10,7 @@ resolution-markers = [ [[package]] name = "agentscore-commerce" -version = "1.3.6" +version = "1.3.7" source = { editable = "." } dependencies = [ { name = "agentscore-py" }, @@ -46,6 +46,9 @@ starlette = [ stripe = [ { name = "stripe" }, ] +ucp = [ + { name = "joserfc" }, +] x402 = [ { name = "x402", extra = ["evm", "fastapi"] }, ] @@ -59,6 +62,7 @@ dev = [ { name = "django", version = "6.0.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.12'" }, { name = "fastapi" }, { name = "flask" }, + { name = "joserfc" }, { name = "lefthook" }, { name = "pytest" }, { name = "pytest-asyncio" }, @@ -83,13 +87,14 @@ requires-dist = [ { name = "fastapi", marker = "extra == 'fastapi'", specifier = ">=0.100.0" }, { name = "flask", marker = "extra == 'flask'", specifier = ">=2.0.0" }, { name = "httpx", specifier = ">=0.25.0,<1.0.0" }, + { name = "joserfc", marker = "extra == 'ucp'", specifier = ">=1.0.0,<2" }, { name = "pympp", extras = ["server", "tempo", "stripe"], marker = "extra == 'mppx'", specifier = ">=0.6,<1" }, { name = "sanic", marker = "extra == 'sanic'", specifier = ">=23.0.0" }, { name = "starlette", marker = "extra == 'starlette'", specifier = ">=0.27.0" }, { name = "stripe", marker = "extra == 'stripe'", specifier = ">=11.0.0" }, { name = "x402", extras = ["evm", "fastapi"], marker = "extra == 'x402'", specifier = ">=2.9,<3" }, ] -provides-extras = ["starlette", "fastapi", "flask", "django", "aiohttp", "sanic", "stripe", "x402", "mppx", "coinbase"] +provides-extras = ["starlette", "fastapi", "flask", "django", "aiohttp", "sanic", "stripe", "x402", "mppx", "coinbase", "ucp"] [package.metadata.requires-dev] dev = [ @@ -99,6 +104,7 @@ dev = [ { name = "django", specifier = ">=4.0" }, { name = "fastapi", specifier = ">=0.100.0" }, { name = "flask", specifier = ">=2.0.0" }, + { name = "joserfc", specifier = ">=1.0.0,<2" }, { name = "lefthook", specifier = ">=2.1.6" }, { name = "pytest", specifier = ">=7.0" }, { name = "pytest-asyncio", specifier = ">=0.21" }, @@ -1600,6 +1606,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, ] +[[package]] +name = "joserfc" +version = "1.6.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cryptography" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/3b/dc/5f768c2e391e9afabe5d18e3221346deb5fb6338565f1ccc9e7c6d7befdd/joserfc-1.6.5.tar.gz", hash = "sha256:1482a7db78fb4602e44ed89e51b599d052e091288c7c532c5b694e20149dec48", size = 231881, upload-time = "2026-05-06T04:58:13.408Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/3b/ad1cb22e75c963b1f07c8a2329bf47227ce7e4361df5eb2fb101b2ce33ef/joserfc-1.6.5-py3-none-any.whl", hash = "sha256:e9878a0f8243fe7b95e11fdda81374ca9f7a689e302751579d3dfdeec559675e", size = 70464, upload-time = "2026-05-06T04:58:11.668Z" }, +] + [[package]] name = "jsonalias" version = "0.1.1" From bacfc5e2e2f7b2bd4f0fe1d4ae3ad215cb905194 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 11:13:16 -0700 Subject: [PATCH 02/37] chore(deps): refresh transitive deps MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - markdown-it-py 4.1.0 → 4.2.0 - pydantic-settings 2.14.0 → 2.14.1 - types-requests 2.33.0.20260503 → 2.33.0.20260508 - urllib3 2.6.3 → 2.7.0 Full suite green (737 passed, 3 skipped). Co-Authored-By: Claude Opus 4.7 (1M context) --- uv.lock | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/uv.lock b/uv.lock index 2e9f988..5709554 100644 --- a/uv.lock +++ b/uv.lock @@ -1642,14 +1642,14 @@ wheels = [ [[package]] name = "markdown-it-py" -version = "4.1.0" +version = "4.2.0" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "mdurl" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/5c/5c/f3aedc83549aae71cd52b9e9687fe896e3dc6e966ba20eba04718605d198/markdown_it_py-4.1.0.tar.gz", hash = "sha256:760e3f87b2787c044c5138a5ba107b7c2be26c03b13cc7f8fe42756b65b1df6c", size = 81613, upload-time = "2026-05-06T16:32:13.649Z" } +sdist = { url = "https://files.pythonhosted.org/packages/06/ff/7841249c247aa650a76b9ee4bbaeae59370dc8bfd2f6c01f3630c35eb134/markdown_it_py-4.2.0.tar.gz", hash = "sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49", size = 82454, upload-time = "2026-05-07T12:08:28.36Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/a8/88/802c82060c54bc7dde21eb0033e337838b8181a1323254aa9ec41cbfc3d1/markdown_it_py-4.1.0-py3-none-any.whl", hash = "sha256:d4939a62a2dd0cd9cb80a191a711ba1d39bac8ed5ef9e9966895b0171c01c46d", size = 90955, upload-time = "2026-05-06T16:32:12.184Z" }, + { url = "https://files.pythonhosted.org/packages/b3/81/4da04ced5a082363ecfa159c010d200ecbd959ae410c10c0264a38cac0f5/markdown_it_py-4.2.0-py3-none-any.whl", hash = "sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a", size = 91687, upload-time = "2026-05-07T12:08:27.182Z" }, ] [[package]] @@ -2178,16 +2178,16 @@ wheels = [ [[package]] name = "pydantic-settings" -version = "2.14.0" +version = "2.14.1" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "pydantic" }, { name = "python-dotenv" }, { name = "typing-inspection" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/42/98/c8345dccdc31de4228c039a98f6467a941e39558da41c1744fbe29fa5666/pydantic_settings-2.14.0.tar.gz", hash = "sha256:24285fd4b0e0c06507dd9fdfd331ee23794305352aaec8fc4eb92d4047aeb67d", size = 235709, upload-time = "2026-04-20T13:37:40.293Z" } +sdist = { url = "https://files.pythonhosted.org/packages/07/60/1d1e59c9c90d54591469ada7d268251f71c24bdb765f1a8a832cee8c6653/pydantic_settings-2.14.1.tar.gz", hash = "sha256:e874d3bec7e787b0c9958277956ed9b4dd5de6a80e162188fdaff7c5e26fd5fa", size = 235551, upload-time = "2026-05-08T13:40:06.542Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/01/dd/bebff3040138f00ae8a102d426b27349b9a49acc310fcae7f92112d867e3/pydantic_settings-2.14.0-py3-none-any.whl", hash = "sha256:fc8d5d692eb7092e43c8647c1c35a3ecd00e040fcf02ed86f4cb5458ca62182e", size = 60940, upload-time = "2026-04-20T13:37:38.586Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8d/f1af3832f5e6eb13ba94ee809e72b8ecb5eef226d27ee0bef7d963d943c7/pydantic_settings-2.14.1-py3-none-any.whl", hash = "sha256:6e3c7edfd8277687cdc598f56e5cff0e9bfff0910a3749deaa8d4401c3a2b9de", size = 60964, upload-time = "2026-05-08T13:40:04.958Z" }, ] [[package]] @@ -3003,14 +3003,14 @@ wheels = [ [[package]] name = "types-requests" -version = "2.33.0.20260503" +version = "2.33.0.20260508" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "urllib3" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/a1/b8/57e94268c0d82ac3eaa2fc35aa8ca7bbc2542f726b67dcf90b0b00a3b14d/types_requests-2.33.0.20260503.tar.gz", hash = "sha256:9721b2d9dbee7131f2fb39f20f0ebb1999c18cef4b512c9a7932f3722de7c5f4", size = 23931, upload-time = "2026-05-03T05:20:08.882Z" } +sdist = { url = "https://files.pythonhosted.org/packages/c7/6b/eb226bdd61a982c9a03e02c657fb4ab001733506e6423906ac142331f2e3/types_requests-2.33.0.20260508.tar.gz", hash = "sha256:81b2ae5f0d20967714a6aa5ef9284c05570d7cb06b7de8f2a77b918b63ddd411", size = 23991, upload-time = "2026-05-08T04:50:56.818Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/c3/82/959113a6351f3ca046cd0a8cd2cee071d7ea47473560557a01eeae9a6fe2/types_requests-2.33.0.20260503-py3-none-any.whl", hash = "sha256:02aaa7e3577a13471715bb1bddb693cc985ea514f754b503bf033e6a09a3e528", size = 20736, upload-time = "2026-05-03T05:20:07.858Z" }, + { url = "https://files.pythonhosted.org/packages/cb/96/080db0afdf2c5cc5fe512b41354e8d114fe8f65e9510c56ff8dfd40216ce/types_requests-2.33.0.20260508-py3-none-any.whl", hash = "sha256:fa01459cca184229713df03709db46a905325906d27e042cd4fd7ea3d15d3400", size = 20722, upload-time = "2026-05-08T04:50:55.548Z" }, ] [[package]] @@ -3100,11 +3100,11 @@ wheels = [ [[package]] name = "urllib3" -version = "2.6.3" +version = "2.7.0" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c7/24/5f1b3bdffd70275f6661c76461e25f024d5a38a46f04aaca912426a2b1d3/urllib3-2.6.3.tar.gz", hash = "sha256:1b62b6884944a57dbe321509ab94fd4d3b307075e0c2eae991ac71ee15ad38ed", size = 435556, upload-time = "2026-01-07T16:24:43.925Z" } +sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/39/08/aaaad47bc4e9dc8c725e68f9d04865dbcb2052843ff09c97b08904852d84/urllib3-2.6.3-py3-none-any.whl", hash = "sha256:bf272323e553dfb2e87d9bfd225ca7b0f467b919d7bbd355436d3fd37cb0acd4", size = 131584, upload-time = "2026-01-07T16:24:42.685Z" }, + { url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" }, ] [[package]] From 7e6d6edf28c6d8442d2549134c34625c917e8161 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 13:54:27 -0700 Subject: [PATCH 03/37] hardening(identity): UCP signing security + ergonomics fixes Reviewer audit surfaced three real issues + several gaps. Fixes: Security: - verify_ucp_profile pre-deserializes the JWS protected header and enforces kid + typ='ucp-profile+jws' + alg in {EdDSA, ES256} BEFORE passing to joserfc, closing the missing-kid bypass where joserfc's KeySet-based verify silently iterates every key. - Rejects duplicate kids in JWKS. - _canonicalize_profile rejects float values at sign-time (cross-language float canonicalization is not stable). Ergonomics: - New UCPVerificationError (ValueError subclass) with discriminated `code` attribute. Wraps joserfc's BadSignatureError / DecodeError so consumers don't import joserfc internals. - New UCPSigningKey.from_jwk() classmethod fixes the broken README/Mintlify example: `UCPSigningKey(**key.public_jwk)` raises TypeError because dataclass has fixed fields. The classmethod routes known fields + captures `x`/`y` into `extras`. Docs: - README documents kid/typ/alg enforcement, error codes, float defense, HSM/KMS via joserfc, key rotation runbook. - Mintlify python-commerce.mdx fixed. - New examples/signed_ucp_merchant.py demonstrates the full sign+verify flow with env-var-loaded key + asyncio.Lock-protected cache + cache headers + alg auto-detection from JWK shape. Tests: - Added 14 new tests covering kid-less JWS rejection, wrong typ, alg-confusion (HS256 hostile key), dup-kid, body mismatch, no_signature, tampered signature segment, malformed JWS, EdDSA determinism, ES256 non-determinism, float rejection, UCPSigningKey.from_jwk round-trip. - 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 | 19 ++ agentscore_commerce/identity/__init__.py | 2 + agentscore_commerce/identity/ucp.py | 18 ++ agentscore_commerce/identity/ucp_jwks.py | 163 +++++++++++++-- examples/signed_ucp_merchant.py | 144 ++++++++++++++ .../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/test_ucp_cross_lang.py | 32 +++ tests/test_ucp_jwks.py | 185 +++++++++++++++++- 13 files changed, 848 insertions(+), 21 deletions(-) create mode 100644 examples/signed_ucp_merchant.py 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/test_ucp_cross_lang.py diff --git a/README.md b/README.md index 25ceb70..ccf5d24 100644 --- a/README.md +++ b/README.md @@ -206,17 +206,36 @@ UCP §6 trust-mode requires profiles to carry a JWS signature backed by a JWKS a ```python from agentscore_commerce.identity import ( + UCPSigningKey, + UCPVerificationError, build_jwks_response, + build_ucp_profile, generate_ucp_signing_key, sign_ucp_profile, verify_ucp_profile, ) key = generate_ucp_signing_key(kid="merchant-2026-05") +profile = build_ucp_profile( + name="My Service", + services=[...], + payment_handlers=[...], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], +) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=key.public_jwk["kid"], alg="EdDSA") jwks = build_jwks_response([key.public_jwk]) ``` +`verify_ucp_profile` 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 raise `UCPVerificationError` (a `ValueError` subclass) with a discriminated `code` attribute (`no_signature`/`missing_kid`/`kid_not_found`/`duplicate_kid`/`unsupported_alg`/`wrong_typ`/`signature_invalid`/`body_mismatch`/`malformed_jws`). + +`sign_ucp_profile` rejects profiles containing `float` values: cross-language float canonicalization is not stable, so use decimal strings (e.g. `"9.99"`) for any monetary or fractional fields you put in `extras`. + +**HSM / KMS-backed signing.** `signing_key` accepts any joserfc `Key` subclass — including remote signers wrapped via `joserfc.jwk.OKPKey`/`ECKey`. 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. Drop the old JWK after 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 to prevent a swap-after-sign attack. + ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol with no identity-publishing surface — ACP merchants integrate via the existing `build_402_body` + `build_payment_headers` + Stripe SPT rail. ## Stripe multichain diff --git a/agentscore_commerce/identity/__init__.py b/agentscore_commerce/identity/__init__.py index 42f1c27..57643de 100644 --- a/agentscore_commerce/identity/__init__.py +++ b/agentscore_commerce/identity/__init__.py @@ -56,6 +56,7 @@ ) from agentscore_commerce.identity.ucp_jwks import ( GeneratedUCPKey, + UCPVerificationError, build_jwks_response, generate_ucp_signing_key, sign_ucp_profile, @@ -113,6 +114,7 @@ def _load_asgi_middleware() -> tuple[Any, Any]: "UCPProfile", "UCPService", "UCPSigningKey", + "UCPVerificationError", "VerifyWalletSignerMatchOptions", "VerifyWalletSignerResult", "build_a2a_agent_card", diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 52d08b6..a8a63b7 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -61,6 +61,24 @@ def to_dict(self) -> dict[str, Any]: out.update(self.extras) return out + @classmethod + def from_jwk(cls, jwk: dict[str, Any]) -> UCPSigningKey: + """Construct a UCPSigningKey from a public JWK dict. + + Routes the JWK's known fields (kid/kty/alg/use/crv) onto the dataclass and + captures any other fields (x/y/n/e/etc.) into ``extras``. Use this when + publishing the output of :func:`generate_ucp_signing_key` directly. + """ + known = {"kid", "kty", "alg", "use", "crv"} + return cls( + kid=jwk["kid"], + kty=jwk["kty"], + alg=jwk.get("alg"), + use=jwk.get("use"), + crv=jwk.get("crv"), + extras={k: v for k, v in jwk.items() if k not in known}, + ) + @dataclass class UCPService: diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 78514a3..7cdb1ef 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -32,6 +32,36 @@ "Install the optional dependency: `pip install agentscore-commerce[ucp]` (or `uv pip install joserfc`)." ) +_ALLOWED_ALGS = ("EdDSA", "ES256") +_UCP_TYP = "ucp-profile+jws" + + +class UCPVerificationError(ValueError): + """Discriminated error for UCP signature verification failures. + + Subclasses ``ValueError`` so existing ``except ValueError`` blocks keep working. + Inspect ``code`` to branch on failure mode without parsing the message string + or importing joserfc internals. + """ + + def __init__( + self, + code: Literal[ + "no_signature", + "missing_kid", + "kid_not_found", + "duplicate_kid", + "unsupported_alg", + "wrong_typ", + "signature_invalid", + "body_mismatch", + "malformed_jws", + ], + message: str, + ) -> None: + super().__init__(message) + self.code = code + def _load_joserfc() -> Any: """Lazy-import joserfc so the optional dep isn't required for non-signing flows.""" @@ -95,12 +125,37 @@ def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDS public_jwk.setdefault("alg", alg) public_jwk.setdefault("use", "sig") - # Quiet unused-import warning when only one branch executes. - _ = joserfc + del joserfc return GeneratedUCPKey(private_key=priv, public_jwk=public_jwk) +def _reject_floats(value: Any) -> None: + """Walk ``value`` and raise if any non-integer ``float`` is encountered. + + Cross-language float canonicalization (RFC 8785 §3.2.2.3) diverges between + Python's ``json.dumps`` and Node's ``JSON.stringify`` (e.g. ``1.0`` vs ``1``, + ``1e-7`` vs ``1e-07``). Catching the drift at sign-time prevents + silent verifier-side failures in production. Use decimal strings (``"9.99"``) + for monetary or fractional fields. + """ + if isinstance(value, bool): + return # bool subclasses int; allow. + if isinstance(value, float): + msg = ( + f"UCP profile canonicalization rejects float value {value!r}. " + "Use a decimal string (e.g. '9.99') for monetary or fractional fields " + "to preserve cross-language byte-parity." + ) + raise ValueError(msg) + if isinstance(value, dict): + for v in value.values(): + _reject_floats(v) + elif isinstance(value, list | tuple): + for v in value: + _reject_floats(v) + + def _canonicalize_profile(profile: dict[str, Any]) -> bytes: """Canonicalize a UCP profile body for signing. @@ -108,10 +163,13 @@ def _canonicalize_profile(profile: dict[str, Any]) -> bytes: nesting level, returns UTF-8 JSON bytes. Cross-language byte-identical with the Node ``stableStringify`` output. + Throws ``ValueError`` on float input — see :func:`_reject_floats`. + UCP §6.2: "the JSON-serialized profile body, with ``signature`` removed and keys ordered lexicographically at every nesting level." """ stripped = {k: v for k, v in profile.items() if k != "signature"} + _reject_floats(stripped) # ``ensure_ascii=False`` so non-ASCII characters travel as UTF-8 (matches Node's # JSON.stringify default). ``sort_keys=True`` sorts keys at every level. Compact # separators avoid whitespace drift. @@ -145,26 +203,49 @@ def sign_ucp_profile( from joserfc.jws import JWSRegistry # type: ignore[import-not-found] canonical_body = _canonicalize_profile(profile) - header = {"alg": alg, "kid": kid, "typ": "ucp-profile+jws"} + header = {"alg": alg, "kid": kid, "typ": _UCP_TYP} # joserfc treats EdDSA as "not recommended" by default; UCP §6 explicitly accepts # both EdDSA and ES256, so allow both. - registry = JWSRegistry(algorithms=["EdDSA", "ES256"]) + registry = JWSRegistry(algorithms=list(_ALLOWED_ALGS)) signature = jws.serialize_compact(header, canonical_body, signing_key, registry=registry) - _ = joserfc + del joserfc return {**profile, "signature": signature} +def _peek_jws_header(jws_compact: str) -> dict[str, Any]: + """Decode the JWS protected header (first segment) without verifying. + + Used to enforce kid/typ/alg requirements before handing the JWS to joserfc's + deserialize_compact (which would skip these checks for kid-less JWSs). + """ + import base64 + + try: + header_b64 = jws_compact.split(".")[0] + padding = "=" * (-len(header_b64) % 4) + header_bytes = base64.urlsafe_b64decode(header_b64 + padding) + return json.loads(header_bytes) + except (ValueError, IndexError, json.JSONDecodeError) as exc: + raise UCPVerificationError("malformed_jws", f"Could not decode JWS protected header: {exc}") from exc + + def verify_ucp_profile( signed_profile: dict[str, Any], jwks: dict[str, Any], ) -> bool: """Verify a signed UCP profile against a JWKS. - Returns ``True`` when the JWS validates against a matching key in ``jwks`` AND the - signed payload matches the canonical body of the profile-as-presented. Raises on - signature mismatch, missing key, or canonicalization drift. + Returns ``True`` when: + * the JWS protected header carries ``kid`` + ``typ='ucp-profile+jws'`` + a + registered ``alg`` (EdDSA or ES256), + * the JWKS contains exactly one key with the matching ``kid``, + * the JWS signature validates against that key, + * the signed payload byte-equals the canonical body of the presented profile. + + Raises :class:`UCPVerificationError` (a ``ValueError`` subclass) with a + discriminated ``code`` attribute on every failure mode. Example:: @@ -177,27 +258,72 @@ def verify_ucp_profile( sig = signed_profile.get("signature") if not sig: - msg = "UCP profile has no `signature` field; expected JWS Compact Serialization." - raise ValueError(msg) + raise UCPVerificationError( + "no_signature", + "UCP profile has no `signature` field; expected JWS Compact Serialization.", + ) + if not isinstance(sig, str): + raise UCPVerificationError( + "no_signature", + f"UCP `signature` must be a string; got {type(sig).__name__}.", + ) + + # Pre-deserialize header checks — joserfc's deserialize_compact accepts kid-less + # JWSs (it iterates the KeySet) so we enforce kid/typ/alg ourselves. + header = _peek_jws_header(sig) + if header.get("typ") != _UCP_TYP: + raise UCPVerificationError( + "wrong_typ", + f"UCP signature typ must be {_UCP_TYP!r}; got {header.get('typ')!r}.", + ) + if header.get("alg") not in _ALLOWED_ALGS: + raise UCPVerificationError( + "unsupported_alg", + f"UCP signing alg must be one of {_ALLOWED_ALGS}; got {header.get('alg')!r}.", + ) + kid = header.get("kid") + if not kid or not isinstance(kid, str): + raise UCPVerificationError("missing_kid", "UCP signature header missing `kid`.") + + keys_list = jwks.get("keys", []) if isinstance(jwks, dict) else [] + matches = [k for k in keys_list if isinstance(k, dict) and k.get("kid") == kid] + if not matches: + raise UCPVerificationError("kid_not_found", f"No JWK in JWKS matching kid={kid!r}.") + if len(matches) > 1: + raise UCPVerificationError( + "duplicate_kid", + f"JWKS contains {len(matches)} keys with kid={kid!r}; expected exactly one.", + ) stripped = {k: v for k, v in signed_profile.items() if k != "signature"} expected_payload = _canonicalize_profile(stripped) - # joserfc's KeySetSerialization type is a precise TypedDict; in practice the helper - # accepts a plain dict-of-keys at runtime, so cast at the boundary. - key_set = KeySet.import_key_set(cast("Any", jwks)) - registry = JWSRegistry(algorithms=["EdDSA", "ES256"]) - obj = jws.deserialize_compact(sig, key_set, registry=registry) + key_set = KeySet.import_key_set(cast("Any", {"keys": matches})) + registry = JWSRegistry(algorithms=list(_ALLOWED_ALGS)) + try: + obj = jws.deserialize_compact(sig, key_set, registry=registry) + except Exception as exc: + # joserfc raises various subclasses (BadSignatureError, DecodeError, ...). + # Wrap in our own type so callers don't need to import joserfc internals. + from joserfc.errors import BadSignatureError, DecodeError # type: ignore[import-not-found] + + if isinstance(exc, BadSignatureError): + raise UCPVerificationError("signature_invalid", f"UCP signature verification failed: {exc}") from exc + if isinstance(exc, DecodeError): + raise UCPVerificationError("malformed_jws", f"Malformed JWS: {exc}") from exc + raise # Compare the bytes that were actually signed against the canonical body of the # profile we received. ``deserialize_compact`` 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. if obj.payload != expected_payload: - msg = "UCP profile body does not match the signed payload (tampered or non-canonical)." - raise ValueError(msg) + raise UCPVerificationError( + "body_mismatch", + "UCP profile body does not match the signed payload (tampered or non-canonical).", + ) - _ = joserfc + del joserfc return True @@ -217,6 +343,7 @@ async def jwks(): __all__ = [ "GeneratedUCPKey", + "UCPVerificationError", "build_jwks_response", "generate_ucp_signing_key", "sign_ucp_profile", diff --git a/examples/signed_ucp_merchant.py b/examples/signed_ucp_merchant.py new file mode 100644 index 0000000..f6bce3f --- /dev/null +++ b/examples/signed_ucp_merchant.py @@ -0,0 +1,144 @@ +"""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:: + + uv run uvicorn examples.signed_ucp_merchant:app --port 3010 + +Production checklist: + +* Set ``UCP_SIGNING_KEY_JWK_PRIVATE`` to a JSON-encoded private JWK (mint via + :func:`generate_ucp_signing_key` 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. +""" + +from __future__ import annotations + +import asyncio +import json +import logging +import os +from typing import Any, Literal + +from fastapi import FastAPI +from fastapi.responses import JSONResponse + +from agentscore_commerce.identity import ( + UCPPaymentHandler, + UCPService, + UCPSigningKey, + UCPVerificationError, + build_jwks_response, + build_ucp_profile, + generate_ucp_signing_key, + sign_ucp_profile, + verify_ucp_profile, +) +from agentscore_commerce.identity.ucp_jwks import GeneratedUCPKey + +logger = logging.getLogger("signed_ucp_merchant") + +KID = os.environ.get("UCP_SIGNING_KEY_KID", "merchant-2026-05") +ALG: Literal["EdDSA", "ES256"] = "ES256" if os.environ.get("UCP_SIGNING_KEY_ALG") == "ES256" else "EdDSA" + +# Asyncio lock + cached Future so concurrent first-callers don't generate +# different keys (race condition fix). +_lock = asyncio.Lock() +_cached: GeneratedUCPKey | None = None + + +async def load_signing_key() -> GeneratedUCPKey: + global _cached + async with _lock: + if _cached is not None: + return _cached + env_jwk = os.environ.get("UCP_SIGNING_KEY_JWK_PRIVATE") + if env_jwk: + from joserfc.jwk import ECKey, OKPKey # type: ignore[import-not-found] + + try: + jwk_dict = json.loads(env_jwk) + except json.JSONDecodeError as exc: + msg = f"UCP_SIGNING_KEY_JWK_PRIVATE is not valid JSON: {exc}" + raise ValueError(msg) from exc + # Detect alg from JWK shape; ignore env if it conflicts. + kty = jwk_dict.get("kty") + crv = jwk_dict.get("crv") + if kty == "OKP" and crv == "Ed25519": + priv = OKPKey.import_key(jwk_dict) + effective_alg: Literal["EdDSA", "ES256"] = "EdDSA" + elif kty == "EC" and crv == "P-256": + priv = ECKey.import_key(jwk_dict) + effective_alg = "ES256" + else: + msg = f"Unsupported env JWK: kty={kty} crv={crv}" + raise ValueError(msg) + public_jwk: dict[str, Any] = priv.as_dict(private=False) + public_jwk.setdefault("kid", jwk_dict.get("kid", KID)) + public_jwk["alg"] = effective_alg + public_jwk["use"] = "sig" + _cached = GeneratedUCPKey(private_key=priv, public_jwk=public_jwk) + return _cached + logger.warning( + "UCP_SIGNING_KEY_JWK_PRIVATE not set — generating ephemeral key. " + "Verifier caches will break across restarts." + ) + _cached = generate_ucp_signing_key(kid=KID, alg=ALG) + return _cached + + +app = FastAPI() + + +@app.get("/.well-known/ucp") +async def well_known_ucp() -> JSONResponse: + key = await load_signing_key() + profile = build_ucp_profile( + name="My Agent Service", + services=[UCPService(type="rest", url="https://agents.example.com")], + payment_handlers=[UCPPaymentHandler(name="tempo", config={"recipient": "0xfeedface"})], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + ) + signed = sign_ucp_profile( + profile.to_dict(), + signing_key=key.private_key, + kid=key.public_jwk["kid"], + alg=key.public_jwk.get("alg", ALG), + ) + return JSONResponse(signed, headers={"Cache-Control": "public, max-age=60"}) + + +@app.get("/.well-known/jwks.json") +async def well_known_jwks() -> JSONResponse: + key = await load_signing_key() + return JSONResponse( + build_jwks_response([key.public_jwk]), + headers={ + "Cache-Control": "public, max-age=300", + "Content-Type": "application/jwk-set+json", + }, + ) + + +@app.get("/_selftest/ucp") +async def selftest() -> JSONResponse: + """Local round-trip: sign+serve+fetch+verify, return UCPVerificationError code on failure.""" + profile_resp = await well_known_ucp() + jwks_resp = await well_known_jwks() + profile = json.loads(profile_resp.body.decode()) + jwks = json.loads(jwks_resp.body.decode()) + try: + verify_ucp_profile(profile, jwks) + return JSONResponse({"ok": True, "kid": profile["signing_keys"][0]["kid"]}) + except UCPVerificationError as exc: + return JSONResponse({"ok": False, "code": exc.code, "message": str(exc)}, status_code=500) 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/test_ucp_cross_lang.py b/tests/test_ucp_cross_lang.py new file mode 100644 index 0000000..34f2ef4 --- /dev/null +++ b/tests/test_ucp_cross_lang.py @@ -0,0 +1,32 @@ +"""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 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. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from agentscore_commerce.identity import verify_ucp_profile + +FIXTURE_DIR = Path(__file__).parent / "fixtures" / "cross-lang" +FIXTURES = sorted(FIXTURE_DIR.glob("*.json")) + + +@pytest.mark.parametrize("fixture_path", FIXTURES, ids=[p.name for p in FIXTURES]) +def test_verifies_cross_lang_fixture(fixture_path: Path) -> None: + data = json.loads(fixture_path.read_text()) + assert verify_ucp_profile(data["profile"], data["jwks"]) is True + + +def test_corpus_contains_both_generators() -> None: + generators = {json.loads(p.read_text())["generator"] for p in FIXTURES} + assert "node" in generators + assert "python" in generators + assert len(FIXTURES) >= 6 diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index 27b3eca..5fb94b1 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -4,7 +4,9 @@ import pytest +from agentscore_commerce.identity.ucp import UCPSigningKey from agentscore_commerce.identity.ucp_jwks import ( + UCPVerificationError, build_jwks_response, generate_ucp_signing_key, sign_ucp_profile, @@ -102,10 +104,9 @@ def test_rejects_when_jwks_missing_signing_key(self) -> None: profile = _base_profile([signer.public_jwk]) signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="signer") - from joserfc.errors import InvalidKeyIdError - - with pytest.raises(InvalidKeyIdError): + with pytest.raises(UCPVerificationError) as exc_info: verify_ucp_profile(signed, build_jwks_response([other.public_jwk])) + assert exc_info.value.code == "kid_not_found" def test_rejects_profile_without_signature(self) -> None: key = generate_ucp_signing_key(kid="k") @@ -137,3 +138,181 @@ def test_wraps_keys_in_keys_array(self) -> None: def test_handles_empty_key_set(self) -> None: assert build_jwks_response([]) == {"keys": []} + + +class TestSecurity: + """Coverage for alg-confusion + kid + typ + dup-kid + tampering attacks.""" + + def _hand_sign_compact(self, header: dict, payload_bytes: bytes, key: object, registry: object) -> str: + from joserfc import jws + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + # Cast for ty + reg = registry if isinstance(registry, JWSRegistry) else JWSRegistry(algorithms=["EdDSA", "ES256", "HS256"]) + return jws.serialize_compact(header, payload_bytes, key, registry=reg) + + def test_rejects_kid_less_jws(self) -> None: + """A JWS with no kid header is rejected even if the JWKS has a key that would verify.""" + from joserfc import jws + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + signer = generate_ucp_signing_key(kid="real-kid") + profile = _base_profile([signer.public_jwk]) + # Hand-craft a JWS with NO kid in the header. + canonical = ( + __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + registry = JWSRegistry(algorithms=["EdDSA", "ES256"]) + kid_less_sig = jws.serialize_compact( + {"alg": "EdDSA", "typ": "ucp-profile+jws"}, + canonical, + signer.private_key, + registry=registry, + ) + signed = {**profile, "signature": kid_less_sig} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) + assert exc.value.code == "missing_kid" + + def test_rejects_wrong_typ(self) -> None: + from joserfc import jws + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + signer = generate_ucp_signing_key(kid="k") + profile = _base_profile([signer.public_jwk]) + canonical = ( + __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + registry = JWSRegistry(algorithms=["EdDSA"]) + wrong_typ_sig = jws.serialize_compact( + {"alg": "EdDSA", "kid": "k", "typ": "JWT"}, + canonical, + signer.private_key, + registry=registry, + ) + signed = {**profile, "signature": wrong_typ_sig} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) + assert exc.value.code == "wrong_typ" + + def test_rejects_unsupported_alg(self) -> None: + from joserfc import jws + from joserfc.jwk import OctKey # type: ignore[import-not-found] + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + # Build a hostile oct key + HS256 sig over the canonical body of a real profile. + signer = generate_ucp_signing_key(kid="real") + profile = _base_profile([signer.public_jwk]) + canonical = ( + __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + oct_key = OctKey.generate_key(parameters={"kid": "real", "alg": "HS256", "use": "sig"}) + registry = JWSRegistry(algorithms=["HS256"]) + evil_sig = jws.serialize_compact( + {"alg": "HS256", "kid": "real", "typ": "ucp-profile+jws"}, + canonical, + oct_key, + registry=registry, + ) + signed = {**profile, "signature": evil_sig} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) + assert exc.value.code == "unsupported_alg" + + def test_rejects_duplicate_kid_in_jwks(self) -> None: + a = generate_ucp_signing_key(kid="dup") + b = generate_ucp_signing_key(kid="dup") + profile = _base_profile([a.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=a.private_key, kid="dup") + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([a.public_jwk, b.public_jwk])) + assert exc.value.code == "duplicate_kid" + + def test_emits_typed_error_for_body_mismatch(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = _base_profile([signer.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + tampered = {**signed, "name": "Different"} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(tampered, build_jwks_response([signer.public_jwk])) + assert exc.value.code == "body_mismatch" + + def test_emits_typed_error_for_no_signature(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = _base_profile([signer.public_jwk]) + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(profile, build_jwks_response([signer.public_jwk])) + assert exc.value.code == "no_signature" + + def test_rejects_tampered_signature_segment(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = _base_profile([signer.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + # Flip last char of the signature segment. + h, p, s = signed["signature"].split(".") + flipped_s = s[:-1] + ("B" if s.endswith("A") else "A") + tampered = {**signed, "signature": f"{h}.{p}.{flipped_s}"} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(tampered, build_jwks_response([signer.public_jwk])) + # joserfc may classify as either signature_invalid or malformed_jws depending on the flip. + assert exc.value.code in ("signature_invalid", "malformed_jws") + + def test_rejects_malformed_jws(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = _base_profile([signer.public_jwk]) + garbage = {**profile, "signature": "not.a.jws"} + with pytest.raises(UCPVerificationError): + verify_ucp_profile(garbage, build_jwks_response([signer.public_jwk])) + + def test_eddsa_signing_is_deterministic(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = _base_profile([signer.public_jwk]) + a = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + b = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert a["signature"] == b["signature"] + + def test_es256_signing_is_non_deterministic_but_both_verify(self) -> None: + signer = generate_ucp_signing_key(kid="k", alg="ES256") + profile = _base_profile([signer.public_jwk]) + a = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k", alg="ES256") + b = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k", alg="ES256") + assert a["signature"] != b["signature"] + assert verify_ucp_profile(a, build_jwks_response([signer.public_jwk])) is True + assert verify_ucp_profile(b, build_jwks_response([signer.public_jwk])) is True + + +class TestFloatRejection: + def test_rejects_float_in_profile(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"rate": 0.0125}} + with pytest.raises(ValueError, match="rejects float"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_accepts_int_and_string(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"count": 7, "label": "wine"}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + +class TestUCPSigningKeyFromJWK: + def test_round_trip_eddsa(self) -> None: + gen = generate_ucp_signing_key(kid="merchant-2026-05") + sk = UCPSigningKey.from_jwk(gen.public_jwk) + assert sk.kid == "merchant-2026-05" + assert sk.kty == "OKP" + assert sk.alg == "EdDSA" + assert sk.use == "sig" + assert sk.crv == "Ed25519" + assert "x" in sk.extras + # Re-emit and confirm the JWK round-trips. + as_dict = sk.to_dict() + assert as_dict["kid"] == "merchant-2026-05" + assert as_dict["x"] == gen.public_jwk["x"] + + def test_round_trip_es256(self) -> None: + gen = generate_ucp_signing_key(kid="es", alg="ES256") + sk = UCPSigningKey.from_jwk(gen.public_jwk) + assert sk.kty == "EC" + assert sk.crv == "P-256" + assert "x" in sk.extras and "y" in sk.extras From df383ff6ca824b2f43f795301f6da4514a747d88 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 15:14:18 -0700 Subject: [PATCH 04/37] hardening: round-2 reviewer findings (security + test gaps) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Mirrors node-commerce hardening for cross-language parity. - verify_ucp_profile: wrap UnsupportedHeaderError (RFC 7515 §4.1.11 unrecognized `crit`) into UCPVerificationError('unrecognized_critical_header'). - verify_ucp_profile: malformed JWKS shape (non-dict, missing keys array) now emits UCPVerificationError('malformed_jwks') instead of cryptic AttributeError. Same for non-dict signed_profile input. - verify_ucp_profile: reject JWKs with use != 'sig' as 'unusable_key' (RFC 7517 §4.2). - sign_ucp_profile: enforce kid in profile.signing_keys[] at sign-time. - UCPSigningKey.from_jwk: reject `oct` symmetric keys + missing `kid`/`kty` with typed ValueError instead of bare KeyError. - Drop 3 redundant `del joserfc` statements. - Cross-language fixture corpus: 6 → 12 fixtures (minimal, ES256-rails, extras-int, capability, unicode, multi-key per language). - Fix vacuous key-order-independence test (json.dumps preserves order on Python 3.7+); now hand-constructs reverse insertion-order dict. - README KMS claim rewritten — `OKPKey`/`ECKey` import key MATERIAL, they don't wrap remote signers. Tests: 773 pass, 3 skipped. ruff + ty clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- agentscore_commerce/identity/ucp.py | 21 +++++ agentscore_commerce/identity/ucp_jwks.py | 66 +++++++++++--- .../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/test_ucp_cross_lang.py | 9 +- tests/test_ucp_jwks.py | 86 +++++++++++++++++-- 11 files changed, 501 insertions(+), 19 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 ccf5d24..9fc4969 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ jwks = build_jwks_response([key.public_jwk]) `sign_ucp_profile` rejects profiles containing `float` values: cross-language float canonicalization is not stable, so use decimal strings (e.g. `"9.99"`) for any monetary or fractional fields you put in `extras`. -**HSM / KMS-backed signing.** `signing_key` accepts any joserfc `Key` subclass — including remote signers wrapped via `joserfc.jwk.OKPKey`/`ECKey`. The signing key never has to leave the HSM. +**Persisting the private JWK.** Mint once via `generate_ucp_signing_key()`, serialize via `key.private_key.as_dict(private=True)`, store in your secret manager. On each container start, read the secret, `OKPKey.import_key(jwk_dict)` (or `ECKey.import_key` for ES256) to re-hydrate. Remote-signer flows (KMS-backed asymmetric keys) require subclassing the joserfc Key to delegate the sign hook; `OKPKey`/`ECKey` themselves only carry local key material. **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. Drop the old JWK after your verifier-side cache TTL has elapsed. diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index a8a63b7..d3a34e3 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -68,7 +68,28 @@ def from_jwk(cls, jwk: dict[str, Any]) -> UCPSigningKey: Routes the JWK's known fields (kid/kty/alg/use/crv) onto the dataclass and captures any other fields (x/y/n/e/etc.) into ``extras``. Use this when publishing the output of :func:`generate_ucp_signing_key` directly. + + Rejects symmetric (``oct``) keys and JWKs missing required fields with a + typed ``ValueError`` rather than a bare ``KeyError``. """ + if not isinstance(jwk, dict): + msg = f"UCPSigningKey.from_jwk expected a dict; got {type(jwk).__name__}." + raise ValueError(msg) + if "kid" not in jwk: + msg = "UCPSigningKey.from_jwk: JWK missing required field `kid`." + raise ValueError(msg) + if "kty" not in jwk: + msg = "UCPSigningKey.from_jwk: JWK missing required field `kty`." + raise ValueError(msg) + if jwk["kty"] not in {"OKP", "EC", "RSA"}: + msg = ( + f"UCPSigningKey.from_jwk: kty={jwk['kty']!r} 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." + ) + raise ValueError(msg) + known = {"kid", "kty", "alg", "use", "crv"} return cls( kid=jwk["kid"], diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 7cdb1ef..3da282f 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -56,6 +56,9 @@ def __init__( "signature_invalid", "body_mismatch", "malformed_jws", + "malformed_jwks", + "unrecognized_critical_header", + "unusable_key", ], message: str, ) -> None: @@ -105,7 +108,7 @@ def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDS # key.private_key — persist securely # key.public_jwk — publish at /.well-known/jwks.json """ - joserfc = _load_joserfc() + _load_joserfc() if alg == "EdDSA": from joserfc.jwk import OKPKey # type: ignore[import-not-found] @@ -125,8 +128,6 @@ def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDS public_jwk.setdefault("alg", alg) public_jwk.setdefault("use", "sig") - del joserfc - return GeneratedUCPKey(private_key=priv, public_jwk=public_jwk) @@ -198,10 +199,23 @@ def sign_ucp_profile( profile = build_ucp_profile(..., signing_keys=[UCPSigningKey(**key.public_jwk)]) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid='merchant-2026-05') """ - joserfc = _load_joserfc() + _load_joserfc() from joserfc import jws # type: ignore[import-not-found] from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + # 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. + declared_kids = [ + k.get("kid") if isinstance(k, dict) else getattr(k, "kid", None) for k in profile.get("signing_keys", []) + ] + if kid not in declared_kids: + msg = ( + f"sign_ucp_profile: kid {kid!r} is not present in profile.signing_keys[] " + f"(declared kids: {declared_kids!r}). Verifiers will not find the key." + ) + raise ValueError(msg) + canonical_body = _canonicalize_profile(profile) header = {"alg": alg, "kid": kid, "typ": _UCP_TYP} # joserfc treats EdDSA as "not recommended" by default; UCP §6 explicitly accepts @@ -209,8 +223,6 @@ def sign_ucp_profile( registry = JWSRegistry(algorithms=list(_ALLOWED_ALGS)) signature = jws.serialize_compact(header, canonical_body, signing_key, registry=registry) - del joserfc - return {**profile, "signature": signature} @@ -251,11 +263,25 @@ def verify_ucp_profile( ok = verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) """ - joserfc = _load_joserfc() + _load_joserfc() from joserfc import jws # type: ignore[import-not-found] from joserfc.jwk import KeySet # type: ignore[import-not-found] from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + # JWKS shape guard so a malformed argument emits a typed UCPVerificationError + # rather than a confusing kid_not_found / AttributeError. + if not isinstance(jwks, dict) or not isinstance(jwks.get("keys"), list): + raise UCPVerificationError( + "malformed_jwks", + f"UCP verifier expected JWKS shape {{'keys': [...]}}; got {type(jwks).__name__}.", + ) + + if not isinstance(signed_profile, dict): + raise UCPVerificationError( + "no_signature", + f"UCP verifier expected a profile dict; got {type(signed_profile).__name__}.", + ) + sig = signed_profile.get("signature") if not sig: raise UCPVerificationError( @@ -294,6 +320,13 @@ def verify_ucp_profile( "duplicate_kid", f"JWKS contains {len(matches)} keys with kid={kid!r}; expected exactly one.", ) + # RFC 7517 §4.2: reject keys not intended for signature verification. + matched_use = matches[0].get("use") + if matched_use is not None and matched_use != "sig": + raise UCPVerificationError( + "unusable_key", + f"JWK with kid={kid!r} has use={matched_use!r}; expected 'sig'.", + ) stripped = {k: v for k, v in signed_profile.items() if k != "signature"} expected_payload = _canonicalize_profile(stripped) @@ -303,14 +336,26 @@ def verify_ucp_profile( try: obj = jws.deserialize_compact(sig, key_set, registry=registry) except Exception as exc: - # joserfc raises various subclasses (BadSignatureError, DecodeError, ...). - # Wrap in our own type so callers don't need to import joserfc internals. - from joserfc.errors import BadSignatureError, DecodeError # type: ignore[import-not-found] + # joserfc raises various subclasses. Wrap in our own type so callers + # don't need to import joserfc internals. + from joserfc.errors import ( # type: ignore[import-not-found] + BadSignatureError, + DecodeError, + UnsupportedHeaderError, + ) if isinstance(exc, BadSignatureError): raise UCPVerificationError("signature_invalid", f"UCP signature verification failed: {exc}") from exc if isinstance(exc, DecodeError): raise UCPVerificationError("malformed_jws", f"Malformed JWS: {exc}") from exc + # 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. + if isinstance(exc, UnsupportedHeaderError): + raise UCPVerificationError( + "unrecognized_critical_header", + f"UCP signing rejected unrecognized critical header: {exc}", + ) from exc raise # Compare the bytes that were actually signed against the canonical body of the @@ -323,7 +368,6 @@ def verify_ucp_profile( "UCP profile body does not match the signed payload (tampered or non-canonical).", ) - del joserfc return True 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/test_ucp_cross_lang.py b/tests/test_ucp_cross_lang.py index 34f2ef4..96e77e6 100644 --- a/tests/test_ucp_cross_lang.py +++ b/tests/test_ucp_cross_lang.py @@ -25,8 +25,13 @@ def test_verifies_cross_lang_fixture(fixture_path: Path) -> None: assert verify_ucp_profile(data["profile"], data["jwks"]) is True -def test_corpus_contains_both_generators() -> None: +def test_corpus_covers_canonical_scenarios() -> None: + names = {p.name for p in FIXTURES} generators = {json.loads(p.read_text())["generator"] for p in FIXTURES} assert "node" in generators assert "python" in generators - assert len(FIXTURES) >= 6 + # Each language ships 6 scenarios so cross-lang verify exercises all of them. + for lang in ("node", "py"): + for scenario in ("minimal", "es256-rails", "extras-int", "capability", "unicode", "multikey"): + assert f"{lang}-{scenario}.json" in names, f"missing fixture {lang}-{scenario}.json" + assert len(FIXTURES) == 12 diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index 5fb94b1..778d675 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -117,14 +117,15 @@ def test_rejects_profile_without_signature(self) -> None: class TestCanonicalization: def test_key_order_in_json_does_not_affect_verification(self) -> None: - import json - key = generate_ucp_signing_key(kid="k") profile = _base_profile([key.public_jwk]) signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") - # Round-trip through JSON (loses original key order) - reordered = json.loads(json.dumps(signed)) + # Hand-construct the same profile with keys in REVERSE insertion order + # so canonicalization actually has work to do. ``json.loads(json.dumps(...))`` + # preserves the source order on Python 3.7+, which is a vacuous round-trip. + reordered = {k: signed[k] for k in sorted(signed.keys(), reverse=True)} + assert next(iter(reordered)) != next(iter(sorted(signed))) # sanity ok = verify_ucp_profile(reordered, build_jwks_response([key.public_jwk])) assert ok is True @@ -305,7 +306,6 @@ def test_round_trip_eddsa(self) -> None: assert sk.use == "sig" assert sk.crv == "Ed25519" assert "x" in sk.extras - # Re-emit and confirm the JWK round-trips. as_dict = sk.to_dict() assert as_dict["kid"] == "merchant-2026-05" assert as_dict["x"] == gen.public_jwk["x"] @@ -316,3 +316,79 @@ def test_round_trip_es256(self) -> None: assert sk.kty == "EC" assert sk.crv == "P-256" assert "x" in sk.extras and "y" in sk.extras + + def test_rejects_oct_symmetric_key(self) -> None: + with pytest.raises(ValueError, match=r"oct.*rejected|not a supported asymmetric key type"): + UCPSigningKey.from_jwk({"kid": "k", "kty": "oct", "k": "AAAA"}) + + def test_rejects_jwk_missing_kid(self) -> None: + with pytest.raises(ValueError, match="missing required field `kid`"): + UCPSigningKey.from_jwk({"kty": "OKP"}) + + def test_rejects_jwk_missing_kty(self) -> None: + with pytest.raises(ValueError, match="missing required field `kty`"): + UCPSigningKey.from_jwk({"kid": "k"}) + + def test_rejects_non_dict_input(self) -> None: + with pytest.raises(ValueError, match="expected a dict"): + UCPSigningKey.from_jwk("not a jwk") # type: ignore[arg-type] + + +class TestAdditionalHardening: + def test_sign_ucp_profile_rejects_kid_not_in_signing_keys(self) -> None: + key = generate_ucp_signing_key(kid="real") + profile = _base_profile([key.public_jwk]) + with pytest.raises(ValueError, match=r"not present in profile.signing_keys"): + sign_ucp_profile(profile, signing_key=key.private_key, kid="wrong") + + def test_verify_rejects_malformed_jwks_missing_keys(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, {}) + assert exc.value.code == "malformed_jwks" + + def test_verify_rejects_non_dict_jwks(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, [key.public_jwk]) # type: ignore[arg-type] + assert exc.value.code == "malformed_jwks" + + def test_verify_rejects_non_dict_profile(self) -> None: + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile("not a profile", {"keys": []}) # type: ignore[arg-type] + assert exc.value.code == "no_signature" + + def test_verify_wraps_unrecognized_critical_header(self) -> None: + import base64 + + from joserfc import jws + from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + # Hand-craft a JWS with crit (use the raw underlying key to bypass joserfc's sign-time check). + canonical = ( + __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + header = {"alg": "EdDSA", "kid": "k", "typ": "ucp-profile+jws", "crit": ["fakething"], "fakething": "x"} + header_b64 = ( + base64.urlsafe_b64encode(__import__("json").dumps(header, separators=(",", ":")).encode()) + .rstrip(b"=") + .decode() + ) + payload_b64 = base64.urlsafe_b64encode(canonical).rstrip(b"=").decode() + signing_input = f"{header_b64}.{payload_b64}".encode() + sig = key.private_key.private_key.sign(signing_input) + sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b"=").decode() + jws_compact = f"{header_b64}.{payload_b64}.{sig_b64}" + + signed = {**profile, "signature": jws_compact} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "unrecognized_critical_header" + # Silence unused-import warnings — registry is referenced for the joserfc namespace. + _ = jws, JWSRegistry From afc7c3c78c61512d0a38867c387f7d80c471dc8e Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 15:58:19 -0700 Subject: [PATCH 05/37] hardening: round-3 reviewer findings (HIGH JWS header guard + parity) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit HIGH: - verify_ucp_profile: _peek_jws_header now asserts the decoded header is a dict; non-dict JWS header (e.g. `null`, list, number) was raising AttributeError instead of UCPVerificationError(malformed_jws). Parity with Node. Other: - UCPProfile.to_dict: extras-collision filter rejects keys matching reserved profile fields (signing_keys, services, etc.) so `extras={'signing_keys': [...]}` can't silently destroy the explicit field. - generate_ucp_signing_key, sign_ucp_profile, verify_ucp_profile: wrap joserfc calls in a context manager that suppresses the SecurityWarning emitted by joserfc on every EdDSA op (RFC 9864 deprecation, but UCP §6 explicitly mandates EdDSA support). pyproject.toml ships an additional pytest filterwarnings rule for tests that hand-construct joserfc calls. - sign_ucp_profile: kid must be a non-empty string. Empty kid silently passed the in-signing_keys check before. Tests: 773 pass, 3 skipped. ruff + ty clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp.py | 20 ++++++++++- agentscore_commerce/identity/ucp_jwks.py | 43 +++++++++++++++++++++--- pyproject.toml | 7 ++++ 3 files changed, 64 insertions(+), 6 deletions(-) diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index d3a34e3..b651e54 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -181,7 +181,25 @@ def to_dict(self) -> dict[str, Any]: } if self.name is not None: out["name"] = self.name - out.update(self.extras) + # Filter `extras` so a caller passing + # ``extras={"signing_keys": [...]}`` can't silently destroy the + # explicit field. Reserved-field collisions are rejected at + # build-time-equivalent surface. + reserved = { + "version", + "spec", + "services", + "capabilities", + "payment_handlers", + "signing_keys", + "name", + "signature", + } + for k, v in self.extras.items(): + if k in reserved: + msg = f"UCPProfile.extras key {k!r} collides with a reserved profile field; rejected." + raise ValueError(msg) + out[k] = v return out diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 3da282f..bb228d2 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -24,9 +24,14 @@ from __future__ import annotations +import contextlib import json +import warnings from dataclasses import dataclass -from typing import Any, Literal, cast +from typing import TYPE_CHECKING, Any, Literal, cast + +if TYPE_CHECKING: + from collections.abc import Iterator _JOSE_INSTALL_HINT = ( "Install the optional dependency: `pip install agentscore-commerce[ucp]` (or `uv pip install joserfc`)." @@ -36,6 +41,21 @@ _UCP_TYP = "ucp-profile+jws" +@contextlib.contextmanager +def _suppress_joserfc_eddsa_warning() -> Iterator[None]: + """Silence joserfc's per-call SecurityWarning for EdDSA. + + joserfc treats EdDSA as "deprecated via RFC 9864" and emits a + SecurityWarning every time we sign or verify. UCP §6 explicitly mandates + EdDSA support so we suppress the warning at the call site rather than + forcing every consumer to set warnings.filterwarnings globally. CI runs + with -W error would otherwise fail noisily on every signing test. + """ + with warnings.catch_warnings(): + warnings.filterwarnings("ignore", message=r".*EdDSA.*", category=Warning) + yield + + class UCPVerificationError(ValueError): """Discriminated error for UCP signature verification failures. @@ -113,7 +133,8 @@ def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDS if alg == "EdDSA": from joserfc.jwk import OKPKey # type: ignore[import-not-found] - priv = OKPKey.generate_key(crv="Ed25519", parameters={"kid": kid, "alg": alg, "use": "sig"}) + with _suppress_joserfc_eddsa_warning(): + priv = OKPKey.generate_key(crv="Ed25519", parameters={"kid": kid, "alg": alg, "use": "sig"}) elif alg == "ES256": from joserfc.jwk import ECKey # type: ignore[import-not-found] @@ -203,6 +224,10 @@ def sign_ucp_profile( from joserfc import jws # type: ignore[import-not-found] from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + if not isinstance(kid, str) or not kid: + msg = "sign_ucp_profile: `kid` must be a non-empty string." + raise ValueError(msg) + # 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. @@ -221,7 +246,8 @@ def sign_ucp_profile( # joserfc treats EdDSA as "not recommended" by default; UCP §6 explicitly accepts # both EdDSA and ES256, so allow both. registry = JWSRegistry(algorithms=list(_ALLOWED_ALGS)) - signature = jws.serialize_compact(header, canonical_body, signing_key, registry=registry) + with _suppress_joserfc_eddsa_warning(): + signature = jws.serialize_compact(header, canonical_body, signing_key, registry=registry) return {**profile, "signature": signature} @@ -238,9 +264,15 @@ def _peek_jws_header(jws_compact: str) -> dict[str, Any]: header_b64 = jws_compact.split(".")[0] padding = "=" * (-len(header_b64) % 4) header_bytes = base64.urlsafe_b64decode(header_b64 + padding) - return json.loads(header_bytes) + decoded = json.loads(header_bytes) except (ValueError, IndexError, json.JSONDecodeError) as exc: raise UCPVerificationError("malformed_jws", f"Could not decode JWS protected header: {exc}") from exc + if not isinstance(decoded, dict): + raise UCPVerificationError( + "malformed_jws", + f"JWS protected header must decode to a JSON object; got {type(decoded).__name__}.", + ) + return decoded def verify_ucp_profile( @@ -334,7 +366,8 @@ def verify_ucp_profile( key_set = KeySet.import_key_set(cast("Any", {"keys": matches})) registry = JWSRegistry(algorithms=list(_ALLOWED_ALGS)) try: - obj = jws.deserialize_compact(sig, key_set, registry=registry) + with _suppress_joserfc_eddsa_warning(): + obj = jws.deserialize_compact(sig, key_set, registry=registry) except Exception as exc: # joserfc raises various subclasses. Wrap in our own type so callers # don't need to import joserfc internals. diff --git a/pyproject.toml b/pyproject.toml index 32922dc..6e60116 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -80,3 +80,10 @@ include = ["agentscore_commerce"] [tool.pytest.ini_options] asyncio_mode = "auto" addopts = "--cov=agentscore_commerce --cov-report=term-missing --cov-fail-under=95" +# joserfc emits SecurityWarning for EdDSA on every sign/verify (see RFC 9864). +# UCP §6 explicitly accepts EdDSA so we suppress; the SDK helpers wrap their +# own joserfc calls with a context-manager filter, but tests that hand-construct +# joserfc calls directly need the suppression too. +filterwarnings = [ + "ignore:.*EdDSA.*:Warning", +] From 757dedf950320a0623685c7ad7ec1a650e2d776c Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 16:24:13 -0700 Subject: [PATCH 06/37] hardening: round-4 reviewer findings (warning scope + edge tests + README codes) Tighten the EdDSA SecurityWarning suppression to the exact joserfc message and class so a future, unrelated EdDSA-vulnerability warning is no longer swallowed: scope is now `joserfc.errors.SecurityWarning` with literal regex `^EdDSA is deprecated via RFC 9864$` in both the helper and the pyproject `filterwarnings`. Add explicit canonicalization tests for NaN, positive-infinity, and negative-infinity floats (matching node-commerce parity). Add coverage for `unusable_key` (JWK with `use=enc`), non-string `signature` field (int / None / list / dict), non-dict JWKS entries, and JWS protected headers that decode to a JSON array. Extend the README error-code list with `unusable_key`, `malformed_jwks`, and `unrecognized_critical_header`. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- agentscore_commerce/identity/ucp_jwks.py | 19 +++++--- pyproject.toml | 9 ++-- tests/test_ucp_jwks.py | 59 ++++++++++++++++++++++++ 4 files changed, 77 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 9fc4969..9dc559f 100644 --- a/README.md +++ b/README.md @@ -226,7 +226,7 @@ signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=ke jwks = build_jwks_response([key.public_jwk]) ``` -`verify_ucp_profile` 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 raise `UCPVerificationError` (a `ValueError` subclass) with a discriminated `code` attribute (`no_signature`/`missing_kid`/`kid_not_found`/`duplicate_kid`/`unsupported_alg`/`wrong_typ`/`signature_invalid`/`body_mismatch`/`malformed_jws`). +`verify_ucp_profile` 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 raise `UCPVerificationError` (a `ValueError` subclass) with a discriminated `code` attribute (`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`). `sign_ucp_profile` rejects profiles containing `float` values: cross-language float canonicalization is not stable, so use decimal strings (e.g. `"9.99"`) for any monetary or fractional fields you put in `extras`. diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index bb228d2..76fbe2d 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -43,16 +43,21 @@ @contextlib.contextmanager def _suppress_joserfc_eddsa_warning() -> Iterator[None]: - """Silence joserfc's per-call SecurityWarning for EdDSA. + """Silence joserfc's exact RFC 9864 EdDSA SecurityWarning. - joserfc treats EdDSA as "deprecated via RFC 9864" and emits a - SecurityWarning every time we sign or verify. UCP §6 explicitly mandates - EdDSA support so we suppress the warning at the call site rather than - forcing every consumer to set warnings.filterwarnings globally. CI runs - with -W error would otherwise fail noisily on every signing test. + UCP §6 requires EdDSA support and joserfc emits a per-call deprecation + warning. The filter is pinned to the exact message + class + (``joserfc.errors.SecurityWarning``: ``"EdDSA is deprecated via RFC 9864"``) + so a future, unrelated EdDSA warning still surfaces normally. """ + from joserfc.errors import SecurityWarning # type: ignore[import-not-found] + with warnings.catch_warnings(): - warnings.filterwarnings("ignore", message=r".*EdDSA.*", category=Warning) + warnings.filterwarnings( + "ignore", + message=r"^EdDSA is deprecated via RFC 9864$", + category=SecurityWarning, + ) yield diff --git a/pyproject.toml b/pyproject.toml index 6e60116..6560c1c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -81,9 +81,10 @@ include = ["agentscore_commerce"] asyncio_mode = "auto" addopts = "--cov=agentscore_commerce --cov-report=term-missing --cov-fail-under=95" # joserfc emits SecurityWarning for EdDSA on every sign/verify (see RFC 9864). -# UCP §6 explicitly accepts EdDSA so we suppress; the SDK helpers wrap their -# own joserfc calls with a context-manager filter, but tests that hand-construct -# joserfc calls directly need the suppression too. +# UCP §6 explicitly accepts EdDSA so we suppress. SDK helpers wrap their own +# joserfc calls with a context-manager filter, but tests that hand-construct +# joserfc calls directly need the suppression too. Scope matches the exact +# message + class so a future unrelated EdDSA warning still surfaces. filterwarnings = [ - "ignore:.*EdDSA.*:Warning", + "ignore:^EdDSA is deprecated via RFC 9864$:joserfc.errors.SecurityWarning", ] diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index 778d675..fe09e71 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -289,6 +289,24 @@ def test_rejects_float_in_profile(self) -> None: with pytest.raises(ValueError, match="rejects float"): sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + def test_rejects_nan(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"value": float("nan")}} + with pytest.raises(ValueError, match="rejects float"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_positive_infinity(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"value": float("inf")}} + with pytest.raises(ValueError, match="rejects float"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_negative_infinity(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"value": float("-inf")}} + with pytest.raises(ValueError, match="rejects float"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + def test_accepts_int_and_string(self) -> None: signer = generate_ucp_signing_key(kid="k") profile = {**_base_profile([signer.public_jwk]), "extras": {"count": 7, "label": "wine"}} @@ -362,6 +380,47 @@ def test_verify_rejects_non_dict_profile(self) -> None: verify_ucp_profile("not a profile", {"keys": []}) # type: ignore[arg-type] assert exc.value.code == "no_signature" + def test_verify_rejects_unusable_key_use_enc(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + enc_jwk = {**key.public_jwk, "use": "enc"} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([enc_jwk])) + assert exc.value.code == "unusable_key" + + @pytest.mark.parametrize("bad_sig", [42, None, [], {}]) + def test_verify_rejects_non_string_signature(self, bad_sig: object) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + tampered = {**profile, "signature": bad_sig} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(tampered, build_jwks_response([key.public_jwk])) + assert exc.value.code == "no_signature" + + @pytest.mark.parametrize("bad_entry", [None, "string"]) + def test_verify_rejects_non_dict_jwks_entry(self, bad_entry: object) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, {"keys": [bad_entry]}) + assert exc.value.code == "kid_not_found" + + def test_verify_rejects_protected_header_decoding_to_json_array(self) -> None: + import base64 + + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + header_array_b64 = ( + base64.urlsafe_b64encode(__import__("json").dumps(["EdDSA", "kid"]).encode()).rstrip(b"=").decode() + ) + bogus_jws = f"{header_array_b64}.payload.sig" + signed = {**profile, "signature": bogus_jws} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "malformed_jws" + def test_verify_wraps_unrecognized_critical_header(self) -> None: import base64 From 4373205c1eb9d4731990549057fc2b6ee607e26c Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 16:49:36 -0700 Subject: [PATCH 07/37] hardening: round-5 reviewer findings (alg-mismatch + warning scope + non-ASCII keys fixture) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit verify_ucp_profile now rejects matched JWKs whose declared alg disagrees with the JWS protected header alg (RFC 7517 §4.4: a JWK with `alg` constrains its use to that algorithm). Closes a small security gap where a JWKS publisher advertising the wrong alg on a key would silently let mismatched signatures through joserfc's verify path. _suppress_joserfc_eddsa_warning docstring corrected. The previous text claimed joserfc emits the RFC-9864 SecurityWarning at every call including key generation; empirically the warning fires only at JWS sign/verify (in the JWS registry), not at OKPKey.generate_key. Removed the redundant suppression wrapper around generate_key in generate_ucp_signing_key — it was a no-op. New cross-language fixture py-emoji-keys.json mirrors the node-emoji-keys.json fixture landing in node-commerce. Locks codepoint-aware key sort: Python's default sorted() orders by Unicode codepoint, JS default sort orders by UTF-16 code units which diverges for supplementary-plane chars (e.g. U+1F377). The fixture covers BMP CJK Compatibility (U+8C48), non-BMP wine glass (U+1F377), and ASCII so both languages must explicitly sort by codepoint to maintain byte parity. Tests: 786 pass + 3 skipped, 95.17% coverage. ruff + ty clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp_jwks.py | 24 ++++++--- tests/fixtures/cross-lang/py-emoji-keys.json | 51 ++++++++++++++++++++ tests/test_ucp_cross_lang.py | 9 +++- tests/test_ucp_jwks.py | 13 +++++ 4 files changed, 88 insertions(+), 9 deletions(-) create mode 100644 tests/fixtures/cross-lang/py-emoji-keys.json diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 76fbe2d..0e9ae30 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -43,12 +43,14 @@ @contextlib.contextmanager def _suppress_joserfc_eddsa_warning() -> Iterator[None]: - """Silence joserfc's exact RFC 9864 EdDSA SecurityWarning. + """Suppress joserfc's RFC-9864-deprecation SecurityWarning around JWS sign/verify. - UCP §6 requires EdDSA support and joserfc emits a per-call deprecation - warning. The filter is pinned to the exact message + class + joserfc emits this on every JWS operation that uses EdDSA, despite EdDSA + being the actively-recommended-by-IETF algorithm for new deployments. The + filter is pinned to the exact message + class (``joserfc.errors.SecurityWarning``: ``"EdDSA is deprecated via RFC 9864"``) - so a future, unrelated EdDSA warning still surfaces normally. + so any other SecurityWarning still surfaces normally. Key generation does + not emit this warning, so suppression has no effect there. """ from joserfc.errors import SecurityWarning # type: ignore[import-not-found] @@ -138,8 +140,7 @@ def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDS if alg == "EdDSA": from joserfc.jwk import OKPKey # type: ignore[import-not-found] - with _suppress_joserfc_eddsa_warning(): - priv = OKPKey.generate_key(crv="Ed25519", parameters={"kid": kid, "alg": alg, "use": "sig"}) + priv = OKPKey.generate_key(crv="Ed25519", parameters={"kid": kid, "alg": alg, "use": "sig"}) elif alg == "ES256": from joserfc.jwk import ECKey # type: ignore[import-not-found] @@ -357,13 +358,22 @@ def verify_ucp_profile( "duplicate_kid", f"JWKS contains {len(matches)} keys with kid={kid!r}; expected exactly one.", ) + matched = matches[0] # RFC 7517 §4.2: reject keys not intended for signature verification. - matched_use = matches[0].get("use") + matched_use = matched.get("use") if matched_use is not None and matched_use != "sig": raise UCPVerificationError( "unusable_key", f"JWK with kid={kid!r} has use={matched_use!r}; expected 'sig'.", ) + # RFC 7517 §4.4: a JWK with declared `alg` constrains its use to that algorithm. + header_alg = header.get("alg") + matched_alg = matched.get("alg") + if matched_alg is not None and matched_alg != header_alg: + raise UCPVerificationError( + "unusable_key", + f"JWK alg {matched_alg!r} does not match JWS header alg {header_alg!r}.", + ) stripped = {k: v for k, v in signed_profile.items() if k != "signature"} expected_payload = _canonicalize_profile(stripped) 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..7c226ac --- /dev/null +++ b/tests/fixtures/cross-lang/py-emoji-keys.json @@ -0,0 +1,51 @@ +{ + "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": "CX-4oqEqpxhUtsTrsaF2df7KBeIR0Wpe4bgwnlsMk8A", + "kid": "py-emoji-keys-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ], + "extras": { + "豈": 1, + "🍷": 2, + "a": 3 + }, + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6Mywi6LGIIjoxLCLwn423IjoyfSwibmFtZSI6IkVtb2ppIEtleXMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnt9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZW1vamkuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZW1vamkta2V5cy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJDWC00b3FFcXB4aFV0c1Ryc2FGMmRmN0tCZUlSMFdwZTRiZ3dubHNNazhBIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.aXPJHy4hcLxAF1zd9zLSZbbSMBP56BTeZVXY3V_Ywv4sqabLWgJGRmmp2iyJNamCFgYJ8jPIfd9nF1UU2R9WBg" + }, + "jwks": { + "keys": [ + { + "crv": "Ed25519", + "x": "CX-4oqEqpxhUtsTrsaF2df7KBeIR0Wpe4bgwnlsMk8A", + "kid": "py-emoji-keys-EdDSA", + "alg": "EdDSA", + "use": "sig", + "kty": "OKP" + } + ] + }, + "alg": "EdDSA", + "kid": "py-emoji-keys-EdDSA", + "generator": "python" +} diff --git a/tests/test_ucp_cross_lang.py b/tests/test_ucp_cross_lang.py index 96e77e6..a341981 100644 --- a/tests/test_ucp_cross_lang.py +++ b/tests/test_ucp_cross_lang.py @@ -30,8 +30,13 @@ def test_corpus_covers_canonical_scenarios() -> None: generators = {json.loads(p.read_text())["generator"] for p in FIXTURES} assert "node" in generators assert "python" in generators - # Each language ships 6 scenarios so cross-lang verify exercises all of them. + # Each language ships 6 base scenarios so cross-lang verify exercises all of them. for lang in ("node", "py"): for scenario in ("minimal", "es256-rails", "extras-int", "capability", "unicode", "multikey"): assert f"{lang}-{scenario}.json" in names, f"missing fixture {lang}-{scenario}.json" - assert len(FIXTURES) == 12 + # `py-emoji-keys.json` locks codepoint-aware key sort: Python sorts by Unicode + # codepoint by default, JS default sort orders by UTF-16 code units which + # diverges for supplementary-plane chars. The signed body covers BMP CJK + # Compatibility (U+8C48), non-BMP wine glass (U+1F377), and ASCII so both + # languages must explicitly sort by codepoint to maintain byte parity. + assert "py-emoji-keys.json" in names diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index fe09e71..353deee 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -389,6 +389,19 @@ def test_verify_rejects_unusable_key_use_enc(self) -> None: verify_ucp_profile(signed, build_jwks_response([enc_jwk])) assert exc.value.code == "unusable_key" + def test_verify_rejects_unusable_key_alg_mismatch(self) -> None: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + # JWKS advertises the same kid but with a wrong `alg` (RFC 7517 §4.4 violation): + # JWS header carries alg=EdDSA, JWK declares alg=ES256. + wrong_alg_jwk = {**key.public_jwk, "alg": "ES256"} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([wrong_alg_jwk])) + assert exc.value.code == "unusable_key" + assert "ES256" in str(exc.value) + assert "EdDSA" in str(exc.value) + @pytest.mark.parametrize("bad_sig", [42, None, [], {}]) def test_verify_rejects_non_string_signature(self, bad_sig: object) -> None: key = generate_ucp_signing_key(kid="k") From 9c2e09892c70fff0656efe34bfd738819964a89a Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 17:12:10 -0700 Subject: [PATCH 08/37] hardening: round-6 reviewer findings (cross-lang corpus + extras + reject set) Tighten cross-lang fixture corpus + extras-collision defenses + _reject_floats walking so cross-language byte parity has airtight regression coverage. Cross-lang fixture corpus: previous py-emoji-keys.json 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 in the verifier-side parity check. Regenerates py-emoji-keys.json with 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. UCPProfile.to_dict reserved set adds __class__, __dict__, __init__ for symmetry with node-commerce's prototype-pollution defense. Python's runtime model doesn't have JS-style __proto__ pollution but the reserved-name check guards against bidirectional vendor data passing through both SDKs and surprising downstream consumers. _reject_floats now walks set / frozenset in addition to list / tuple. A profile containing a set with a float would otherwise crash later in json.dumps with an untyped TypeError; catching it at the same surface as the list/tuple path keeps the error message consistent. README joserfc note adds tested-version pin (joserfc>=1.0.0,<2) mirroring node-commerce's "tested against jose v5.x; pin jose@^5". Tests: 800 pass + 3 skipped, 95.22% coverage. ruff + ty clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 6 ++- agentscore_commerce/identity/ucp.py | 7 ++- agentscore_commerce/identity/ucp_jwks.py | 2 +- .../fixtures/cross-lang/node-emoji-keys.json | 52 +++++++++++++++++++ tests/fixtures/cross-lang/py-emoji-keys.json | 13 ++--- tests/test_ucp.py | 24 +++++++++ tests/test_ucp_cross_lang.py | 23 +++++--- tests/test_ucp_jwks.py | 12 +++++ 8 files changed, 122 insertions(+), 17 deletions(-) create mode 100644 tests/fixtures/cross-lang/node-emoji-keys.json diff --git a/README.md b/README.md index 9dc559f..4f999e2 100644 --- a/README.md +++ b/README.md @@ -202,7 +202,11 @@ profile = build_ucp_profile( ) ``` -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 `joserfc` extra (`pip install agentscore-commerce[ucp]`): +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 `joserfc` extra (tested against joserfc v1.x; pin `joserfc>=1.0.0,<2`): + +```bash +pip install agentscore-commerce[ucp] +``` ```python from agentscore_commerce.identity import ( diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index b651e54..784e65a 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -184,7 +184,9 @@ def to_dict(self) -> dict[str, Any]: # Filter `extras` so a caller passing # ``extras={"signing_keys": [...]}`` can't silently destroy the # explicit field. Reserved-field collisions are rejected at - # build-time-equivalent surface. + # build-time-equivalent surface. ``__class__`` / ``__dict__`` / + # ``__init__`` mirror node-commerce's prototype-pollution defense + # against bidirectional vendor data passing through both SDKs. reserved = { "version", "spec", @@ -194,6 +196,9 @@ def to_dict(self) -> dict[str, Any]: "signing_keys", "name", "signature", + "__class__", + "__dict__", + "__init__", } for k, v in self.extras.items(): if k in reserved: diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 0e9ae30..1b0955e 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -179,7 +179,7 @@ def _reject_floats(value: Any) -> None: if isinstance(value, dict): for v in value.values(): _reject_floats(v) - elif isinstance(value, list | tuple): + elif isinstance(value, list | tuple | set | frozenset): for v in value: _reject_floats(v) 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/py-emoji-keys.json b/tests/fixtures/cross-lang/py-emoji-keys.json index 7c226ac..aa3b589 100644 --- a/tests/fixtures/cross-lang/py-emoji-keys.json +++ b/tests/fixtures/cross-lang/py-emoji-keys.json @@ -19,7 +19,7 @@ "signing_keys": [ { "crv": "Ed25519", - "x": "CX-4oqEqpxhUtsTrsaF2df7KBeIR0Wpe4bgwnlsMk8A", + "x": "xrTm5ZIZUbFC1_S2Yw5KZkf-9m8--CmwP6-bkttx-ik", "kid": "py-emoji-keys-EdDSA", "alg": "EdDSA", "use": "sig", @@ -27,17 +27,18 @@ } ], "extras": { - "豈": 1, - "🍷": 2, - "a": 3 + "a": 1, + "豈": 2, + "": 3, + "🍷": 4 }, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6Mywi6LGIIjoxLCLwn423IjoyfSwibmFtZSI6IkVtb2ppIEtleXMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnt9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZW1vamkuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZW1vamkta2V5cy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJDWC00b3FFcXB4aFV0c1Ryc2FGMmRmN0tCZUlSMFdwZTRiZ3dubHNNazhBIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.aXPJHy4hcLxAF1zd9zLSZbbSMBP56BTeZVXY3V_Ywv4sqabLWgJGRmmp2iyJNamCFgYJ8jPIfd9nF1UU2R9WBg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJ1Y3AtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJleHRyYXMiOnsiYSI6MSwi6LGIIjoyLCLugIAiOjMsIvCfjbciOjR9LCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InhyVG01WklaVWJGQzFfUzJZdzVLWmtmLTltOC0tQ213UDYtYmt0dHgtaWsifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.O2ENDO4OJreRSvRZqbyMzbQlaG3SKy_zsfMFqqV6HUkwvIzmpH2bot_XtJzyz23RTsBdwvZtLxQJOSnBFkIfBQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "CX-4oqEqpxhUtsTrsaF2df7KBeIR0Wpe4bgwnlsMk8A", + "x": "xrTm5ZIZUbFC1_S2Yw5KZkf-9m8--CmwP6-bkttx-ik", "kid": "py-emoji-keys-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/test_ucp.py b/tests/test_ucp.py index 493e198..903f8e9 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -1,5 +1,7 @@ """Tests for build_ucp_profile.""" +import pytest + from agentscore_commerce.identity import ( AGENTSCORE_UCP_CAPABILITY, AssessResult, @@ -109,3 +111,25 @@ def test_respects_agentscore_schema_url_override(): ) cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) assert cap.schema == "https://custom.example/schema.json" + + +@pytest.mark.parametrize( + "key", + [ + "version", + "spec", + "services", + "capabilities", + "payment_handlers", + "signing_keys", + "name", + "signature", + "__class__", + "__dict__", + "__init__", + ], +) +def test_extras_reserved_collision_rejected(key: str) -> None: + profile = build_ucp_profile(**_base_kwargs(), extras={key: "attacker"}) + with pytest.raises(ValueError, match="collides with a reserved profile field"): + profile.to_dict() diff --git a/tests/test_ucp_cross_lang.py b/tests/test_ucp_cross_lang.py index a341981..8acb769 100644 --- a/tests/test_ucp_cross_lang.py +++ b/tests/test_ucp_cross_lang.py @@ -30,13 +30,20 @@ def test_corpus_covers_canonical_scenarios() -> None: generators = {json.loads(p.read_text())["generator"] for p in FIXTURES} assert "node" in generators assert "python" in generators - # Each language ships 6 base scenarios so cross-lang verify exercises all of them. + # `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 lang in ("node", "py"): - for scenario in ("minimal", "es256-rails", "extras-int", "capability", "unicode", "multikey"): + for scenario in ( + "minimal", + "es256-rails", + "extras-int", + "capability", + "unicode", + "multikey", + "emoji-keys", + ): assert f"{lang}-{scenario}.json" in names, f"missing fixture {lang}-{scenario}.json" - # `py-emoji-keys.json` locks codepoint-aware key sort: Python sorts by Unicode - # codepoint by default, JS default sort orders by UTF-16 code units which - # diverges for supplementary-plane chars. The signed body covers BMP CJK - # Compatibility (U+8C48), non-BMP wine glass (U+1F377), and ASCII so both - # languages must explicitly sort by codepoint to maintain byte parity. - assert "py-emoji-keys.json" in names diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index 353deee..2a38e90 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -313,6 +313,18 @@ def test_accepts_int_and_string(self) -> None: signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + def test_rejects_float_in_set(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"vals": {0.5}}} + with pytest.raises(ValueError, match="rejects float"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_float_in_frozenset(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"vals": frozenset({0.25})}} + with pytest.raises(ValueError, match="rejects float"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + class TestUCPSigningKeyFromJWK: def test_round_trip_eddsa(self) -> None: From 2fed413f74a6dd55e86500570c4a8f8f98a6a4e8 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 17:38:06 -0700 Subject: [PATCH 09/37] fix(ucp): round-7 review parity fixes - verify_ucp_profile: compare signed payload against canonical body using hmac.compare_digest for constant-time equality, matching the node-commerce sibling. - UCPProfile.to_dict reserved set: replace Python dunders with __proto__ / constructor / prototype so the reserved set is byte-identical across the Node and Python SDKs (Node-signed profiles carrying those keys are rejected by both). - README: tell publishers to set Cache-Control: public, max-age=300 on /.well-known/jwks.json and wait at least that long before removing the old JWK during rotation. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- agentscore_commerce/identity/ucp.py | 13 ++++++------- agentscore_commerce/identity/ucp_jwks.py | 3 ++- tests/test_ucp.py | 6 +++--- 4 files changed, 12 insertions(+), 12 deletions(-) diff --git a/README.md b/README.md index 4f999e2..e0f5245 100644 --- a/README.md +++ b/README.md @@ -236,7 +236,7 @@ jwks = build_jwks_response([key.public_jwk]) **Persisting the private JWK.** Mint once via `generate_ucp_signing_key()`, serialize via `key.private_key.as_dict(private=True)`, store in your secret manager. On each container start, read the secret, `OKPKey.import_key(jwk_dict)` (or `ECKey.import_key` for ES256) to re-hydrate. Remote-signer flows (KMS-backed asymmetric keys) require subclassing the joserfc Key to delegate the sign hook; `OKPKey`/`ECKey` themselves only carry local key material. -**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. Drop the old JWK after 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. 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 to prevent a swap-after-sign attack. diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 784e65a..23298cd 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -183,10 +183,9 @@ def to_dict(self) -> dict[str, Any]: out["name"] = self.name # Filter `extras` so a caller passing # ``extras={"signing_keys": [...]}`` can't silently destroy the - # explicit field. Reserved-field collisions are rejected at - # build-time-equivalent surface. ``__class__`` / ``__dict__`` / - # ``__init__`` mirror node-commerce's prototype-pollution defense - # against bidirectional vendor data passing through both SDKs. + # explicit field. ``__proto__`` / ``constructor`` / ``prototype`` + # match the node-commerce reserved set so a Node-signed profile + # carrying those keys is rejected identically by both SDKs. reserved = { "version", "spec", @@ -196,9 +195,9 @@ def to_dict(self) -> dict[str, Any]: "signing_keys", "name", "signature", - "__class__", - "__dict__", - "__init__", + "__proto__", + "constructor", + "prototype", } for k, v in self.extras.items(): if k in reserved: diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 1b0955e..2772ed6 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -25,6 +25,7 @@ from __future__ import annotations import contextlib +import hmac import json import warnings from dataclasses import dataclass @@ -410,7 +411,7 @@ def verify_ucp_profile( # profile we received. ``deserialize_compact`` 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. - if obj.payload != expected_payload: + if not hmac.compare_digest(obj.payload, expected_payload): raise UCPVerificationError( "body_mismatch", "UCP profile body does not match the signed payload (tampered or non-canonical).", diff --git a/tests/test_ucp.py b/tests/test_ucp.py index 903f8e9..07eed4a 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -124,9 +124,9 @@ def test_respects_agentscore_schema_url_override(): "signing_keys", "name", "signature", - "__class__", - "__dict__", - "__init__", + "__proto__", + "constructor", + "prototype", ], ) def test_extras_reserved_collision_rejected(key: str) -> None: From ea8075972cde35780e7cf421b4d53524c2c9c790 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 18:19:11 -0700 Subject: [PATCH 10/37] hardening: round-9 reviewer findings (int-boundary cross-lang parity) Close cross-language byte-parity hole: Python's _reject_floats only walked for float, while Node's stableStringify hard-rejects integers outside Number.MAX_SAFE_INTEGER (2^53 - 1). A Python-signed profile with an oversized int produced a valid self-verifying envelope that Node could not parse. Sign-time rejection on the Python side now matches Node, plus a checked-in cross-lang fixture (int-boundary) covers the safe-edge integer for both languages. Renames _reject_floats to _reject_unsafe_numbers (private; not exported) and extends it to raise ValueError for any int whose magnitude exceeds 2^53 - 1. bool subclass of int still allowed; container walking (dict, list, tuple, set, frozenset) preserved. Tests: 7 new cases under TestUnsafeNumberRejection (max_safe boundary accept, min_safe boundary accept, 2^53 reject, 2^60 reject, -(2^53) reject, nested 2^60 reject, bool accept). Existing float rejection tests kept intact under the broader class name. Cross-lang fixture: tests/fixtures/cross-lang/{py,node}-int-boundary.json exercise max_safe_int / min_safe_int / small_int / neg_small_int / zero. Both fixtures verify in both languages. node-int-boundary.json was generated by the node-commerce companion script and copied here. scripts/generate_int_boundary_fixture.py: one-shot regenerator. Tests: 809 pass + 3 skipped, 95.22% coverage. ruff + ty clean. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- agentscore_commerce/identity/ucp_jwks.py | 40 ++++++++---- scripts/generate_int_boundary_fixture.py | 61 +++++++++++++++++++ .../cross-lang/node-int-boundary.json | 46 ++++++++++++++ .../fixtures/cross-lang/py-int-boundary.json | 48 +++++++++++++++ tests/test_ucp_cross_lang.py | 1 + tests/test_ucp_jwks.py | 44 ++++++++++++- 7 files changed, 229 insertions(+), 13 deletions(-) create mode 100644 scripts/generate_int_boundary_fixture.py 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 e0f5245..bbf0cf6 100644 --- a/README.md +++ b/README.md @@ -232,7 +232,7 @@ jwks = build_jwks_response([key.public_jwk]) `verify_ucp_profile` 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 raise `UCPVerificationError` (a `ValueError` subclass) with a discriminated `code` attribute (`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`). -`sign_ucp_profile` rejects profiles containing `float` values: cross-language float canonicalization is not stable, so use decimal strings (e.g. `"9.99"`) for any monetary or fractional fields you put in `extras`. +`sign_ucp_profile` rejects profiles containing `float` values and `int` values whose magnitude exceeds `Number.MAX_SAFE_INTEGER` (2^53 - 1): cross-language float canonicalization is not stable, and Python's arbitrary-width ints lose precision when JS verifiers reparse the canonical body. Use decimal strings (e.g. `"9.99"`) for monetary or fractional fields and for any integer that may exceed the safe range. **Persisting the private JWK.** Mint once via `generate_ucp_signing_key()`, serialize via `key.private_key.as_dict(private=True)`, store in your secret manager. On each container start, read the secret, `OKPKey.import_key(jwk_dict)` (or `ECKey.import_key` for ES256) to re-hydrate. Remote-signer flows (KMS-backed asymmetric keys) require subclassing the joserfc Key to delegate the sign hook; `OKPKey`/`ECKey` themselves only carry local key material. diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 2772ed6..bb0a6e4 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -41,6 +41,8 @@ _ALLOWED_ALGS = ("EdDSA", "ES256") _UCP_TYP = "ucp-profile+jws" +_MAX_SAFE_INT = 2**53 - 1 + @contextlib.contextmanager def _suppress_joserfc_eddsa_warning() -> Iterator[None]: @@ -159,14 +161,22 @@ def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDS return GeneratedUCPKey(private_key=priv, public_jwk=public_jwk) -def _reject_floats(value: Any) -> None: - """Walk ``value`` and raise if any non-integer ``float`` is encountered. +def _reject_unsafe_numbers(value: Any) -> None: + """Walk ``value`` and raise on any number that won't survive cross-language parity. + + Two failure modes are rejected: + + * Non-integer ``float`` values. Cross-language float canonicalization (RFC 8785 + §3.2.2.3) diverges between Python's ``json.dumps`` and Node's ``JSON.stringify`` + (e.g. ``1.0`` vs ``1``, ``1e-7`` vs ``1e-07``). Use decimal strings (``"9.99"``) + for monetary or fractional fields. + * ``int`` values whose magnitude exceeds ``Number.MAX_SAFE_INTEGER`` (2^53 - 1). + Python ints are arbitrary-width, but JS verifiers parse the canonical body via + ``JSON.parse`` which silently loses precision past 2^53. Use a decimal string + for any integer that may exceed the safe range. - Cross-language float canonicalization (RFC 8785 §3.2.2.3) diverges between - Python's ``json.dumps`` and Node's ``JSON.stringify`` (e.g. ``1.0`` vs ``1``, - ``1e-7`` vs ``1e-07``). Catching the drift at sign-time prevents - silent verifier-side failures in production. Use decimal strings (``"9.99"``) - for monetary or fractional fields. + Catching the drift at sign-time prevents silent verifier-side failures in + production. """ if isinstance(value, bool): return # bool subclasses int; allow. @@ -177,12 +187,19 @@ def _reject_floats(value: Any) -> None: "to preserve cross-language byte-parity." ) raise ValueError(msg) + if isinstance(value, int) and abs(value) > _MAX_SAFE_INT: + msg = ( + f"UCP profile canonicalization rejects integer {value} that exceeds " + "Number.MAX_SAFE_INTEGER (2^53 - 1). JS verifiers cannot losslessly " + "parse this; use a decimal string to preserve cross-language byte-parity." + ) + raise ValueError(msg) if isinstance(value, dict): for v in value.values(): - _reject_floats(v) + _reject_unsafe_numbers(v) elif isinstance(value, list | tuple | set | frozenset): for v in value: - _reject_floats(v) + _reject_unsafe_numbers(v) def _canonicalize_profile(profile: dict[str, Any]) -> bytes: @@ -192,13 +209,14 @@ def _canonicalize_profile(profile: dict[str, Any]) -> bytes: nesting level, returns UTF-8 JSON bytes. Cross-language byte-identical with the Node ``stableStringify`` output. - Throws ``ValueError`` on float input — see :func:`_reject_floats`. + Throws ``ValueError`` on float input or oversized int (see + :func:`_reject_unsafe_numbers`). UCP §6.2: "the JSON-serialized profile body, with ``signature`` removed and keys ordered lexicographically at every nesting level." """ stripped = {k: v for k, v in profile.items() if k != "signature"} - _reject_floats(stripped) + _reject_unsafe_numbers(stripped) # ``ensure_ascii=False`` so non-ASCII characters travel as UTF-8 (matches Node's # JSON.stringify default). ``sort_keys=True`` sorts keys at every level. Compact # separators avoid whitespace drift. diff --git a/scripts/generate_int_boundary_fixture.py b/scripts/generate_int_boundary_fixture.py new file mode 100644 index 0000000..8fe8dd8 --- /dev/null +++ b/scripts/generate_int_boundary_fixture.py @@ -0,0 +1,61 @@ +"""One-shot generator for the int-boundary cross-lang fixture (Python side). + +Writes ``tests/fixtures/cross-lang/py-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. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from agentscore_commerce.identity.ucp_jwks import ( + build_jwks_response, + generate_ucp_signing_key, + sign_ucp_profile, +) + +OUT = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "cross-lang" / "py-int-boundary.json" + +KID = "py-int-boundary-EdDSA" + + +def main() -> None: + key = generate_ucp_signing_key(kid=KID) + + 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": [key.public_jwk], + "extras": { + "max_safe_int": 9007199254740991, + "min_safe_int": -9007199254740991, + "small_int": 42, + "neg_small_int": -42, + "zero": 0, + }, + } + + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid=KID) + + fixture = { + "profile": signed, + "jwks": build_jwks_response([key.public_jwk]), + "alg": "EdDSA", + "kid": KID, + "generator": "python", + } + + OUT.write_text(json.dumps(fixture, indent=2) + "\n") + print(f"wrote {OUT}") + + +if __name__ == "__main__": + main() 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/test_ucp_cross_lang.py b/tests/test_ucp_cross_lang.py index 8acb769..2c5adea 100644 --- a/tests/test_ucp_cross_lang.py +++ b/tests/test_ucp_cross_lang.py @@ -45,5 +45,6 @@ def test_corpus_covers_canonical_scenarios() -> None: "unicode", "multikey", "emoji-keys", + "int-boundary", ): assert f"{lang}-{scenario}.json" in names, f"missing fixture {lang}-{scenario}.json" diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index 2a38e90..d58fa6c 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -282,7 +282,7 @@ def test_es256_signing_is_non_deterministic_but_both_verify(self) -> None: assert verify_ucp_profile(b, build_jwks_response([signer.public_jwk])) is True -class TestFloatRejection: +class TestUnsafeNumberRejection: def test_rejects_float_in_profile(self) -> None: signer = generate_ucp_signing_key(kid="k") profile = {**_base_profile([signer.public_jwk]), "extras": {"rate": 0.0125}} @@ -325,6 +325,48 @@ def test_rejects_float_in_frozenset(self) -> None: with pytest.raises(ValueError, match="rejects float"): sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + def test_accepts_max_safe_int_boundary(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"big": 2**53 - 1}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + def test_accepts_min_safe_int_boundary(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"big": -(2**53 - 1)}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + def test_rejects_int_above_max_safe_boundary(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"big": 2**53}} + with pytest.raises(ValueError, match="MAX_SAFE_INTEGER"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_int_well_above_max_safe(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"big": 2**60}} + with pytest.raises(ValueError, match="MAX_SAFE_INTEGER"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_int_below_min_safe(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"neg": -(2**53)}} + with pytest.raises(ValueError, match="MAX_SAFE_INTEGER"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_oversized_int_in_nested_list(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"a": [{"b": 2**60}]}} + with pytest.raises(ValueError, match="MAX_SAFE_INTEGER"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_accepts_bool_values(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"flag": True, "other": False}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + class TestUCPSigningKeyFromJWK: def test_round_trip_eddsa(self) -> None: From 2042ed267ead4f0b2d8805f463030ad495766cb3 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Fri, 8 May 2026 19:22:14 -0700 Subject: [PATCH 11/37] hardening: round-11 reviewer findings (typed-error contract + dict-key parity) Wrap verifier-side _canonicalize_profile call so JCS-incompatible numbers in a received profile emit a typed UCPVerificationError (body_mismatch) instead of a raw ValueError. Symmetric with the Node sibling fix. Reorder verify_ucp_profile shape checks profile-first so verify(None, malformed_jwks) returns no_signature, matching node-commerce. Walk dict keys in _reject_unsafe_numbers so {2**60: "a"} raises at sign-time. String dict keys (including numeric-looking ones like "1.5") remain allowed; bool keys remain allowed (bool is int). Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp_jwks.py | 23 ++++++--- tests/test_ucp_jwks.py | 64 ++++++++++++++++++++++++ 2 files changed, 79 insertions(+), 8 deletions(-) diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index bb0a6e4..d63596e 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -195,7 +195,8 @@ def _reject_unsafe_numbers(value: Any) -> None: ) raise ValueError(msg) if isinstance(value, dict): - for v in value.values(): + for k, v in value.items(): + _reject_unsafe_numbers(k) _reject_unsafe_numbers(v) elif isinstance(value, list | tuple | set | frozenset): for v in value: @@ -325,6 +326,12 @@ def verify_ucp_profile( from joserfc.jwk import KeySet # type: ignore[import-not-found] from joserfc.jws import JWSRegistry # type: ignore[import-not-found] + if not isinstance(signed_profile, dict): + raise UCPVerificationError( + "no_signature", + f"UCP verifier expected a profile dict; got {type(signed_profile).__name__}.", + ) + # JWKS shape guard so a malformed argument emits a typed UCPVerificationError # rather than a confusing kid_not_found / AttributeError. if not isinstance(jwks, dict) or not isinstance(jwks.get("keys"), list): @@ -333,12 +340,6 @@ def verify_ucp_profile( f"UCP verifier expected JWKS shape {{'keys': [...]}}; got {type(jwks).__name__}.", ) - if not isinstance(signed_profile, dict): - raise UCPVerificationError( - "no_signature", - f"UCP verifier expected a profile dict; got {type(signed_profile).__name__}.", - ) - sig = signed_profile.get("signature") if not sig: raise UCPVerificationError( @@ -395,7 +396,13 @@ def verify_ucp_profile( ) stripped = {k: v for k, v in signed_profile.items() if k != "signature"} - expected_payload = _canonicalize_profile(stripped) + try: + expected_payload = _canonicalize_profile(stripped) + except (ValueError, TypeError) as exc: + raise UCPVerificationError( + "body_mismatch", + f"Failed to canonicalize received profile for verification: {exc}", + ) from exc key_set = KeySet.import_key_set(cast("Any", {"keys": matches})) registry = JWSRegistry(algorithms=list(_ALLOWED_ALGS)) diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index d58fa6c..5318a6a 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -518,3 +518,67 @@ def test_verify_wraps_unrecognized_critical_header(self) -> None: assert exc.value.code == "unrecognized_critical_header" # Silence unused-import warnings — registry is referenced for the joserfc namespace. _ = jws, JWSRegistry + + +class TestVerifierCanonicalizationTypedErrors: + """Verifier-side canonicalize must NEVER leak raw ValueError; always UCPVerificationError(body_mismatch).""" + + def _make_signed(self) -> tuple[dict, dict]: + key = generate_ucp_signing_key(kid="k") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="k") + return signed, build_jwks_response([key.public_jwk]) + + def test_received_profile_with_float_raises_typed_body_mismatch(self) -> None: + signed, jwks = self._make_signed() + tampered = {**signed, "extras": {"n": 1.5}} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(tampered, jwks) + assert exc.value.code == "body_mismatch" + + def test_received_profile_with_oversized_int_raises_typed_body_mismatch(self) -> None: + signed, jwks = self._make_signed() + tampered = {**signed, "extras": {"n": 2**60}} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(tampered, jwks) + assert exc.value.code == "body_mismatch" + + def test_received_profile_with_nan_raises_typed_body_mismatch(self) -> None: + signed, jwks = self._make_signed() + tampered = {**signed, "extras": {"n": float("nan")}} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(tampered, jwks) + assert exc.value.code == "body_mismatch" + + +class TestRejectUnsafeNumbersDictKeys: + def test_sign_rejects_oversized_int_dict_key(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {2**60: "a"}} + with pytest.raises(ValueError, match="MAX_SAFE_INTEGER"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_sign_rejects_float_dict_key(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {1.5: "a"}} + with pytest.raises(ValueError, match="rejects float"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_sign_accepts_string_dict_keys_that_look_like_numbers(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"1.5": "a", "1152921504606846976": "b"}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + def test_sign_accepts_bool_dict_keys(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {True: "x"}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + +class TestVerifierErrorPrecedence: + def test_null_profile_with_malformed_jwks_returns_no_signature(self) -> None: + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(None, "not a jwks") # type: ignore[arg-type] + assert exc.value.code == "no_signature" From a4fd2f55e9bcfd8621f9e5b8c147193d11795384 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 03:19:36 -0700 Subject: [PATCH 12/37] hardening: round-17 reviewer findings (crit precedence cross-lang parity) Move the JWS `crit` header check ahead of the JWKS kid lookup in `verify_ucp_profile` so a crit-violating JWS with a missing/duplicate/unusable kid surfaces `unrecognized_critical_header` instead of `kid_not_found` / `duplicate_kid` / `unusable_key`. Matches node-commerce's manual peek order: typ -> alg -> kid -> crit -> kid_lookup. Without this, joserfc only enforces `crit` after its own kid lookup, so the two SDKs diverged on inputs like `{kid: 'real', crit: ['unknown']}` against a JWKS missing 'real'. Adds a regression-guard test that hand-crafts a JWS carrying both a crit violation AND a missing kid; existing crit + kid_not_found single-fault tests still pass because the new check fires only when `crit` is present. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp_jwks.py | 19 ++++++++++++ tests/test_ucp_jwks.py | 38 ++++++++++++++++++++++++ 2 files changed, 57 insertions(+) diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index d63596e..94f19f8 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -369,6 +369,25 @@ def verify_ucp_profile( if not kid or not isinstance(kid, str): raise UCPVerificationError("missing_kid", "UCP signature header missing `kid`.") + # UCP doesn't define any critical headers; any crit advertised is by definition + # unrecognized. Reject before the JWKS kid lookup so a crit-violating JWS with a + # missing/duplicate/unusable kid surfaces crit (not kid_not_found / duplicate_kid / + # unusable_key), matching node-commerce's manual peek order: + # typ -> alg -> kid -> crit -> kid_lookup. Cross-language ordering parity is + # non-obvious because joserfc's deserialize_compact only enforces crit AFTER + # the kid lookup, so we must check it here ourselves. + crit = header.get("crit") + if crit is not None: + if not isinstance(crit, list) or len(crit) == 0: + raise UCPVerificationError( + "malformed_jws", + f"JWS protected header crit must be a non-empty array; got {crit!r}.", + ) + raise UCPVerificationError( + "unrecognized_critical_header", + f"JWS protected header advertises unrecognized crit headers: {crit!r}.", + ) + keys_list = jwks.get("keys", []) if isinstance(jwks, dict) else [] matches = [k for k in keys_list if isinstance(k, dict) and k.get("kid") == kid] if not matches: diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index 5318a6a..427b18d 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -519,6 +519,44 @@ def test_verify_wraps_unrecognized_critical_header(self) -> None: # Silence unused-import warnings — registry is referenced for the joserfc namespace. _ = jws, JWSRegistry + def test_verify_crit_with_missing_kid_emits_unrecognized_critical_header(self) -> None: + """JWS with both crit violation AND missing kid emits unrecognized_critical_header, + matching node-commerce's typ -> alg -> kid -> crit precedence (regression guard for + the round-17 cross-SDK parity gap).""" + import base64 + + key = generate_ucp_signing_key(kid="real") + profile = _base_profile([key.public_jwk]) + # Hand-craft a JWS with header carrying both crit AND a kid that the JWKS does NOT contain. + canonical = ( + __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + header = { + "alg": "EdDSA", + "kid": "nonexistent", + "typ": "ucp-profile+jws", + "crit": ["fakething"], + "fakething": "x", + } + header_b64 = ( + base64.urlsafe_b64encode(__import__("json").dumps(header, separators=(",", ":")).encode()) + .rstrip(b"=") + .decode() + ) + payload_b64 = base64.urlsafe_b64encode(canonical).rstrip(b"=").decode() + signing_input = f"{header_b64}.{payload_b64}".encode() + sig = key.private_key.private_key.sign(signing_input) + sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b"=").decode() + jws_compact = f"{header_b64}.{payload_b64}.{sig_b64}" + + signed = {**profile, "signature": jws_compact} + # JWKS contains 'real' but the JWS advertises kid='nonexistent'. Without the + # crit-before-kid-lookup check the verifier would emit kid_not_found, diverging + # from node-commerce. + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "unrecognized_critical_header" + class TestVerifierCanonicalizationTypedErrors: """Verifier-side canonicalize must NEVER leak raw ValueError; always UCPVerificationError(body_mismatch).""" From 81ffd00f9193040b56566093a705814c6c1e4b41 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 03:27:14 -0700 Subject: [PATCH 13/37] fix(ucp): treat JWS crit=null as malformed_jws MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Round-17 added a `crit` precedence check gated on `if crit is not None`, which let JSON null fall through to joserfc's iter and surface a raw TypeError instead of the typed UCPVerificationError. Switch to a key-presence gate (`if "crit" in header`) so null hits the shape branch and emits malformed_jws, matching node-commerce's behavior. Adds regression tests for crit=null, crit=[], and crit="string" (all RFC 7515 §4.1.11 violations); the existing crit=['fakething'] coverage keeps unrecognized_critical_header. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp_jwks.py | 7 ++- tests/test_ucp_jwks.py | 56 ++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 2 deletions(-) diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 94f19f8..1a271da 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -376,8 +376,11 @@ def verify_ucp_profile( # typ -> alg -> kid -> crit -> kid_lookup. Cross-language ordering parity is # non-obvious because joserfc's deserialize_compact only enforces crit AFTER # the kid lookup, so we must check it here ourselves. - crit = header.get("crit") - if crit is not None: + # Gate on key-presence (not `is not None`) so that JSON `null` falls through to + # the shape check and surfaces typed `malformed_jws`, not joserfc's raw TypeError + # when it tries to iterate `None`. RFC 7515 §4.1.11 requires a non-empty array. + if "crit" in header: + crit = header["crit"] if not isinstance(crit, list) or len(crit) == 0: raise UCPVerificationError( "malformed_jws", diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index 427b18d..28ba6f4 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -6,6 +6,7 @@ from agentscore_commerce.identity.ucp import UCPSigningKey from agentscore_commerce.identity.ucp_jwks import ( + GeneratedUCPKey, UCPVerificationError, build_jwks_response, generate_ucp_signing_key, @@ -557,6 +558,61 @@ def test_verify_crit_with_missing_kid_emits_unrecognized_critical_header(self) - verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) assert exc.value.code == "unrecognized_critical_header" + def _hand_craft_jws_with_crit(self, key: GeneratedUCPKey, profile: dict, crit_value: object) -> str: + """Build a JWS whose protected header carries an arbitrary `crit` value + (including JSON null / non-list shapes) by signing the raw bytes directly. + joserfc's high-level sign API would reject these on the way in.""" + import base64 + + canonical = ( + __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") + ) + header = {"alg": "EdDSA", "kid": "real", "typ": "ucp-profile+jws", "crit": crit_value} + header_b64 = ( + base64.urlsafe_b64encode(__import__("json").dumps(header, separators=(",", ":")).encode()) + .rstrip(b"=") + .decode() + ) + payload_b64 = base64.urlsafe_b64encode(canonical).rstrip(b"=").decode() + signing_input = f"{header_b64}.{payload_b64}".encode() + sig = key.private_key.private_key.sign(signing_input) + sig_b64 = base64.urlsafe_b64encode(sig).rstrip(b"=").decode() + return f"{header_b64}.{payload_b64}.{sig_b64}" + + def test_verify_crit_null_emits_malformed_jws(self) -> None: + """JWS protected header with crit=null is malformed (RFC 7515 §4.1.11 + requires a non-empty array). Regression guard: the previous `is not None` + gate let JSON null fall through to joserfc's iterate-crit, which crashed + with a raw TypeError instead of the typed UCPVerificationError. Node + sibling already maps crit=null to malformed_jws.""" + key = generate_ucp_signing_key(kid="real") + profile = _base_profile([key.public_jwk]) + jws_compact = self._hand_craft_jws_with_crit(key, profile, None) + signed = {**profile, "signature": jws_compact} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "malformed_jws" + + def test_verify_crit_empty_array_emits_malformed_jws(self) -> None: + """RFC 7515 §4.1.11 requires `crit` be a non-empty array.""" + key = generate_ucp_signing_key(kid="real") + profile = _base_profile([key.public_jwk]) + jws_compact = self._hand_craft_jws_with_crit(key, profile, []) + signed = {**profile, "signature": jws_compact} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "malformed_jws" + + def test_verify_crit_string_emits_malformed_jws(self) -> None: + """`crit` must be an array per RFC 7515 §4.1.11; a string is malformed.""" + key = generate_ucp_signing_key(kid="real") + profile = _base_profile([key.public_jwk]) + jws_compact = self._hand_craft_jws_with_crit(key, profile, "fakething") + signed = {**profile, "signature": jws_compact} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "malformed_jws" + class TestVerifierCanonicalizationTypedErrors: """Verifier-side canonicalize must NEVER leak raw ValueError; always UCPVerificationError(body_mismatch).""" From 30133925b2d895e9f1b2ac52fcc7b033fce0b801 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 03:55:00 -0700 Subject: [PATCH 14/37] hardening(identity): reject U+2028/U+2029 in profile canonicalization 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) --- agentscore_commerce/identity/ucp_jwks.py | 21 +++++++- tests/test_ucp_jwks.py | 67 ++++++++++++++++++++++++ 2 files changed, 86 insertions(+), 2 deletions(-) diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 1a271da..9f3bff3 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -162,9 +162,9 @@ def generate_ucp_signing_key(*, kid: str, alg: Literal["EdDSA", "ES256"] = "EdDS def _reject_unsafe_numbers(value: Any) -> None: - """Walk ``value`` and raise on any number that won't survive cross-language parity. + """Walk ``value`` and raise on anything that won't survive cross-language parity. - Two failure modes are rejected: + Three failure modes are rejected: * Non-integer ``float`` values. Cross-language float canonicalization (RFC 8785 §3.2.2.3) diverges between Python's ``json.dumps`` and Node's ``JSON.stringify`` @@ -174,6 +174,12 @@ def _reject_unsafe_numbers(value: Any) -> None: Python ints are arbitrary-width, but JS verifiers parse the canonical body via ``JSON.parse`` which silently loses precision past 2^53. Use a decimal string for any integer that may exceed the safe range. + * Strings containing U+2028 (LINE SEPARATOR) or U+2029 (PARAGRAPH SEPARATOR). + Pre-ES2019 V8 (and any environment whose ``JSON.stringify`` still escapes + these codepoints) emits the escaped sequences while + ``json.dumps(ensure_ascii=False)`` emits them raw, so the canonical bytes + would diverge across the Node and Python siblings. Mirror of the rejection + in ``core/api/src/lib/canonicalize.ts``. Catching the drift at sign-time prevents silent verifier-side failures in production. @@ -194,6 +200,17 @@ def _reject_unsafe_numbers(value: Any) -> None: "parse this; use a decimal string to preserve cross-language byte-parity." ) raise ValueError(msg) + if isinstance(value, str): + if "\u2028" in value or "\u2029" in value: + msg = ( + "UCP profile 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)." + ) + raise ValueError(msg) + return if isinstance(value, dict): for k, v in value.items(): _reject_unsafe_numbers(k) diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index 28ba6f4..26b270e 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -671,6 +671,73 @@ def test_sign_accepts_bool_dict_keys(self) -> None: assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True +# U+2028 / U+2029 named via escape so the RUF001 ambiguous-character lint +# doesn't fire on the test inputs (the codepoints are intentional, not typos). +_U2028 = "\u2028" +_U2029 = "\u2029" + + +class TestLineParagraphSeparatorRejection: + """U+2028 / U+2029 are escaped by pre-ES2019 V8 (``JSON.stringify`` emits + the escaped sequences) but emitted raw by ``json.dumps(ensure_ascii=False)``. + + Modern V8 emits them raw too, so the divergence is theoretical on today's + Node, but the rejection mirrors core/api/src/lib/canonicalize.ts so the + contract stays symmetric for any pre-ES2019 verifier path (older V8, + browser-side verifier code). + """ + + def test_rejects_u2028_at_top_level(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"note": f"before{_U2028}after"}} + with pytest.raises(ValueError, match="U\\+2028"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_u2029_at_top_level(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"note": f"before{_U2029}after"}} + with pytest.raises(ValueError, match="U\\+2029"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_u2028_nested_in_list(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"items": ["ok", f"bad{_U2028}tail"]}} + with pytest.raises(ValueError, match="U\\+2028"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_u2029_nested_in_list(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"items": ["ok", f"bad{_U2029}tail"]}} + with pytest.raises(ValueError, match="U\\+2029"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_u2028_nested_in_dict_value(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"deep": {"inner": f"before{_U2028}after"}}} + with pytest.raises(ValueError, match="U\\+2028"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_u2029_nested_in_dict_value(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"deep": {"inner": f"before{_U2029}after"}}} + with pytest.raises(ValueError, match="U\\+2029"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_u2028_in_dict_key(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {f"bad{_U2028}key": "value"}} + with pytest.raises(ValueError, match="U\\+2028"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_accepts_u2027_sanity_case(self) -> None: + # U+2027 (HYPHENATION POINT) is a different codepoint, not a target of + # the rejection. Confirms we're matching exactly U+2028 / U+2029. + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"note": "before\u2027after"}} + signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True + + class TestVerifierErrorPrecedence: def test_null_profile_with_malformed_jwks_returns_no_signature(self) -> None: with pytest.raises(UCPVerificationError) as exc: From 7fe7f32a8f15faaa3bf6f061f36090e23f871306 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 04:11:46 -0700 Subject: [PATCH 15/37] docs(identity): fix sign_ucp_profile docstring example constructor The example used `UCPSigningKey(**key.public_jwk)`, which raises TypeError because UCPSigningKey only accepts kid/kty/alg/use/crv/extras (the JWK `x`/`y` fields belong in extras). Switch to the `UCPSigningKey.from_jwk(...)` classmethod, matching the runnable example in examples/signed_ucp_merchant.py. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp_jwks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 9f3bff3..55bfaaf 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -260,7 +260,7 @@ def sign_ucp_profile( Example:: - profile = build_ucp_profile(..., signing_keys=[UCPSigningKey(**key.public_jwk)]) + profile = build_ucp_profile(..., signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)]) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid='merchant-2026-05') """ _load_joserfc() From acda9561cd7a117026575ff16495fa2e0d2e857d Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 04:50:14 -0700 Subject: [PATCH 16/37] hardening(identity): align build_ucp_profile claims coalescing with node sibling The API can return account_verification with either null or empty-string for un-set fields depending on the row state. The python builder used dict.get(k, DEFAULT) for age_bracket/jurisdiction, which returns None verbatim when the key is present-but-null instead of falling through to the default. The node sibling used `??` (collapse null/undefined only, pass empty-string verbatim). 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 python to `dict.get(k) or DEFAULT` for age_bracket and jurisdiction so null AND empty-string both fall through to the schema default. The kyc_level and verified_at branches already used `or` semantics; verified and left as-is. Node sibling switched to `||` in the same round. Adds a data-driven-claims cross-lang fixture that, unlike the rest of the corpus, exercises build_ucp_profile's actual data path (constructs a synthetic AssessResult 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) --- agentscore_commerce/identity/ucp.py | 10 ++- .../generate_data_driven_claims_fixture.py | 80 +++++++++++++++++++ .../cross-lang/node-data-driven-claims.json | 57 +++++++++++++ .../cross-lang/py-data-driven-claims.json | 57 +++++++++++++ tests/test_ucp.py | 46 +++++++++++ tests/test_ucp_cross_lang.py | 5 ++ 6 files changed, 253 insertions(+), 2 deletions(-) create mode 100644 scripts/generate_data_driven_claims_fixture.py 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/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 23298cd..0f003f3 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -261,12 +261,18 @@ async def ucp_profile(): operator_verification = {} if not isinstance(account_verification, dict): account_verification = {} + # `dict.get(k) or DEFAULT` (not `dict.get(k, DEFAULT)`) coerces both a + # missing key AND a present-but-falsy (None / "") value to the default, + # matching the node sibling's `||` semantics. 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. claims = { "operator_id": data.resolved_operator, "kyc_level": account_verification.get("kyc_level") or operator_verification.get("level") or "none", "sanctions_clear": account_verification.get("sanctions_clear") is True, - "age_bracket": account_verification.get("age_bracket", "unknown"), - "jurisdiction": account_verification.get("jurisdiction", ""), + "age_bracket": account_verification.get("age_bracket") or "unknown", + "jurisdiction": account_verification.get("jurisdiction") or "", "verified_at": account_verification.get("verified_at") or operator_verification.get("verified_at"), "verify_url": data.verify_url, "issuer": "https://agentscore.sh", diff --git a/scripts/generate_data_driven_claims_fixture.py b/scripts/generate_data_driven_claims_fixture.py new file mode 100644 index 0000000..3122928 --- /dev/null +++ b/scripts/generate_data_driven_claims_fixture.py @@ -0,0 +1,80 @@ +"""One-shot generator for the data-driven-claims cross-lang fixture (Python side). + +Writes ``tests/fixtures/cross-lang/py-data-driven-claims.json``. Unlike the +other cross-lang fixtures (which hand-craft the ``agentscore-identity`` +capability), this one EXERCISES ``build_ucp_profile``'s data path: it +constructs a synthetic ``AssessResult`` with the API-shape "missing" sentinels +(empty string for kyc_level, None 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. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from agentscore_commerce.identity import ( + AssessResult, + UCPService, + UCPSigningKey, + build_ucp_profile, +) +from agentscore_commerce.identity.ucp_jwks import ( + build_jwks_response, + generate_ucp_signing_key, + sign_ucp_profile, +) + +OUT = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "cross-lang" / "py-data-driven-claims.json" + +KID = "py-data-driven-claims-EdDSA" + + +def main() -> None: + key = generate_ucp_signing_key(kid=KID) + + result = AssessResult( + allow=True, + resolved_operator="op_data_driven", + verify_url="https://agentscore.sh/verify/op_data_driven", + raw={ + "account_verification": { + # Empty string is the API's "set but unknown" shape for some + # columns; None 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": None, + "jurisdiction": None, + "verified_at": None, + }, + }, + ) + + profile = build_ucp_profile( + name="Data Driven Claims Merchant", + services=[UCPService(type="rest", url="https://d.example.com")], + payment_handlers=[], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + data=result, + ) + + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=KID) + + fixture = { + "profile": signed, + "jwks": build_jwks_response([key.public_jwk]), + "alg": "EdDSA", + "kid": KID, + "generator": "python", + } + + OUT.write_text(json.dumps(fixture, indent=2) + "\n") + print(f"wrote {OUT}") + + +if __name__ == "__main__": + main() 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/test_ucp.py b/tests/test_ucp.py index 07eed4a..371a7c9 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -133,3 +133,49 @@ def test_extras_reserved_collision_rejected(key: str) -> None: profile = build_ucp_profile(**_base_kwargs(), extras={key: "attacker"}) with pytest.raises(ValueError, match="collides with a reserved profile field"): profile.to_dict() + + +# 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. + + +def _claims_of(account_verification: dict) -> dict: + result = AssessResult( + allow=True, + resolved_operator="op_abc", + raw={"account_verification": account_verification}, + ) + profile = build_ucp_profile(**_base_kwargs(), data=result) + d = profile.to_dict() + cap = next(c for c in d["capabilities"] if c["name"] == AGENTSCORE_UCP_CAPABILITY) + return cap["claims"] + + +def test_coerces_empty_string_kyc_level_to_none() -> None: + assert _claims_of({"kyc_level": ""})["kyc_level"] == "none" + + +def test_coerces_null_age_bracket_to_unknown() -> None: + assert _claims_of({"age_bracket": None})["age_bracket"] == "unknown" + + +def test_coerces_empty_string_age_bracket_to_unknown() -> None: + assert _claims_of({"age_bracket": ""})["age_bracket"] == "unknown" + + +def test_coerces_null_jurisdiction_to_empty_string() -> None: + assert _claims_of({"jurisdiction": None})["jurisdiction"] == "" + + +def test_coerces_empty_string_jurisdiction_to_empty_string() -> None: + assert _claims_of({"jurisdiction": ""})["jurisdiction"] == "" + + +def test_coerces_null_verified_at_to_none() -> None: + assert _claims_of({"verified_at": None})["verified_at"] is None + + +def test_coerces_empty_string_verified_at_to_none() -> None: + assert _claims_of({"verified_at": ""})["verified_at"] is None diff --git a/tests/test_ucp_cross_lang.py b/tests/test_ucp_cross_lang.py index 2c5adea..3466ad7 100644 --- a/tests/test_ucp_cross_lang.py +++ b/tests/test_ucp_cross_lang.py @@ -46,5 +46,10 @@ def test_corpus_covers_canonical_scenarios() -> None: "multikey", "emoji-keys", "int-boundary", + # `data-driven-claims` is the only fixture in the corpus that + # exercises ``build_ucp_profile`` / ``buildUCPProfile``'s data path + # (vs. hand-crafted capabilities). Catches drift in + # ``account_verification`` coalescing. + "data-driven-claims", ): assert f"{lang}-{scenario}.json" in names, f"missing fixture {lang}-{scenario}.json" From 48bced1be8e5b43377b9d78fb8041d41ccf3e81b Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 05:27:48 -0700 Subject: [PATCH 17/37] hardening(identity): typed-field fallback + per-element extras guard + set/frozenset reject Three reviewer LOW findings on the UCP profile path: * `build_ucp_profile` now falls back to the typed `AssessResult.operator_verification` (and ad-hoc `account_verification`) field when `data.raw` doesn't carry the block. Production callers populate `raw`, but a hand-constructed `AssessResult(raw=None)` was silently dropping the operator verification block, diverging from the node sibling's typed-field read path. * `UCPService`, `UCPCapability`, and `UCPSigningKey` now mirror `UCPProfile.to_dict`'s reserved-key collision guard so vendor `extras` can't silently overwrite a canonical declared field via `out.update(extras)`. `UCPPaymentHandler` has no `extras` attribute and is unaffected. * `_reject_unsafe_numbers` now rejects `set` / `frozenset` outright with a typed `ValueError` (mirroring node's `stableStringify: Set values are not allowed`). Previously an empty set or a set-of-valid-strings fell through cleanly and surfaced a raw `TypeError` from `json.dumps` later. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp.py | 41 ++++++- agentscore_commerce/identity/ucp_jwks.py | 13 ++- tests/test_ucp.py | 131 +++++++++++++++++++++++ tests/test_ucp_jwks.py | 26 ++++- 4 files changed, 203 insertions(+), 8 deletions(-) diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 0f003f3..84d69ea 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -50,6 +50,8 @@ class UCPSigningKey: extras: dict[str, Any] = field(default_factory=dict) """Additional JWK fields (x, y, n, e, etc.) merged into the serialized output.""" + _RESERVED = frozenset({"kid", "kty", "alg", "use", "crv"}) + def to_dict(self) -> dict[str, Any]: out: dict[str, Any] = {"kid": self.kid, "kty": self.kty} if self.alg is not None: @@ -58,7 +60,11 @@ def to_dict(self) -> dict[str, Any]: out["use"] = self.use if self.crv is not None: out["crv"] = self.crv - out.update(self.extras) + for k, v in self.extras.items(): + if k in self._RESERVED: + msg = f"UCPSigningKey.extras key {k!r} collides with a reserved field; rejected." + raise ValueError(msg) + out[k] = v return out @classmethod @@ -110,13 +116,19 @@ class UCPService: version: str | None = None extras: dict[str, Any] = field(default_factory=dict) + _RESERVED = frozenset({"type", "url", "version"}) + def to_dict(self) -> dict[str, Any]: out: dict[str, Any] = {"type": self.type} if self.url is not None: out["url"] = self.url if self.version is not None: out["version"] = self.version - out.update(self.extras) + for k, v in self.extras.items(): + if k in self._RESERVED: + msg = f"UCPService.extras key {k!r} collides with a reserved field; rejected." + raise ValueError(msg) + out[k] = v return out @@ -129,13 +141,19 @@ class UCPCapability: version: str | None = None extras: dict[str, Any] = field(default_factory=dict) + _RESERVED = frozenset({"name", "schema", "version"}) + def to_dict(self) -> dict[str, Any]: out: dict[str, Any] = {"name": self.name} if self.schema is not None: out["schema"] = self.schema if self.version is not None: out["version"] = self.version - out.update(self.extras) + for k, v in self.extras.items(): + if k in self._RESERVED: + msg = f"UCPCapability.extras key {k!r} collides with a reserved field; rejected." + raise ValueError(msg) + out[k] = v return out @@ -256,7 +274,24 @@ async def ucp_profile(): if data is not None and data.resolved_operator: raw = data.raw or {} operator_verification = raw.get("operator_verification") if isinstance(raw, dict) else None + if not operator_verification: + # Fallback to the typed AssessResult.operator_verification field when + # `raw` doesn't carry it. Mirrors the node sibling's typed-field read + # path so a hand-constructed AssessResult (no `raw`) still surfaces + # the operator verification block in the UCP capability claims. + typed_op = getattr(data, "operator_verification", None) + if typed_op is not None and not isinstance(typed_op, dict): + # Convert OperatorVerification dataclass to a plain dict. + operator_verification = { + "level": getattr(typed_op, "level", None), + "operator_type": getattr(typed_op, "operator_type", None), + "verified_at": getattr(typed_op, "verified_at", None), + } + else: + operator_verification = typed_op account_verification = raw.get("account_verification") if isinstance(raw, dict) else None + if not account_verification: + account_verification = getattr(data, "account_verification", None) if not isinstance(operator_verification, dict): operator_verification = {} if not isinstance(account_verification, dict): diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 55bfaaf..d04848a 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -211,11 +211,22 @@ def _reject_unsafe_numbers(value: Any) -> None: ) raise ValueError(msg) return + # Reject set / frozenset with a typed message (mirrors the node sibling's + # "Set values are not allowed" rejection in stableStringify). Without this, + # an empty set or a set-of-valid-strings falls through `_reject_unsafe_numbers` + # cleanly and surfaces a raw `TypeError` from `json.dumps` later. Sets aren't + # representable in JSON; convert to a sorted list before passing. + if isinstance(value, set | frozenset): + msg = ( + f"{type(value).__name__} values are not allowed in canonicalized JSON. " + "Convert to a sorted list before passing." + ) + raise ValueError(msg) if isinstance(value, dict): for k, v in value.items(): _reject_unsafe_numbers(k) _reject_unsafe_numbers(v) - elif isinstance(value, list | tuple | set | frozenset): + elif isinstance(value, list | tuple): for v in value: _reject_unsafe_numbers(v) diff --git a/tests/test_ucp.py b/tests/test_ucp.py index 371a7c9..8ce6a6c 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -5,6 +5,7 @@ from agentscore_commerce.identity import ( AGENTSCORE_UCP_CAPABILITY, AssessResult, + OperatorVerification, UCPCapability, UCPPaymentHandler, UCPService, @@ -179,3 +180,133 @@ def test_coerces_null_verified_at_to_none() -> None: def test_coerces_empty_string_verified_at_to_none() -> None: assert _claims_of({"verified_at": ""})["verified_at"] is None + + +# Typed-field fallback: production callers populate `data.raw`, but a +# hand-constructed AssessResult (no raw) should still surface the operator +# verification block via the typed `AssessResult.operator_verification` field +# and the (optional) `account_verification` attribute. Mirrors the node sibling's +# typed-field read path. + + +def test_typed_operator_verification_fallback_when_raw_is_none() -> None: + result = AssessResult( + allow=True, + resolved_operator="op_typed", + operator_verification=OperatorVerification( + level="enhanced", + operator_type="api", + verified_at="2026-04-01T00:00:00Z", + ), + raw=None, + ) + profile = build_ucp_profile(**_base_kwargs(), data=result) + d = profile.to_dict() + cap = next(c for c in d["capabilities"] if c["name"] == AGENTSCORE_UCP_CAPABILITY) + claims = cap["claims"] + assert claims["operator_id"] == "op_typed" + assert claims["kyc_level"] == "enhanced" + assert claims["verified_at"] == "2026-04-01T00:00:00Z" + + +def test_typed_account_verification_fallback_via_setattr() -> None: + # `AssessResult` doesn't declare `account_verification` as a typed field, but + # a caller can still attach one ad-hoc. The fallback reads it via getattr so + # parity with the node sibling holds whichever way the caller populates it. + result = AssessResult( + allow=True, + resolved_operator="op_typed", + operator_verification=OperatorVerification(level="verified"), + raw=None, + ) + result.account_verification = { # type: ignore[attr-defined] + "kyc_level": "verified", + "age_bracket": "21+", + "jurisdiction": "US", + "sanctions_clear": True, + } + profile = build_ucp_profile(**_base_kwargs(), data=result) + d = profile.to_dict() + cap = next(c for c in d["capabilities"] if c["name"] == AGENTSCORE_UCP_CAPABILITY) + claims = cap["claims"] + assert claims["kyc_level"] == "verified" + assert claims["age_bracket"] == "21+" + assert claims["jurisdiction"] == "US" + assert claims["sanctions_clear"] is True + + +def test_raw_takes_precedence_over_typed_fallback() -> None: + # When raw carries `operator_verification`, the typed-field fallback is NOT + # consulted. Production callers populate raw and the typed fields stay + # in sync; this test pins the precedence so a typed mismatch can't silently + # override the raw payload. + result = AssessResult( + allow=True, + resolved_operator="op_xyz", + operator_verification=OperatorVerification(level="enhanced"), + raw={ + "operator_verification": {"level": "verified"}, + "account_verification": {"kyc_level": "verified"}, + }, + ) + profile = build_ucp_profile(**_base_kwargs(), data=result) + cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) + # `kyc_level` reads from raw account_verification.kyc_level first. + assert cap.extras["claims"]["kyc_level"] == "verified" + + +# Per-element to_dict reserved-key collision guard. Mirrors the parent +# UCPProfile.to_dict guard so vendor extras can't silently overwrite a canonical +# field on UCPService / UCPCapability / UCPSigningKey via `out.update(extras)`. + + +def test_ucp_service_extras_collision_with_type_rejected() -> None: + svc = UCPService(type="rest", extras={"type": "different"}) + with pytest.raises(ValueError, match=r"UCPService\.extras key 'type' collides"): + svc.to_dict() + + +def test_ucp_service_extras_collision_with_url_rejected() -> None: + svc = UCPService(type="rest", url="https://x.example", extras={"url": "https://attacker.example"}) + with pytest.raises(ValueError, match=r"UCPService\.extras key 'url' collides"): + svc.to_dict() + + +def test_ucp_service_extras_non_reserved_pass_through() -> None: + svc = UCPService(type="rest", url="https://x.example", extras={"region": "us-west-1"}) + assert svc.to_dict() == {"type": "rest", "url": "https://x.example", "region": "us-west-1"} + + +def test_ucp_capability_extras_collision_with_name_rejected() -> None: + cap = UCPCapability(name="checkout", extras={"name": "different"}) + with pytest.raises(ValueError, match=r"UCPCapability\.extras key 'name' collides"): + cap.to_dict() + + +def test_ucp_capability_extras_collision_with_schema_rejected() -> None: + cap = UCPCapability(name="checkout", schema="https://x/y", extras={"schema": "https://attacker"}) + with pytest.raises(ValueError, match=r"UCPCapability\.extras key 'schema' collides"): + cap.to_dict() + + +def test_ucp_capability_extras_non_reserved_pass_through() -> None: + cap = UCPCapability(name="checkout", extras={"claims": {"k": "v"}}) + assert cap.to_dict() == {"name": "checkout", "claims": {"k": "v"}} + + +def test_ucp_signing_key_extras_collision_with_kid_rejected() -> None: + sk = UCPSigningKey(kid="me", kty="EC", extras={"kid": "attacker"}) + with pytest.raises(ValueError, match=r"UCPSigningKey\.extras key 'kid' collides"): + sk.to_dict() + + +def test_ucp_signing_key_extras_collision_with_kty_rejected() -> None: + sk = UCPSigningKey(kid="me", kty="EC", extras={"kty": "RSA"}) + with pytest.raises(ValueError, match=r"UCPSigningKey\.extras key 'kty' collides"): + sk.to_dict() + + +def test_ucp_signing_key_extras_non_reserved_pass_through() -> None: + sk = UCPSigningKey(kid="me", kty="EC", alg="ES256", crv="P-256", extras={"x": "abc", "y": "def"}) + out = sk.to_dict() + assert out == {"kid": "me", "kty": "EC", "alg": "ES256", "crv": "P-256", "x": "abc", "y": "def"} diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index 26b270e..3210326 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -314,16 +314,34 @@ def test_accepts_int_and_string(self) -> None: signed = sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True - def test_rejects_float_in_set(self) -> None: + def test_rejects_set_values_outright(self) -> None: + # `set` is not representable in JSON; the canonicalizer rejects it with a + # typed message before any element-level checks run. Mirrors node's + # `stableStringify: Set values are not allowed`. signer = generate_ucp_signing_key(kid="k") profile = {**_base_profile([signer.public_jwk]), "extras": {"vals": {0.5}}} - with pytest.raises(ValueError, match="rejects float"): + with pytest.raises(ValueError, match="set values are not allowed"): sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") - def test_rejects_float_in_frozenset(self) -> None: + def test_rejects_frozenset_values_outright(self) -> None: signer = generate_ucp_signing_key(kid="k") profile = {**_base_profile([signer.public_jwk]), "extras": {"vals": frozenset({0.25})}} - with pytest.raises(ValueError, match="rejects float"): + with pytest.raises(ValueError, match="frozenset values are not allowed"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_empty_set_with_typed_message(self) -> None: + # Empty set + set-of-valid-strings would fall through `_reject_unsafe_numbers` + # cleanly and surface a raw `TypeError` from `json.dumps` later. The typed + # reject ensures callers get a guiding ValueError instead. + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"vals": set()}} + with pytest.raises(ValueError, match="set values are not allowed"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_set_of_valid_strings_with_typed_message(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"vals": {"valid", "strings"}}} + with pytest.raises(ValueError, match="set values are not allowed"): sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") def test_accepts_max_safe_int_boundary(self) -> None: From 1bc9ef3ec4c24a2aff246b18b0c2b89000ec91ce Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 05:56:09 -0700 Subject: [PATCH 18/37] hardening(identity): round-26 UCP signing reviewer findings verify_ucp_profile now treats explicit JSON null on JWK.use / JWK.alg as absent (skip-on-null) rather than rejecting via joserfc's stricter KeySet.import_key_set validation; both fields are optional per RFC 7517 and the Node sibling now uses ``!= null`` semantics, so a JWK with ``"use": null`` / ``"alg": null`` should pass language-symmetric verify. The ``is not None`` checks above already skip null; we now also drop the explicit-null entries before handing the JWK to joserfc so its key parameter registry doesn't reject them downstream. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp_jwks.py | 9 +++++++ tests/test_ucp_jwks.py | 33 ++++++++++++++++++++++++ 2 files changed, 42 insertions(+) diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index d04848a..9772e24 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -430,6 +430,10 @@ def verify_ucp_profile( ) matched = matches[0] # 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 the Node sibling's ``!= null`` semantics in + # ucp-jwks.ts and the two languages stay symmetric. matched_use = matched.get("use") if matched_use is not None and matched_use != "sig": raise UCPVerificationError( @@ -444,6 +448,11 @@ def verify_ucp_profile( "unusable_key", f"JWK alg {matched_alg!r} does not match JWS header alg {header_alg!r}.", ) + # joserfc's KeySet.import_key_set runs a stricter dict-key validation that + # rejects ``use: None`` / ``alg: None`` outright. Strip explicit nulls for + # those two fields before handing the JWK off so skip-on-null actually + # propagates to the import step. + matches = [{k: v for k, v in matched.items() if not (k in ("use", "alg") and v is None)}] stripped = {k: v for k, v in signed_profile.items() if k != "signature"} try: diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index 3210326..e062aa7 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -756,6 +756,39 @@ def test_accepts_u2027_sanity_case(self) -> None: assert verify_ucp_profile(signed, build_jwks_response([signer.public_jwk])) is True +class TestJWKUseAlgNullTreatedAsAbsent: + """RFC 7517 lists ``use`` and ``alg`` as optional. Explicit JSON null is + out-of-spec but harmless; treat null as absent (skip-on-null) so a JWK + carrying ``"use": null`` or ``"alg": null`` matches the Node sibling's + ``!= null`` semantics in ucp-jwks.ts and the two languages stay + symmetric. + """ + + def test_verify_succeeds_when_matched_jwk_has_null_use(self) -> None: + key = generate_ucp_signing_key(kid="null-use") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="null-use") + jwks_with_null_use = build_jwks_response([{**key.public_jwk, "use": None}]) + assert verify_ucp_profile(signed, jwks_with_null_use) is True + + def test_verify_succeeds_when_matched_jwk_has_null_alg(self) -> None: + key = generate_ucp_signing_key(kid="null-alg", alg="EdDSA") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="null-alg") + jwks_with_null_alg = build_jwks_response([{**key.public_jwk, "alg": None}]) + assert verify_ucp_profile(signed, jwks_with_null_alg) is True + + def test_verify_still_rejects_use_enc_with_unusable_key(self) -> None: + # Sanity: non-null wrong values continue to fail with unusable_key. + key = generate_ucp_signing_key(kid="enc-sanity") + profile = _base_profile([key.public_jwk]) + signed = sign_ucp_profile(profile, signing_key=key.private_key, kid="enc-sanity") + enc_jwk = {**key.public_jwk, "use": "enc"} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([enc_jwk])) + assert exc.value.code == "unusable_key" + + class TestVerifierErrorPrecedence: def test_null_profile_with_malformed_jwks_returns_no_signature(self) -> None: with pytest.raises(UCPVerificationError) as exc: From 2f598ca56970e3ab34137326cedeca7fe887e748 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 06:17:42 -0700 Subject: [PATCH 19/37] hardening(identity): reject crit array with non-string elements per RFC 7515 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RFC 7515 §4.1.11 requires crit array entries to be strings. The previous shape check accepted arrays like [42] or [42, "valid"] because it only guarded against non-list and empty-list shapes. Node-commerce already rejects these with malformed_jws; Python now matches. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp_jwks.py | 4 ++-- tests/test_ucp_jwks.py | 22 ++++++++++++++++++++++ 2 files changed, 24 insertions(+), 2 deletions(-) diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 9772e24..b65d5be 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -409,10 +409,10 @@ def verify_ucp_profile( # when it tries to iterate `None`. RFC 7515 §4.1.11 requires a non-empty array. if "crit" in header: crit = header["crit"] - if not isinstance(crit, list) or len(crit) == 0: + if not isinstance(crit, list) or len(crit) == 0 or not all(isinstance(c, str) for c in crit): raise UCPVerificationError( "malformed_jws", - f"JWS protected header crit must be a non-empty array; got {crit!r}.", + f"JWS protected header crit must be a non-empty array of strings; got {crit!r}.", ) raise UCPVerificationError( "unrecognized_critical_header", diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index e062aa7..29aa479 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -631,6 +631,28 @@ def test_verify_crit_string_emits_malformed_jws(self) -> None: verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) assert exc.value.code == "malformed_jws" + @pytest.mark.parametrize( + "bad_crit", + [ + [42], + [None], + [{}], + [42, "valid"], + ["valid", 42], + ], + ) + def test_verify_crit_with_non_string_element_emits_malformed_jws(self, bad_crit: object) -> None: + """RFC 7515 §4.1.11: crit array entries MUST be strings. Non-string elements + (including mixed arrays) are malformed. Cross-language parity with node-commerce, + which rejects [42] etc. with malformed_jws.""" + key = generate_ucp_signing_key(kid="real") + profile = _base_profile([key.public_jwk]) + jws_compact = self._hand_craft_jws_with_crit(key, profile, bad_crit) + signed = {**profile, "signature": jws_compact} + with pytest.raises(UCPVerificationError) as exc: + verify_ucp_profile(signed, build_jwks_response([key.public_jwk])) + assert exc.value.code == "malformed_jws" + class TestVerifierCanonicalizationTypedErrors: """Verifier-side canonicalize must NEVER leak raw ValueError; always UCPVerificationError(body_mismatch).""" From 4dfee922b557f689de9eea56e9bcf71c87a28d9e Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 06:32:12 -0700 Subject: [PATCH 20/37] hardening(identity): align UCP read order to typed-first + reject bytes Two cross-language parity fixes from round-29 SDK review: LOW-1: build_ucp_profile now reads operator_verification and account_verification from the typed AssessResult fields first, falling back to data.raw only when the typed field is missing. Previously raw won, diverging from node-commerce which reads typed fields directly. With raw and typed in disagreement, both languages now pick the same source so a profile signed by one verifies in the other. LOW-2: _reject_unsafe_numbers now rejects bytes / bytearray with a typed ValueError before json.dumps can raise its raw "Object of type bytes is not JSON serializable" TypeError. Mirrors the node sibling's "typed arrays are not allowed" rejection in stableStringify. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp.py | 43 +++++++++++++----------- agentscore_commerce/identity/ucp_jwks.py | 11 ++++++ tests/test_ucp.py | 43 +++++++++++++++++++----- tests/test_ucp_jwks.py | 25 ++++++++++++++ 4 files changed, 94 insertions(+), 28 deletions(-) diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 84d69ea..6b5ece9 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -272,28 +272,33 @@ async def ucp_profile(): base_capabilities = list(capabilities or []) if data is not None and data.resolved_operator: - raw = data.raw or {} - operator_verification = raw.get("operator_verification") if isinstance(raw, dict) else None + # Match node-commerce read order: prefer the typed AssessResult fields, + # fall back to ``data.raw`` only when the typed field is missing. The + # Node sibling reads ``input.data.operator_verification`` / + # ``input.data.account_verification`` directly without consulting + # ``raw``; if a caller hand-constructs an AssessResult with mismatched + # typed and raw verification blocks, both languages must pick the same + # source so a profile signed in one verifies in the other. + typed_op = getattr(data, "operator_verification", None) + if typed_op is not None and not isinstance(typed_op, dict): + # Convert OperatorVerification dataclass to a plain dict. + operator_verification = { + "level": getattr(typed_op, "level", None), + "operator_type": getattr(typed_op, "operator_type", None), + "verified_at": getattr(typed_op, "verified_at", None), + } + else: + operator_verification = typed_op if not operator_verification: - # Fallback to the typed AssessResult.operator_verification field when - # `raw` doesn't carry it. Mirrors the node sibling's typed-field read - # path so a hand-constructed AssessResult (no `raw`) still surfaces - # the operator verification block in the UCP capability claims. - typed_op = getattr(data, "operator_verification", None) - if typed_op is not None and not isinstance(typed_op, dict): - # Convert OperatorVerification dataclass to a plain dict. - operator_verification = { - "level": getattr(typed_op, "level", None), - "operator_type": getattr(typed_op, "operator_type", None), - "verified_at": getattr(typed_op, "verified_at", None), - } - else: - operator_verification = typed_op - account_verification = raw.get("account_verification") if isinstance(raw, dict) else None - if not account_verification: - account_verification = getattr(data, "account_verification", None) + raw = data.raw or {} + operator_verification = raw.get("operator_verification") if isinstance(raw, dict) else None if not isinstance(operator_verification, dict): operator_verification = {} + + account_verification = getattr(data, "account_verification", None) + if not account_verification: + raw = data.raw or {} + account_verification = raw.get("account_verification") if isinstance(raw, dict) else None if not isinstance(account_verification, dict): account_verification = {} # `dict.get(k) or DEFAULT` (not `dict.get(k, DEFAULT)`) coerces both a diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index b65d5be..4425111 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -222,6 +222,17 @@ def _reject_unsafe_numbers(value: Any) -> None: "Convert to a sorted list before passing." ) raise ValueError(msg) + # Reject bytes / bytearray with a typed message (mirrors the node sibling's + # "typed arrays are not allowed" rejection in stableStringify). Without this, + # raw bytes fall through cleanly and surface a confusing + # `TypeError: Object of type bytes is not JSON serializable` from + # `json.dumps` later. Convert to a base64url string before passing. + if isinstance(value, bytes | bytearray): + msg = ( + f"{type(value).__name__} values are not allowed in canonicalized JSON. " + "Convert to a base64url string before passing." + ) + raise ValueError(msg) if isinstance(value, dict): for k, v in value.items(): _reject_unsafe_numbers(k) diff --git a/tests/test_ucp.py b/tests/test_ucp.py index 8ce6a6c..8c446ba 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -235,26 +235,51 @@ def test_typed_account_verification_fallback_via_setattr() -> None: assert claims["sanctions_clear"] is True -def test_raw_takes_precedence_over_typed_fallback() -> None: - # When raw carries `operator_verification`, the typed-field fallback is NOT - # consulted. Production callers populate raw and the typed fields stay - # in sync; this test pins the precedence so a typed mismatch can't silently - # override the raw payload. +def test_typed_takes_precedence_over_raw() -> None: + # When the typed `operator_verification` / `account_verification` fields + # disagree with `data.raw`, the typed values win. Mirrors the node sibling + # which reads `input.data.operator_verification` directly without + # consulting `raw`. Production callers populate raw and the typed fields + # stay in sync; pinning typed-precedence keeps a hand-constructed + # AssessResult from emitting a profile that one language verifies and the + # other rejects. result = AssessResult( allow=True, resolved_operator="op_xyz", - operator_verification=OperatorVerification(level="enhanced"), + operator_verification=OperatorVerification(level="verified"), raw={ - "operator_verification": {"level": "verified"}, - "account_verification": {"kyc_level": "verified"}, + "operator_verification": {"level": "none"}, + "account_verification": {"kyc_level": "none"}, }, ) + result.account_verification = {"kyc_level": "verified"} # type: ignore[attr-defined] profile = build_ucp_profile(**_base_kwargs(), data=result) cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) - # `kyc_level` reads from raw account_verification.kyc_level first. + # Typed `account_verification.kyc_level == 'verified'` wins over the + # `none` value carried in `data.raw`. assert cap.extras["claims"]["kyc_level"] == "verified" +def test_raw_fallback_used_when_typed_missing() -> None: + # When typed `operator_verification` / `account_verification` are absent, + # the builder falls back to `data.raw`. This is the production path: + # `AgentScoreClient` populates both, but legacy or ad-hoc callers may + # only set raw. + result = AssessResult( + allow=True, + resolved_operator="op_raw", + operator_verification=None, + raw={ + "operator_verification": {"level": "enhanced"}, + "account_verification": {"kyc_level": "enhanced"}, + }, + ) + profile = build_ucp_profile(**_base_kwargs(), data=result) + cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) + # `kyc_level` falls back to raw `account_verification.kyc_level`. + assert cap.extras["claims"]["kyc_level"] == "enhanced" + + # Per-element to_dict reserved-key collision guard. Mirrors the parent # UCPProfile.to_dict guard so vendor extras can't silently overwrite a canonical # field on UCPService / UCPCapability / UCPSigningKey via `out.update(extras)`. diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index 29aa479..a0e8556 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -344,6 +344,31 @@ def test_rejects_set_of_valid_strings_with_typed_message(self) -> None: with pytest.raises(ValueError, match="set values are not allowed"): sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + def test_rejects_bytes_values_outright(self) -> None: + # `bytes` is not representable in JSON; the canonicalizer rejects it with a + # typed message before `json.dumps` can raise its raw + # `TypeError: Object of type bytes is not JSON serializable`. Mirrors + # node's `stableStringify: typed arrays are not allowed`. + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"blob": b"hello"}} + with pytest.raises(ValueError, match="bytes values are not allowed"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_bytearray_values_outright(self) -> None: + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"blob": bytearray(b"hello")}} + with pytest.raises(ValueError, match="bytearray values are not allowed"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + + def test_rejects_empty_bytes_with_typed_message(self) -> None: + # Empty bytes would fall through `_reject_unsafe_numbers` cleanly and + # surface a raw `TypeError` from `json.dumps` later. The typed reject + # ensures callers get a guiding ValueError instead. + signer = generate_ucp_signing_key(kid="k") + profile = {**_base_profile([signer.public_jwk]), "extras": {"blob": b""}} + with pytest.raises(ValueError, match="bytes values are not allowed"): + sign_ucp_profile(profile, signing_key=signer.private_key, kid="k") + def test_accepts_max_safe_int_boundary(self) -> None: signer = generate_ucp_signing_key(kid="k") profile = {**_base_profile([signer.public_jwk]), "extras": {"big": 2**53 - 1}} From c00d51ba1bb05f0251820eb3bcdc56626dd68c8b Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 06:41:44 -0700 Subject: [PATCH 21/37] hardening(identity): make AssessResult.account_verification a typed field The UCP profile builder reads the typed AssessResult.operator_verification first then falls back to data.raw, but the typed account_verification branch was unreachable because the field wasn't declared on the dataclass. Add the optional account_verification: dict[str, Any] | None field for symmetry with operator_verification (mirrors node-commerce AgentScoreData.account_verification), populate it in AgentScoreClient._project, and adjust the UCP builder to read the typed field directly. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/client.py | 4 ++++ agentscore_commerce/identity/types.py | 5 +++++ agentscore_commerce/identity/ucp.py | 25 ++++++++++++----------- tests/test_ucp.py | 28 +++++++++++++------------- 4 files changed, 36 insertions(+), 26 deletions(-) diff --git a/agentscore_commerce/identity/client.py b/agentscore_commerce/identity/client.py index 7962ce6..280ca2b 100644 --- a/agentscore_commerce/identity/client.py +++ b/agentscore_commerce/identity/client.py @@ -218,6 +218,9 @@ def _project(self, data: dict[str, Any]) -> AssessResult: else None ) + av_data = data.get("account_verification") + account_verification = av_data if isinstance(av_data, dict) else None + # SDK populates `quota` on the AssessResponse from X-Quota-* headers. Surface up # to adapters so merchants can monitor approach-to-cap proactively. quota_raw = data.get("quota") @@ -237,6 +240,7 @@ def _project(self, data: dict[str, Any]) -> AssessResult: reasons=reasons, identity_method=data.get("identity_method"), operator_verification=operator_verification, + account_verification=account_verification, resolved_operator=data.get("resolved_operator"), verify_url=data.get("verify_url"), policy_result=data.get("policy_result"), diff --git a/agentscore_commerce/identity/types.py b/agentscore_commerce/identity/types.py index 6b3174e..79d3705 100644 --- a/agentscore_commerce/identity/types.py +++ b/agentscore_commerce/identity/types.py @@ -308,6 +308,11 @@ class AssessResult: reasons: list[str] = field(default_factory=list) identity_method: str | None = None operator_verification: OperatorVerification | None = None + # Account-level verification block (KYC level, age bracket, jurisdiction, + # sanctions verdict). Mirrors node-commerce's typed AgentScoreData.account_verification + # field so a hand-constructed AssessResult emits the same UCP claims in both + # languages without a raw-dict round trip. + account_verification: dict[str, Any] | None = None resolved_operator: str | None = None verify_url: str | None = None policy_result: PolicyResult | None = None diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 6b5ece9..6ee1686 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -21,7 +21,7 @@ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any +from typing import TYPE_CHECKING, Any, cast if TYPE_CHECKING: from agentscore_commerce.identity.types import AssessResult @@ -279,28 +279,29 @@ async def ucp_profile(): # ``raw``; if a caller hand-constructs an AssessResult with mismatched # typed and raw verification blocks, both languages must pick the same # source so a profile signed in one verifies in the other. - typed_op = getattr(data, "operator_verification", None) - if typed_op is not None and not isinstance(typed_op, dict): + typed_op = data.operator_verification + operator_verification: dict[str, Any] = {} + if isinstance(typed_op, dict): + operator_verification = cast("dict[str, Any]", typed_op) + elif typed_op is not None: # Convert OperatorVerification dataclass to a plain dict. operator_verification = { "level": getattr(typed_op, "level", None), "operator_type": getattr(typed_op, "operator_type", None), "verified_at": getattr(typed_op, "verified_at", None), } - else: - operator_verification = typed_op if not operator_verification: raw = data.raw or {} - operator_verification = raw.get("operator_verification") if isinstance(raw, dict) else None - if not isinstance(operator_verification, dict): - operator_verification = {} + raw_op = raw.get("operator_verification") if isinstance(raw, dict) else None + if isinstance(raw_op, dict): + operator_verification = raw_op - account_verification = getattr(data, "account_verification", None) + account_verification: dict[str, Any] = data.account_verification or {} if not account_verification: raw = data.raw or {} - account_verification = raw.get("account_verification") if isinstance(raw, dict) else None - if not isinstance(account_verification, dict): - account_verification = {} + raw_av = raw.get("account_verification") if isinstance(raw, dict) else None + if isinstance(raw_av, dict): + account_verification = raw_av # `dict.get(k) or DEFAULT` (not `dict.get(k, DEFAULT)`) coerces both a # missing key AND a present-but-falsy (None / "") value to the default, # matching the node sibling's `||` semantics. The API can return diff --git a/tests/test_ucp.py b/tests/test_ucp.py index 8c446ba..c55ac00 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -183,9 +183,9 @@ def test_coerces_empty_string_verified_at_to_none() -> None: # Typed-field fallback: production callers populate `data.raw`, but a -# hand-constructed AssessResult (no raw) should still surface the operator -# verification block via the typed `AssessResult.operator_verification` field -# and the (optional) `account_verification` attribute. Mirrors the node sibling's +# hand-constructed AssessResult (no raw) should still surface the verification +# block via the typed `AssessResult.operator_verification` / +# `AssessResult.account_verification` fields. Mirrors the node sibling's # typed-field read path. @@ -209,22 +209,22 @@ def test_typed_operator_verification_fallback_when_raw_is_none() -> None: assert claims["verified_at"] == "2026-04-01T00:00:00Z" -def test_typed_account_verification_fallback_via_setattr() -> None: - # `AssessResult` doesn't declare `account_verification` as a typed field, but - # a caller can still attach one ad-hoc. The fallback reads it via getattr so - # parity with the node sibling holds whichever way the caller populates it. +def test_typed_account_verification_fallback_when_raw_is_none() -> None: + # `AssessResult.account_verification` is a typed optional field; a + # hand-constructed result populates it directly via the constructor and the + # builder reads it without consulting `raw`. result = AssessResult( allow=True, resolved_operator="op_typed", operator_verification=OperatorVerification(level="verified"), + account_verification={ + "kyc_level": "verified", + "age_bracket": "21+", + "jurisdiction": "US", + "sanctions_clear": True, + }, raw=None, ) - result.account_verification = { # type: ignore[attr-defined] - "kyc_level": "verified", - "age_bracket": "21+", - "jurisdiction": "US", - "sanctions_clear": True, - } profile = build_ucp_profile(**_base_kwargs(), data=result) d = profile.to_dict() cap = next(c for c in d["capabilities"] if c["name"] == AGENTSCORE_UCP_CAPABILITY) @@ -247,12 +247,12 @@ def test_typed_takes_precedence_over_raw() -> None: allow=True, resolved_operator="op_xyz", operator_verification=OperatorVerification(level="verified"), + account_verification={"kyc_level": "verified"}, raw={ "operator_verification": {"level": "none"}, "account_verification": {"kyc_level": "none"}, }, ) - result.account_verification = {"kyc_level": "verified"} # type: ignore[attr-defined] profile = build_ucp_profile(**_base_kwargs(), data=result) cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) # Typed `account_verification.kyc_level == 'verified'` wins over the From 63a7ee256dbd5c8f7c397df45d1ff247ed094975 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 06:53:20 -0700 Subject: [PATCH 22/37] hardening(identity): typed-empty wins over raw + preserve empty payment_handler config Three cross-language parity fixes for build_ucp_profile / UCPPaymentHandler: 1. `data.account_verification == {}` (and `data.operator_verification == {}`) means "API returned the block with no populated values" and now wins over the `data.raw` fallback. The previous `if not account_verification:` check treated empty-dict as falsy and bled raw values through. Mirrors the Node sibling, which reads the typed field directly without consulting raw. 2. `UCPPaymentHandler.to_dict()` always emits `config` (even when empty). TypeScript serializes `{name: 'tempo', config: {}}` with `config` preserved; the dataclass default is `field(default_factory=dict)` so the field is always a dict. The previous `if self.config:` truthy gate produced byte divergence on explicit `config={}` callers. 3. New `typed-claims` cross-lang fixture exercises the typed-field-only read path (`AssessResult(account_verification={...}, raw=None)`) that the existing `data-driven-claims` fixture didn't cover (it uses `raw=`). Both languages must produce byte-identical canonical bytes for the typed path or cross-lang verify silently drifts in production. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp.py | 50 ++++++----- scripts/generate_typed_claims_fixture.py | 82 +++++++++++++++++++ .../cross-lang/node-typed-claims.json | 57 +++++++++++++ .../fixtures/cross-lang/py-typed-claims.json | 57 +++++++++++++ tests/test_ucp.py | 58 +++++++++++++ tests/test_ucp_cross_lang.py | 13 ++- 6 files changed, 291 insertions(+), 26 deletions(-) create mode 100644 scripts/generate_typed_claims_fixture.py 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/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 6ee1686..a981242 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -165,10 +165,12 @@ class UCPPaymentHandler: config: dict[str, Any] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: - out: dict[str, Any] = {"name": self.name} - if self.config: - out["config"] = self.config - return out + # Always emit `config` (even when empty) so a Python-built handler matches + # the Node sibling byte-for-byte: TypeScript serializes + # `{name: 'tempo', config: {}}` with `config` preserved, and the dataclass + # default is `field(default_factory=dict)` so the field is always a dict. + # Cross-language verify drifts otherwise on explicit `config={}` callers. + return {"name": self.name, "config": self.config} @dataclass @@ -273,35 +275,39 @@ async def ucp_profile(): if data is not None and data.resolved_operator: # Match node-commerce read order: prefer the typed AssessResult fields, - # fall back to ``data.raw`` only when the typed field is missing. The - # Node sibling reads ``input.data.operator_verification`` / - # ``input.data.account_verification`` directly without consulting - # ``raw``; if a caller hand-constructs an AssessResult with mismatched - # typed and raw verification blocks, both languages must pick the same - # source so a profile signed in one verifies in the other. + # fall back to ``data.raw`` only when the typed field is ``None`` (absent). + # An explicitly-empty typed dict means "API returned the block with no + # populated values" and wins over raw — same as the Node sibling, which + # reads ``input.data.operator_verification`` / ``input.data.account_verification`` + # directly without consulting ``raw``. ``is None`` (not truthy) is the + # correct distinction so a caller hand-constructing + # ``AssessResult(account_verification={}, raw={"account_verification": {...}})`` + # gets the same empty-block behavior in both languages. typed_op = data.operator_verification - operator_verification: dict[str, Any] = {} - if isinstance(typed_op, dict): + operator_verification: dict[str, Any] + if typed_op is None: + raw = data.raw or {} + raw_op = raw.get("operator_verification") if isinstance(raw, dict) else None + operator_verification = raw_op if isinstance(raw_op, dict) else {} + elif isinstance(typed_op, dict): operator_verification = cast("dict[str, Any]", typed_op) - elif typed_op is not None: + else: # Convert OperatorVerification dataclass to a plain dict. operator_verification = { "level": getattr(typed_op, "level", None), "operator_type": getattr(typed_op, "operator_type", None), "verified_at": getattr(typed_op, "verified_at", None), } - if not operator_verification: - raw = data.raw or {} - raw_op = raw.get("operator_verification") if isinstance(raw, dict) else None - if isinstance(raw_op, dict): - operator_verification = raw_op - account_verification: dict[str, Any] = data.account_verification or {} - if not account_verification: + account_verification: dict[str, Any] + if data.account_verification is None: raw = data.raw or {} raw_av = raw.get("account_verification") if isinstance(raw, dict) else None - if isinstance(raw_av, dict): - account_verification = raw_av + account_verification = raw_av if isinstance(raw_av, dict) else {} + elif isinstance(data.account_verification, dict): + account_verification = data.account_verification + else: + account_verification = {} # `dict.get(k) or DEFAULT` (not `dict.get(k, DEFAULT)`) coerces both a # missing key AND a present-but-falsy (None / "") value to the default, # matching the node sibling's `||` semantics. The API can return diff --git a/scripts/generate_typed_claims_fixture.py b/scripts/generate_typed_claims_fixture.py new file mode 100644 index 0000000..a446627 --- /dev/null +++ b/scripts/generate_typed_claims_fixture.py @@ -0,0 +1,82 @@ +"""One-shot generator for the typed-claims cross-lang fixture (Python side). + +Writes ``tests/fixtures/cross-lang/py-typed-claims.json``. Sibling to +``generate_data_driven_claims_fixture.py`` but exercises the **typed** +``AssessResult.account_verification`` / ``AssessResult.operator_verification`` +read path (with ``raw=None``) instead of the raw-dict fallback. This catches +drift in typed-field-only callers — production code populates both, but a +hand-constructed AssessResult with only typed fields must produce a profile +that the Node sibling verifies byte-for-byte, since Node's +``buildUCPProfile`` reads the typed fields directly without ever consulting +``raw``. +""" + +from __future__ import annotations + +import json +from pathlib import Path + +from agentscore_commerce.identity import ( + AssessResult, + OperatorVerification, + UCPService, + UCPSigningKey, + build_ucp_profile, +) +from agentscore_commerce.identity.ucp_jwks import ( + build_jwks_response, + generate_ucp_signing_key, + sign_ucp_profile, +) + +OUT = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "cross-lang" / "py-typed-claims.json" + +KID = "py-typed-claims-EdDSA" + + +def main() -> None: + key = generate_ucp_signing_key(kid=KID) + + result = AssessResult( + allow=True, + resolved_operator="op_typed_claims", + verify_url="https://agentscore.sh/verify/op_typed_claims", + operator_verification=OperatorVerification( + 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", + }, + raw=None, + ) + + profile = build_ucp_profile( + name="Typed Claims Merchant", + services=[UCPService(type="rest", url="https://t.example.com")], + payment_handlers=[], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + data=result, + ) + + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=KID) + + fixture = { + "profile": signed, + "jwks": build_jwks_response([key.public_jwk]), + "alg": "EdDSA", + "kid": KID, + "generator": "python", + } + + OUT.write_text(json.dumps(fixture, indent=2) + "\n") + print(f"wrote {OUT}") + + +if __name__ == "__main__": + main() 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/test_ucp.py b/tests/test_ucp.py index c55ac00..1f2aa61 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -1,5 +1,7 @@ """Tests for build_ucp_profile.""" +from typing import Any, cast + import pytest from agentscore_commerce.identity import ( @@ -335,3 +337,59 @@ def test_ucp_signing_key_extras_non_reserved_pass_through() -> None: sk = UCPSigningKey(kid="me", kty="EC", alg="ES256", crv="P-256", extras={"x": "abc", "y": "def"}) out = sk.to_dict() assert out == {"kid": "me", "kty": "EC", "alg": "ES256", "crv": "P-256", "x": "abc", "y": "def"} + + +# UCPPaymentHandler.to_dict always emits `config`. The Node sibling serializes +# `{name: 'tempo', config: {}}` with `config` preserved (TypeScript optional +# field initialized to a new object). Cross-language byte-parity requires the +# Python emitter to do the same — even when the dataclass default +# `field(default_factory=dict)` left config empty. + + +def test_ucp_payment_handler_to_dict_preserves_empty_config() -> None: + assert UCPPaymentHandler(name="tempo").to_dict() == {"name": "tempo", "config": {}} + + +def test_ucp_payment_handler_to_dict_preserves_explicit_empty_config() -> None: + assert UCPPaymentHandler(name="tempo", config={}).to_dict() == {"name": "tempo", "config": {}} + + +def test_ucp_payment_handler_to_dict_preserves_populated_config() -> None: + assert UCPPaymentHandler(name="tempo", config={"recipient": "0xabc"}).to_dict() == { + "name": "tempo", + "config": {"recipient": "0xabc"}, + } + + +# Typed-vs-raw read order: `data.account_verification == {}` means "API +# explicitly returned an empty block" and must win over `data.raw`. Only when +# the typed field is `None` does the builder fall back to raw. Mirrors the Node +# sibling, which reads the typed field directly without consulting raw. + + +def test_typed_empty_account_verification_wins_over_raw() -> None: + result = AssessResult( + allow=True, + resolved_operator="op_xyz", + account_verification={}, + raw={"account_verification": {"kyc_level": "verified"}}, + ) + profile = build_ucp_profile(**_base_kwargs(), data=result) + cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) + # Empty typed dict suppresses the raw fallback; kyc_level falls through to + # the schema default "none" instead of bleeding the raw "verified" value. + assert cap.extras["claims"]["kyc_level"] == "none" + + +def test_typed_empty_operator_verification_wins_over_raw() -> None: + result = AssessResult( + allow=True, + resolved_operator="op_xyz", + # Empty dict is a valid typed value (means "operator block returned empty"). + operator_verification=cast("Any", {}), + raw={"operator_verification": {"level": "enhanced", "verified_at": "2026-01-01T00:00:00Z"}}, + ) + profile = build_ucp_profile(**_base_kwargs(), data=result) + cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) + # Empty typed dict suppresses raw fallback; verified_at falls through to None. + assert cap.extras["claims"]["verified_at"] is None diff --git a/tests/test_ucp_cross_lang.py b/tests/test_ucp_cross_lang.py index 3466ad7..2a737b1 100644 --- a/tests/test_ucp_cross_lang.py +++ b/tests/test_ucp_cross_lang.py @@ -46,10 +46,15 @@ def test_corpus_covers_canonical_scenarios() -> None: "multikey", "emoji-keys", "int-boundary", - # `data-driven-claims` is the only fixture in the corpus that - # exercises ``build_ucp_profile`` / ``buildUCPProfile``'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", ): assert f"{lang}-{scenario}.json" in names, f"missing fixture {lang}-{scenario}.json" From 3198c84cfbef6bb7e38150a7d3bef5d3f089063f Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 08:06:41 -0700 Subject: [PATCH 23/37] docs(ucp): clarify raw fallback as Python-only legacy escape hatch The previous comment claimed parity with node-commerce read order, but Node has no raw fallback at all. Rewrite to accurately describe the behavior: typed fields are canonical; raw fallback is a Python-only hatch for hand-constructed AssessResult instances and may not verify cross-language. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp.py | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index a981242..dcb3e06 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -274,15 +274,12 @@ async def ucp_profile(): base_capabilities = list(capabilities or []) if data is not None and data.resolved_operator: - # Match node-commerce read order: prefer the typed AssessResult fields, - # fall back to ``data.raw`` only when the typed field is ``None`` (absent). - # An explicitly-empty typed dict means "API returned the block with no - # populated values" and wins over raw — same as the Node sibling, which - # reads ``input.data.operator_verification`` / ``input.data.account_verification`` - # directly without consulting ``raw``. ``is None`` (not truthy) is the - # correct distinction so a caller hand-constructing - # ``AssessResult(account_verification={}, raw={"account_verification": {...}})`` - # gets the same empty-block behavior in both languages. + # Read typed AssessResult fields first (the canonical path). Fall back to + # ``data.raw["operator_verification"]`` / ``data.raw["account_verification"]`` + # only when the typed field is ``None``; this is a Python-only legacy + # escape hatch for callers who hand-construct ``AssessResult(raw=..., typed=None)``. + # Node has no equivalent fallback, so profiles built via the raw-only path + # may not verify cross-language. Production callers should populate typed fields. typed_op = data.operator_verification operator_verification: dict[str, Any] if typed_op is None: From e0aedc1d415a42b660c90fd1979a927ed80de0e4 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 08:12:04 -0700 Subject: [PATCH 24/37] chore(deps): refresh uv.lock Run `uv lock --upgrade`. Transitive bump only: - propcache 0.4.1 -> 0.5.2 All declared direct deps remain at their latest versions within existing constraint ranges. joserfc held at 1.x. Co-Authored-By: Claude Opus 4.7 (1M context) --- uv.lock | 202 ++++++++++++++++++++++++++++++-------------------------- 1 file changed, 107 insertions(+), 95 deletions(-) diff --git a/uv.lock b/uv.lock index 5709554..1b2e0d6 100644 --- a/uv.lock +++ b/uv.lock @@ -1905,101 +1905,113 @@ wheels = [ [[package]] name = "propcache" -version = "0.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8c/d4/4e2c9aaf7ac2242b9358f98dccd8f90f2605402f5afeff6c578682c2c491/propcache-0.4.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:60a8fda9644b7dfd5dece8c61d8a85e271cb958075bfc4e01083c148b61a7caf", size = 80208, upload-time = "2025-10-08T19:46:24.597Z" }, - { url = "https://files.pythonhosted.org/packages/c2/21/d7b68e911f9c8e18e4ae43bdbc1e1e9bbd971f8866eb81608947b6f585ff/propcache-0.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c30b53e7e6bda1d547cabb47c825f3843a0a1a42b0496087bb58d8fedf9f41b5", size = 45777, upload-time = "2025-10-08T19:46:25.733Z" }, - { url = "https://files.pythonhosted.org/packages/d3/1d/11605e99ac8ea9435651ee71ab4cb4bf03f0949586246476a25aadfec54a/propcache-0.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:6918ecbd897443087a3b7cd978d56546a812517dcaaca51b49526720571fa93e", size = 47647, upload-time = "2025-10-08T19:46:27.304Z" }, - { url = "https://files.pythonhosted.org/packages/58/1a/3c62c127a8466c9c843bccb503d40a273e5cc69838805f322e2826509e0d/propcache-0.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3d902a36df4e5989763425a8ab9e98cd8ad5c52c823b34ee7ef307fd50582566", size = 214929, upload-time = "2025-10-08T19:46:28.62Z" }, - { url = "https://files.pythonhosted.org/packages/56/b9/8fa98f850960b367c4b8fe0592e7fc341daa7a9462e925228f10a60cf74f/propcache-0.4.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a9695397f85973bb40427dedddf70d8dc4a44b22f1650dd4af9eedf443d45165", size = 221778, upload-time = "2025-10-08T19:46:30.358Z" }, - { url = "https://files.pythonhosted.org/packages/46/a6/0ab4f660eb59649d14b3d3d65c439421cf2f87fe5dd68591cbe3c1e78a89/propcache-0.4.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2bb07ffd7eaad486576430c89f9b215f9e4be68c4866a96e97db9e97fead85dc", size = 228144, upload-time = "2025-10-08T19:46:32.607Z" }, - { url = "https://files.pythonhosted.org/packages/52/6a/57f43e054fb3d3a56ac9fc532bc684fc6169a26c75c353e65425b3e56eef/propcache-0.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd6f30fdcf9ae2a70abd34da54f18da086160e4d7d9251f81f3da0ff84fc5a48", size = 210030, upload-time = "2025-10-08T19:46:33.969Z" }, - { url = "https://files.pythonhosted.org/packages/40/e2/27e6feebb5f6b8408fa29f5efbb765cd54c153ac77314d27e457a3e993b7/propcache-0.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:fc38cba02d1acba4e2869eef1a57a43dfbd3d49a59bf90dda7444ec2be6a5570", size = 208252, upload-time = "2025-10-08T19:46:35.309Z" }, - { url = "https://files.pythonhosted.org/packages/9e/f8/91c27b22ccda1dbc7967f921c42825564fa5336a01ecd72eb78a9f4f53c2/propcache-0.4.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:67fad6162281e80e882fb3ec355398cf72864a54069d060321f6cd0ade95fe85", size = 202064, upload-time = "2025-10-08T19:46:36.993Z" }, - { url = "https://files.pythonhosted.org/packages/f2/26/7f00bd6bd1adba5aafe5f4a66390f243acab58eab24ff1a08bebb2ef9d40/propcache-0.4.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f10207adf04d08bec185bae14d9606a1444715bc99180f9331c9c02093e1959e", size = 212429, upload-time = "2025-10-08T19:46:38.398Z" }, - { url = "https://files.pythonhosted.org/packages/84/89/fd108ba7815c1117ddca79c228f3f8a15fc82a73bca8b142eb5de13b2785/propcache-0.4.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e9b0d8d0845bbc4cfcdcbcdbf5086886bc8157aa963c31c777ceff7846c77757", size = 216727, upload-time = "2025-10-08T19:46:39.732Z" }, - { url = "https://files.pythonhosted.org/packages/79/37/3ec3f7e3173e73f1d600495d8b545b53802cbf35506e5732dd8578db3724/propcache-0.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:981333cb2f4c1896a12f4ab92a9cc8f09ea664e9b7dbdc4eff74627af3a11c0f", size = 205097, upload-time = "2025-10-08T19:46:41.025Z" }, - { url = "https://files.pythonhosted.org/packages/61/b0/b2631c19793f869d35f47d5a3a56fb19e9160d3c119f15ac7344fc3ccae7/propcache-0.4.1-cp311-cp311-win32.whl", hash = "sha256:f1d2f90aeec838a52f1c1a32fe9a619fefd5e411721a9117fbf82aea638fe8a1", size = 38084, upload-time = "2025-10-08T19:46:42.693Z" }, - { url = "https://files.pythonhosted.org/packages/f4/78/6cce448e2098e9f3bfc91bb877f06aa24b6ccace872e39c53b2f707c4648/propcache-0.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:364426a62660f3f699949ac8c621aad6977be7126c5807ce48c0aeb8e7333ea6", size = 41637, upload-time = "2025-10-08T19:46:43.778Z" }, - { url = "https://files.pythonhosted.org/packages/9c/e9/754f180cccd7f51a39913782c74717c581b9cc8177ad0e949f4d51812383/propcache-0.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:e53f3a38d3510c11953f3e6a33f205c6d1b001129f972805ca9b42fc308bc239", size = 38064, upload-time = "2025-10-08T19:46:44.872Z" }, - { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, - { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, - { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, - { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, - { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, - { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, - { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, - { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, - { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, - { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, - { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, - { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, - { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, - { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, - { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, - { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, - { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, - { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, - { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, - { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, - { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, - { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, - { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, - { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, - { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, - { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, - { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, - { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, - { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, - { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, - { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, - { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, - { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, - { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, - { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, - { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, - { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, - { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, - { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, - { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, - { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, - { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, - { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, - { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, - { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, - { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, - { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, - { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, - { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, - { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, - { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, - { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, - { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, - { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, - { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, - { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, - { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, - { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, - { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, - { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, - { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, - { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, - { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, - { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, - { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, - { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, - { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, - { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, - { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, - { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, - { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, - { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, - { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +version = "0.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ec/44/c87281c333769159c50594f22610f77398a47ccbfbbf23074e744e86f87c/propcache-0.5.2.tar.gz", hash = "sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427", size = 50208, upload-time = "2026-05-08T21:02:12.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/f1/8a8cc1c2c7e7934ab77e0163414f736fadbc0f5e8dd9673b952355ac175b/propcache-0.5.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:74b70780220e2dd89175ca24b81b68b67c83db499ae611e7f2313cb329801c78", size = 90744, upload-time = "2026-05-08T20:59:45.799Z" }, + { url = "https://files.pythonhosted.org/packages/c2/f4/651b1225e976bd1a2ba5cfba0c29d096581c2636b437e3a9a7ab6276270a/propcache-0.5.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a4840ab0ae0216d952f4b53dc6d0b992bfc2bedbfe360bdd9b548bc184c08959", size = 52033, upload-time = "2026-05-08T20:59:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/15/a8/8ede85d6aa1f79fc7dc2f8fd2c8d65920b8272c3892903c8a1affde48cfb/propcache-0.5.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:c6844ba6364fb12f403928a82cfd295ab103a2b315c77c747b2dbe4a41894ea7", size = 52754, upload-time = "2026-05-08T20:59:49.202Z" }, + { url = "https://files.pythonhosted.org/packages/7d/fe/b3551b41bbc2f5b5bb088fc6920567cd43101253e68fbaa261339eb96fe1/propcache-0.5.2-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2293949b855ce597f2826452d17c2d545fb5622379c4ea6fdf525e9b8e8a2511", size = 57573, upload-time = "2026-05-08T20:59:50.778Z" }, + { url = "https://files.pythonhosted.org/packages/83/27/ab851ebd1b7172e3e161f5f8d39e315d54a91bea246f01f4d872d3376aef/propcache-0.5.2-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0fd59b5af35f74da48d905dcbad55449ba13be91823cb05a9bd590bbf5b61660", size = 60645, upload-time = "2026-05-08T20:59:52.227Z" }, + { url = "https://files.pythonhosted.org/packages/95/7d/466b3d18022e9897cbda9c735c493c5bd747d7a4c6f5ea1480b4cec434b6/propcache-0.5.2-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29f9309a2e42b0d273be006fdb4be2d6c39a47f6f57d8fb1cf9f81481df81b66", size = 61563, upload-time = "2026-05-08T20:59:53.866Z" }, + { url = "https://files.pythonhosted.org/packages/27/1b/16ab7f2cf2041da2f60d156ba64c2484eadf9168075b4ff43c3ef60045af/propcache-0.5.2-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5aaa2b923c1944ac8febd6609cb373540a5563e7cbcb0fd770f75dace2eb817b", size = 58888, upload-time = "2026-05-08T20:59:55.457Z" }, + { url = "https://files.pythonhosted.org/packages/0a/67/bb777ffd907633563bf35fd859c4ce97b0512c32f4633cf5d1eb7c33512b/propcache-0.5.2-cp311-cp311-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66ea454f095ddf5b6b14f56c064c0941c4788be11e18d2464cf643bf7203ff67", size = 59253, upload-time = "2026-05-08T20:59:57.075Z" }, + { url = "https://files.pythonhosted.org/packages/b9/42/64f8d90b73fd9cdc1499b48057ff6d9cd2a98a25734c9bb62ecf07e87061/propcache-0.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:95f1e3f4760d404b13c9976c0229b2b49a3c8e2c62a9ce92efdd2b11ada75e3f", size = 57558, upload-time = "2026-05-08T20:59:58.602Z" }, + { url = "https://files.pythonhosted.org/packages/eb/02/dba5bc03c9041f2092ea55a449caf5dfe68352c6654511b29ba0654ddb69/propcache-0.5.2-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:85341b12b9d55bad0bded24cac341bb34289469e03a11f3f583ea1cc1db0326c", size = 55007, upload-time = "2026-05-08T20:59:59.837Z" }, + { url = "https://files.pythonhosted.org/packages/14/c0/43f649c7aa2a77a3b100d84e9dea3a483120ecb608bfe36ce49eaff517fe/propcache-0.5.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:26a4dca084132874e639895c3135dfad5eb20bae209f62d1aeb31b03e601c3c0", size = 60355, upload-time = "2026-05-08T21:00:01.144Z" }, + { url = "https://files.pythonhosted.org/packages/83/c0/435dafd27f1cb4a495381dae60e25883ccfe4020bb72818e8184c1678092/propcache-0.5.2-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:3b199b9b2b3d6a7edf3183ba8a9a137a22b97f7df525feb5ae1eccf026d2a9c6", size = 59057, upload-time = "2026-05-08T21:00:02.401Z" }, + { url = "https://files.pythonhosted.org/packages/53/ae/6e292df9135d659944e96cb3389258e4a663e5b2b5f6c217ef0ddc8d2f73/propcache-0.5.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:e59bc9e66329185b93dab73f210f1a37f81cb40f321501db8017c9aea15dba27", size = 61938, upload-time = "2026-05-08T21:00:03.638Z" }, + { url = "https://files.pythonhosted.org/packages/0b/42/314ebc50d8159055411fd6b0bda322ff510e4b1f7d2e4927940ad0f6af20/propcache-0.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:552ffadf6ad409844bc5919c42a0a83d88314cedddaea0e41e80a8b8fffe881f", size = 59731, upload-time = "2026-05-08T21:00:04.881Z" }, + { url = "https://files.pythonhosted.org/packages/b8/9b/2da6dee38871c3c8772fabc2758325a5c9077d6d18c597737dc04dd884cd/propcache-0.5.2-cp311-cp311-win32.whl", hash = "sha256:cd416c1de191973c52ff1a12a57446bfc7642797b282d7caf2162d7d1b8aa9a0", size = 38966, upload-time = "2026-05-08T21:00:06.511Z" }, + { url = "https://files.pythonhosted.org/packages/42/4e/f17363fb58c0afe05b067361cb6d86ed2d29de6506779a27547c4d183075/propcache-0.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:44e488ef40dbb452700b2b1f8188934121f6648f52c295055662d2191959ff82", size = 42135, upload-time = "2026-05-08T21:00:08.088Z" }, + { url = "https://files.pythonhosted.org/packages/c6/eb/6af6685077d22e8b33358d3c548e3282706a0b3cd85044ffba4e5dd08e3b/propcache-0.5.2-cp311-cp311-win_arm64.whl", hash = "sha256:54adaa85a22078d1e306304a40984dc5be99d599bf3dc0a24dc98f7daeab89ab", size = 38381, upload-time = "2026-05-08T21:00:09.692Z" }, + { url = "https://files.pythonhosted.org/packages/4a/cb/e27bc2b2737a0bb49962b275efa051e8f1c35a936df7d5139b6b658b7dc9/propcache-0.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:806719138ecd720339a12410fb9614ac9b2b2d3a5fdf8235d56981c36f4039ba", size = 95887, upload-time = "2026-05-08T21:00:11.277Z" }, + { url = "https://files.pythonhosted.org/packages/e6/13/b8ae04c59392f8d11c6cd9fb4011d1dc7c86b81225c770280300e259ffe1/propcache-0.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:db2b80ea58eab4f86b2beec3cc8b39e8ff9276ac20e96b7cce43c8ae84cd6b5a", size = 54654, upload-time = "2026-05-08T21:00:12.604Z" }, + { url = "https://files.pythonhosted.org/packages/2c/7d/49777a3e20b55863d4794384a38acd460c04157b0a00f8602b0d508b8431/propcache-0.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:e5cbfac9f61484f7e9f3597775500cd3ebe8274e9b050c38f9525c77c97520bf", size = 55190, upload-time = "2026-05-08T21:00:13.935Z" }, + { url = "https://files.pythonhosted.org/packages/44/c7/085d0cd63062e84044e3f05797749c3f8e3938ff3aeb0eb2f69d43fafc91/propcache-0.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5dbc581d2814337da56222fab8dc5f161cd798a434e49bac27930aaef798e144", size = 59995, upload-time = "2026-05-08T21:00:15.526Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/32cf8e3009e92b2645cf1e944f701e8ea4e924dffde1ee26db860bcbf7e4/propcache-0.5.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:857187f381f88c8e2fa2fe56ab94879d011b883d5a2ee5a1b60a8cd2a06846d9", size = 63422, upload-time = "2026-05-08T21:00:16.824Z" }, + { url = "https://files.pythonhosted.org/packages/9e/1b/f112433f99fc979431b87a39ef169e3f8df070d99a72792c56d6937ac48b/propcache-0.5.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:178b4a2cdaac1818e2bf1c5a99b94383fa73ea5382e032a48dec07dc5668dc42", size = 64342, upload-time = "2026-05-08T21:00:18.362Z" }, + { url = "https://files.pythonhosted.org/packages/14/15/5574111ae50dd6e879456888c0eadd4c5a869959775854e18e18a6b345f3/propcache-0.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6f328175a2cde1f0ff2c4ed8ce968b9dcfb55f3a7153f39e2957ed994da13476", size = 61639, upload-time = "2026-05-08T21:00:19.692Z" }, + { url = "https://files.pythonhosted.org/packages/cc/da/4d775080b1490c0ae604acda868bd71aabe3a89ed16f2aa4339eb8a283e7/propcache-0.5.2-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:5671d09a36b06d0fd4a3da0fccbcae360e9b1570924171a15e9e0997f0249fba", size = 61588, upload-time = "2026-05-08T21:00:21.155Z" }, + { url = "https://files.pythonhosted.org/packages/04/ac/f076982cbe2195ee9cf32de5a1e46951d9fb399fc207f390562dd0fd8fb2/propcache-0.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:80168e2ebe4d3ec6599d10ad8f520304ae1cad9b6c5a95372aef1b66b7bfb53a", size = 60029, upload-time = "2026-05-08T21:00:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/70/60/189be62e0dd898dce3b331e1b8c7a543cd3a405ac0c81fe8ee8a9d5d77e1/propcache-0.5.2-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:45f11346f884bc47444f6e6647131055844134c3175b629f84952e2b5cd62b64", size = 56774, upload-time = "2026-05-08T21:00:24.001Z" }, + { url = "https://files.pythonhosted.org/packages/ea/9e/93377b9c7939c1ffae98f878dee955efadfd638078bc86dbc21f9d52f651/propcache-0.5.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:8e778ebd44ef4f66ed60a0416b06b489687db264a9c0b3620362f26489492913", size = 63532, upload-time = "2026-05-08T21:00:25.545Z" }, + { url = "https://files.pythonhosted.org/packages/14/f9/590ef6cfb9b8028d516d287812ece32bb0bc5f11fbb9c8bf6b2e6313fec8/propcache-0.5.2-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:c0cb9ed24c8964e172768d455a38254c2dd8a552905729ce006cad3d3dda59b1", size = 61592, upload-time = "2026-05-08T21:00:27.186Z" }, + { url = "https://files.pythonhosted.org/packages/b4/5e/70958b3034c297a630bba2f17ca7abc2d5f39a803ad7e370ab79d1ecd022/propcache-0.5.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:1d1ad32d9d4355e2be65574fd0bfd3677e7066b009cd5b9b2dee8aa6a6393b33", size = 64788, upload-time = "2026-05-08T21:00:28.8Z" }, + { url = "https://files.pythonhosted.org/packages/12/fd/77fe5936d8c3086ca9048f7f415f122ed82e53884a9ec193646b42deef06/propcache-0.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:c80f4ba3e8f00189165999a742ee526ebeccedf6c3f7beb0c7df821e9772435a", size = 62514, upload-time = "2026-05-08T21:00:30.098Z" }, + { url = "https://files.pythonhosted.org/packages/cf/74/66bd798b5b3be70aa1b391f5cc9d6a0a5532d7fd3b19ec0b213e72e6ad9d/propcache-0.5.2-cp312-cp312-win32.whl", hash = "sha256:8c7972d8f193740d9175f0998ab38717e6cd322d5935c5b0fef8c0d323fd9031", size = 39018, upload-time = "2026-05-08T21:00:31.622Z" }, + { url = "https://files.pythonhosted.org/packages/61/7c/5c0d34aa3024694d6dcb9271cdbdd08c4e47c1c0ad95ec7e7bc74cdea145/propcache-0.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:d9ee8826a7d47863a08ac44e1a5f611a462eefc3a194b492da242128bec75b42", size = 42322, upload-time = "2026-05-08T21:00:32.918Z" }, + { url = "https://files.pythonhosted.org/packages/4d/91/875812f1a3feb20ceba818ef39fbe4d92f1081e04ac815c822496d0d038b/propcache-0.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:2800a4a8ead6b28cccd1ec54b59346f0def7922ee1c7598e8499c733cfbb7c84", size = 38172, upload-time = "2026-05-08T21:00:35.124Z" }, + { url = "https://files.pythonhosted.org/packages/c5/09/f049e45385503fe67db75a6b6186a7b9f0c3930366dc960522c312a825b1/propcache-0.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a", size = 94457, upload-time = "2026-05-08T21:00:36.355Z" }, + { url = "https://files.pythonhosted.org/packages/6b/65/83d1d05655baf63113731bd5a1008435e14f8d1e5a06cbe4ec5b23ad7a31/propcache-0.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117", size = 53835, upload-time = "2026-05-08T21:00:38.072Z" }, + { url = "https://files.pythonhosted.org/packages/a9/12/a6ba6482bb5ea3260c000c9b20881c95fa11c6b30173715668259f844ed7/propcache-0.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098", size = 54545, upload-time = "2026-05-08T21:00:39.319Z" }, + { url = "https://files.pythonhosted.org/packages/a9/19/7fa086f5764c59ec8a8e157cd93aa8497acc00aba9dcdec56bfffb32602d/propcache-0.5.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4", size = 59886, upload-time = "2026-05-08T21:00:40.621Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e4/5d7663dc8235956c8f5281698a3af1d351d8820341ddd890f59d9a9127f2/propcache-0.5.2-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e", size = 63261, upload-time = "2026-05-08T21:00:41.775Z" }, + { url = "https://files.pythonhosted.org/packages/4a/4a/15a03adee24d6350da4292caeac44c34c033d2afe5e87eb370f38854560f/propcache-0.5.2-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7", size = 64184, upload-time = "2026-05-08T21:00:43.018Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c6/979176efdaa3d239e36d503d5af63a0a773b36662ed8f52e5b6a6d9fd40e/propcache-0.5.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d", size = 61534, upload-time = "2026-05-08T21:00:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/c8/22/63e8cd1bae4c2d2be6493b6b7d10566ddafad88137cfbc99964a1119853c/propcache-0.5.2-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a", size = 61500, upload-time = "2026-05-08T21:00:45.796Z" }, + { url = "https://files.pythonhosted.org/packages/60/5a/28e5d9acbac1cc9ccb67045e8c1b943aa8d79fdf39c93bd73cacd68008ea/propcache-0.5.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2", size = 59994, upload-time = "2026-05-08T21:00:47.093Z" }, + { url = "https://files.pythonhosted.org/packages/f3/40/db650677f554a95b9c01a7c9d93d629e93a15562f5deb4573c9ee136fed2/propcache-0.5.2-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa", size = 56884, upload-time = "2026-05-08T21:00:48.376Z" }, + { url = "https://files.pythonhosted.org/packages/80/45/70b39b89516ff8b96bf732fa6fded8cef20f293cb1508690101c3c07ec51/propcache-0.5.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853", size = 63464, upload-time = "2026-05-08T21:00:49.954Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e2/fa59d3a89eac5534293124af4f1d0d0ada091ce4a0ab4610ce03fd2bdd8d/propcache-0.5.2-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a", size = 61588, upload-time = "2026-05-08T21:00:51.281Z" }, + { url = "https://files.pythonhosted.org/packages/0b/97/efb547a55c4bc7381cfb202d6a2239ac621045277bc1ea5dfd3a7f0516c0/propcache-0.5.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704", size = 64667, upload-time = "2026-05-08T21:00:52.602Z" }, + { url = "https://files.pythonhosted.org/packages/92/56/f5c7d9b4b7595d5127da38974d791b2153f3d1eae6c674af3583ace92ad3/propcache-0.5.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4", size = 62463, upload-time = "2026-05-08T21:00:54.303Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3b/484a3a65fc9f9f60c41dcd17b428bace5389544e2c680994534a20755066/propcache-0.5.2-cp313-cp313-win32.whl", hash = "sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d", size = 38621, upload-time = "2026-05-08T21:00:55.808Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fd/3f0f10dba4dabad3bf53102be007abf55481067952bde0fdddff439e7c61/propcache-0.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757", size = 41649, upload-time = "2026-05-08T21:00:57.061Z" }, + { url = "https://files.pythonhosted.org/packages/90/ec/6ce619cc32bb500a482f811f9cd509368b4e58e638d13f2c68f370d6b475/propcache-0.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f", size = 37636, upload-time = "2026-05-08T21:00:58.646Z" }, + { url = "https://files.pythonhosted.org/packages/1b/82/c1d268bbbf2ef981c5bf0fbbe746db617c66e3bcefe431a1aa8943fbe23a/propcache-0.5.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d", size = 98872, upload-time = "2026-05-08T21:00:59.889Z" }, + { url = "https://files.pythonhosted.org/packages/f4/d4/52c871e73e864e6b34c0e2d58ac1ec5ccd149497ddc7ad2137ae98323a35/propcache-0.5.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa", size = 56257, upload-time = "2026-05-08T21:01:01.195Z" }, + { url = "https://files.pythonhosted.org/packages/67/f0/9b90ca2a210b3d09bcfcd96ecd0f55545c091535abce2a45de2775cfd357/propcache-0.5.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94", size = 56696, upload-time = "2026-05-08T21:01:02.941Z" }, + { url = "https://files.pythonhosted.org/packages/9d/0e/6e9d4ba07c8e56e21ddec1e75f12148142b21ca83a51871babce095334f4/propcache-0.5.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164", size = 62378, upload-time = "2026-05-08T21:01:04.475Z" }, + { url = "https://files.pythonhosted.org/packages/65/19/c10badaa463dde8a27ce884f8ee2ec37e6035b7c9f5ff0c8f74f06f08dac/propcache-0.5.2-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f", size = 65283, upload-time = "2026-05-08T21:01:05.959Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b6/93bea99ca80e19cef6512a8580e5b7857bbe09422d9daa7fd4ef5723306c/propcache-0.5.2-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c", size = 66616, upload-time = "2026-05-08T21:01:07.228Z" }, + { url = "https://files.pythonhosted.org/packages/83/e4/5c7462e50625f051f37fb38b8224f7639f667184bbd34424ec83819bb1b7/propcache-0.5.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc", size = 63773, upload-time = "2026-05-08T21:01:08.514Z" }, + { url = "https://files.pythonhosted.org/packages/ca/b6/99238894047b13c823be25027e736626cd414a52a5e30d2c3347c2733529/propcache-0.5.2-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f", size = 63664, upload-time = "2026-05-08T21:01:09.874Z" }, + { url = "https://files.pythonhosted.org/packages/85/1e/a3a1a63116a2b8edb415a8bb9a6f0c34bd03830b1e18e8ce2904e1dc1cf4/propcache-0.5.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb", size = 62643, upload-time = "2026-05-08T21:01:11.132Z" }, + { url = "https://files.pythonhosted.org/packages/e4/03/893cf147de2fc6543c5eaa07ad833170e7e2a2385725bbebe8c0503723bb/propcache-0.5.2-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751", size = 59595, upload-time = "2026-05-08T21:01:12.387Z" }, + { url = "https://files.pythonhosted.org/packages/86/3b/04c1a2e12c57766568ba75ba72b3bf2042818d4c1425fab6fc07155c7cff/propcache-0.5.2-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836", size = 65711, upload-time = "2026-05-08T21:01:13.676Z" }, + { url = "https://files.pythonhosted.org/packages/1c/34/80f8d0099f8d6bacc4de1624c85672681c8cd1149ca2da0e38fd120b817f/propcache-0.5.2-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f", size = 64247, upload-time = "2026-05-08T21:01:14.936Z" }, + { url = "https://files.pythonhosted.org/packages/f3/1a/8b08f3a5f1037e9e370c55883ceeeee0f6dd0416fb2d2d67b8bfc91f2a79/propcache-0.5.2-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55", size = 67102, upload-time = "2026-05-08T21:01:16.281Z" }, + { url = "https://files.pythonhosted.org/packages/34/68/8bdb7bb7756d76e005490649d10e4a8369e610c74d619f71e1aedf889e9c/propcache-0.5.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568", size = 64964, upload-time = "2026-05-08T21:01:17.57Z" }, + { url = "https://files.pythonhosted.org/packages/0a/aa/50fb0b5d3968b61a510926ff8b8465f1d6e976b3ab74496d7a4b9fc42515/propcache-0.5.2-cp313-cp313t-win32.whl", hash = "sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191", size = 42546, upload-time = "2026-05-08T21:01:18.946Z" }, + { url = "https://files.pythonhosted.org/packages/ae/4c/0ddbae64321bd4a95bcbfc19307238016b5b1fee645c84626c8d539e5b74/propcache-0.5.2-cp313-cp313t-win_amd64.whl", hash = "sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7", size = 46330, upload-time = "2026-05-08T21:01:20.162Z" }, + { url = "https://files.pythonhosted.org/packages/00/d9/9cddc8efb78d8af264c5ec9f6d10b62f57c515feda8d321595f56010fb23/propcache-0.5.2-cp313-cp313t-win_arm64.whl", hash = "sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96", size = 40521, upload-time = "2026-05-08T21:01:21.399Z" }, + { url = "https://files.pythonhosted.org/packages/e2/ea/23ee535d90ce8bcc465a3028eb3cc0ce3bd1005f4bb27710b30587de798d/propcache-0.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999", size = 94662, upload-time = "2026-05-08T21:01:22.683Z" }, + { url = "https://files.pythonhosted.org/packages/b5/06/c5a52f419b5d8972f8d46a7577476090d8e3263ff589ce40b5ca4968d5be/propcache-0.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e", size = 53928, upload-time = "2026-05-08T21:01:23.986Z" }, + { url = "https://files.pythonhosted.org/packages/63/b1/4260d67d6bd85e58a66b72d54ce15d5de789b6f3870cc6bedf8ff9667401/propcache-0.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539", size = 54650, upload-time = "2026-05-08T21:01:25.305Z" }, + { url = "https://files.pythonhosted.org/packages/70/06/2f46c318e3307cd7a6a7481def374ce838c0fe20084b39dd54b0879d0e99/propcache-0.5.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e", size = 59912, upload-time = "2026-05-08T21:01:26.545Z" }, + { url = "https://files.pythonhosted.org/packages/4c/29/fe1aebec2ce57ab985a9c382bded1124431f85078113aa222c5d278430d4/propcache-0.5.2-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979", size = 63300, upload-time = "2026-05-08T21:01:27.937Z" }, + { url = "https://files.pythonhosted.org/packages/b4/18/2334b26768b6c82be8c69e83671b767d5ef426aa09b0cba6c2ea47816774/propcache-0.5.2-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80", size = 64208, upload-time = "2026-05-08T21:01:29.484Z" }, + { url = "https://files.pythonhosted.org/packages/2b/76/7f1bfd6afff4c5e38e36a3c6d68eb5f4b7311ea80baf693db78d95b603c4/propcache-0.5.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825", size = 61633, upload-time = "2026-05-08T21:01:31.068Z" }, + { url = "https://files.pythonhosted.org/packages/c4/46/b3ff8aba2b4953a3e50de2cf72f1b5748b8eca93b15f3dc2c84339084c09/propcache-0.5.2-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39", size = 61724, upload-time = "2026-05-08T21:01:32.374Z" }, + { url = "https://files.pythonhosted.org/packages/c5/01/814cfcafbcff954f94c01cf30e097ddc88a076b5440fbcf4570753437d40/propcache-0.5.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4", size = 60069, upload-time = "2026-05-08T21:01:33.67Z" }, + { url = "https://files.pythonhosted.org/packages/da/68/5c6f7622d510cc666a300687e06fd060c1a43361c0c9b20d284f06d8096a/propcache-0.5.2-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5", size = 57099, upload-time = "2026-05-08T21:01:34.915Z" }, + { url = "https://files.pythonhosted.org/packages/55/27/9cb0b4c679124085327957d42521c99dba04c88c90c3e55a6f0b633ebccc/propcache-0.5.2-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702", size = 63391, upload-time = "2026-05-08T21:01:36.231Z" }, + { url = "https://files.pythonhosted.org/packages/f0/9d/7258aaa5bdf60fc6f27591eef6fe52768cb0beda7140be477c8b12c9794a/propcache-0.5.2-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3", size = 61626, upload-time = "2026-05-08T21:01:37.545Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0d/41c602003e8a9b16fe1e7eadf62c7bfba9d5474370b24200bf48b315f45f/propcache-0.5.2-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5", size = 64781, upload-time = "2026-05-08T21:01:38.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/f3/38e66b1856e9bd079deea015bc4a55f7767c0e4db2f7dcf69e7e680ba4ce/propcache-0.5.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4", size = 62570, upload-time = "2026-05-08T21:01:40.415Z" }, + { url = "https://files.pythonhosted.org/packages/95/ca/bbfe9b910ce57dde8bb4876b4520fc02a4e89497c10de26be936758a3aaa/propcache-0.5.2-cp314-cp314-win32.whl", hash = "sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0", size = 39436, upload-time = "2026-05-08T21:01:41.654Z" }, + { url = "https://files.pythonhosted.org/packages/61/d2/45c9defbaa1ea297035d9d4cce9e8f80daafbf19319c6007f157c6256ea9/propcache-0.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c", size = 42373, upload-time = "2026-05-08T21:01:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/44/68/9ea5103f41d5217d7d6ec24db90018e23aebec070c3f9a6e54d12b841fd8/propcache-0.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0", size = 38554, upload-time = "2026-05-08T21:01:44.336Z" }, + { url = "https://files.pythonhosted.org/packages/8a/81/fadf555f42d3b762eea8a53950b0489fdc0aa9da5f8ed9e10ce0a4e01b48/propcache-0.5.2-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb", size = 99395, upload-time = "2026-05-08T21:01:45.883Z" }, + { url = "https://files.pythonhosted.org/packages/f5/c9/c61e134a686949cf7971af3a390148b1156f7be81c73bc0cd12c873e2d48/propcache-0.5.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078", size = 56653, upload-time = "2026-05-08T21:01:47.307Z" }, + { url = "https://files.pythonhosted.org/packages/cb/73/daf935ea7048ddd7ec8eec5345b4a40b619d2d178b3c0a0900796bc3c794/propcache-0.5.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa", size = 56914, upload-time = "2026-05-08T21:01:48.573Z" }, + { url = "https://files.pythonhosted.org/packages/79/9f/aba959b435ea18617edd7cf0a7ad0b9c574b8fc7e3d2cd55fb59cb255d33/propcache-0.5.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917", size = 62567, upload-time = "2026-05-08T21:01:49.903Z" }, + { url = "https://files.pythonhosted.org/packages/6c/a1/859942de9a791ff42f6141736f5b37749b8f53e65edfa49638c67dd67e6a/propcache-0.5.2-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe", size = 65542, upload-time = "2026-05-08T21:01:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/b5/61/315bc0fd6c0fc7f80a528b8afd209e5fc4a875ea79571b91b8f50f442907/propcache-0.5.2-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03", size = 66845, upload-time = "2026-05-08T21:01:52.539Z" }, + { url = "https://files.pythonhosted.org/packages/47/f7/9f8122e3132e8e354ac41975ef8f1099be7d5a16bc7ae562734e993665c0/propcache-0.5.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335", size = 63985, upload-time = "2026-05-08T21:01:53.847Z" }, + { url = "https://files.pythonhosted.org/packages/c8/54/c317819ec157cbf6f35df9df9657a6f82daf34d5faf15948b2f639c2192e/propcache-0.5.2-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285", size = 63999, upload-time = "2026-05-08T21:01:55.179Z" }, + { url = "https://files.pythonhosted.org/packages/5a/56/387e3f7dfce0a9233df41fb888aa1c30222cb4bbbf09537c02dd9bd85fe2/propcache-0.5.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837", size = 62779, upload-time = "2026-05-08T21:01:57.489Z" }, + { url = "https://files.pythonhosted.org/packages/a1/9c/596784cb5824ed61ee960d3f8655a3f0993e107c6e98ab6c818b7fb92ccb/propcache-0.5.2-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8", size = 59796, upload-time = "2026-05-08T21:01:58.736Z" }, + { url = "https://files.pythonhosted.org/packages/c2/3d/1a6cfa1726a48542c1e8784a0761421476a5b68e09b7f36bf95eb954aaba/propcache-0.5.2-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366", size = 66023, upload-time = "2026-05-08T21:02:00.228Z" }, + { url = "https://files.pythonhosted.org/packages/e4/0e/05fd6990369477076e4e280bcb970de760fddf0161a46e988bc95f7940ec/propcache-0.5.2-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56", size = 64448, upload-time = "2026-05-08T21:02:01.888Z" }, + { url = "https://files.pythonhosted.org/packages/cd/86/5f8da315a4309c62c10c0b2516b17492d5d3bbe1bb862b96604db67e2a37/propcache-0.5.2-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d", size = 67329, upload-time = "2026-05-08T21:02:03.484Z" }, + { url = "https://files.pythonhosted.org/packages/da/d3/3368efe79ab21f0cdf86ef49895811c9cc933131d4cde1f28a624e22e712/propcache-0.5.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2", size = 65172, upload-time = "2026-05-08T21:02:04.745Z" }, + { url = "https://files.pythonhosted.org/packages/d5/07/127e8b0bacfb325396196f9d976a22453049b89b9b2b08477cc3145faa44/propcache-0.5.2-cp314-cp314t-win32.whl", hash = "sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821", size = 43813, upload-time = "2026-05-08T21:02:06.025Z" }, + { url = "https://files.pythonhosted.org/packages/88/fb/46dad6c0ae49ed230ab1b16c890c2b6314e2403e6c412976f4a72d64a527/propcache-0.5.2-cp314-cp314t-win_amd64.whl", hash = "sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370", size = 47764, upload-time = "2026-05-08T21:02:07.353Z" }, + { url = "https://files.pythonhosted.org/packages/e7/c4/a47d0a63aa309d10d59ede6e9d4cff03a344a79d1f0f4cd0cd74997b53e0/propcache-0.5.2-cp314-cp314t-win_arm64.whl", hash = "sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6", size = 41140, upload-time = "2026-05-08T21:02:09.065Z" }, + { url = "https://files.pythonhosted.org/packages/3a/ed/1cdcab6ba3d6ab7feca11fc14f0eeea80755bb53ef4e892079f31b10a25f/propcache-0.5.2-py3-none-any.whl", hash = "sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe", size = 14036, upload-time = "2026-05-08T21:02:10.673Z" }, ] [[package]] From 4b9571e28efc58c44936c5974a7d051852718208 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 08:24:08 -0700 Subject: [PATCH 25/37] docs: scrub em dashes and refresh examples README - Replace em dashes with periods, semicolons, colons, or parens across README.md, CLAUDE.md, CONTRIBUTING.md, examples/README.md. - examples/README.md: remove stale claim that Python lacks `create_x402_server` / `create_mppx_server` factories; both have shipped (in `agentscore_commerce.payment`) and are now native. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 42 +++++++++++++++++++++--------------------- CONTRIBUTING.md | 2 +- README.md | 42 +++++++++++++++++++++--------------------- examples/README.md | 17 ++++++++--------- 4 files changed, 51 insertions(+), 52 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index e68ad32..14574ad 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -9,7 +9,7 @@ Every helper is extracted from a real consumer, not speculated. | Submodule | What it is | |---|---| | `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware (KYC, age, sanctions, jurisdiction) | -| `agentscore_commerce.payment` | Networks/USDC/rails registries, paymentauth.org directive builders, `create_x402_server` (wraps `x402[evm]>=2.9` + `cdp-sdk` for `facilitator="coinbase"`; install via the `coinbase` extra), `build_x402_accepts_for_402` (build the 402's `accepts[]` from the registered scheme — derives the right `extra.name` per network), `process_x402_settle` (verify+settle in one call), `create_mppx_server` (wraps `pympp[server,tempo,stripe]>=0.6`), dispatch-by-network, signer extraction, WWW-Authenticate header, Settlement-Overrides header | +| `agentscore_commerce.payment` | Networks/USDC/rails registries, paymentauth.org directive builders, `create_x402_server` (wraps `x402[evm]>=2.9` + `cdp-sdk` for `facilitator="coinbase"`; install via the `coinbase` extra), `build_x402_accepts_for_402` (build the 402's `accepts[]` from the registered scheme; derives the right `extra.name` per network), `process_x402_settle` (verify+settle in one call), `create_mppx_server` (wraps `pympp[server,tempo,stripe]>=0.6`), dispatch-by-network, signer extraction, WWW-Authenticate header, Settlement-Overrides header | | `agentscore_commerce.discovery` | Discovery probe, Bazaar wrapper, `/.well-known/mpp.json`, `llms.txt` builder, `skill.md` builder (Claude-Skill-compatible agent-discovery manifest), OpenAPI snippets, `NoindexNonDiscoveryMiddleware` ASGI middleware | | `agentscore_commerce.challenge` | 402-body builders: accepted_methods, identity_metadata, how_to_pay, agent_instructions, build_402_body, `build_validation_error` (4xx body builder) | | `agentscore_commerce.stripe_multichain` | Multichain PaymentIntent helper, deposit-address lookup, testnet simulator, mppx Stripe wrapper | @@ -17,7 +17,7 @@ Every helper is extracted from a real consumer, not speculated. ## Architecture -Single Python package, hatchling-built, published to PyPI as `agentscore-commerce`. Per-framework identity adapters expose the same surface — `AgentScoreGate` (or `agentscore_gate(app, ...)` for Flask/Sanic), `capture_wallet`, `verify_wallet_signer_match`, `get_assess_data`, `get_gate_degraded_state`, `get_gate_quota_info` — with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim). +Single Python package, hatchling-built, published to PyPI as `agentscore-commerce`. Per-framework identity adapters expose the same surface (`AgentScoreGate`, or `agentscore_gate(app, ...)` for Flask/Sanic; `capture_wallet`, `verify_wallet_signer_match`, `get_assess_data`, `get_gate_degraded_state`, `get_gate_quota_info`) with network-aware address normalization (EVM lowercased, Solana base58 preserved verbatim). | Directory | Contents | |---|---| @@ -30,11 +30,11 @@ Single Python package, hatchling-built, published to PyPI as `agentscore-commerc | `examples/` | Runnable single-file FastAPI apps for each common scenario | | `tests/` | pytest, one file per surface | -Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime — vendors install only what they use via extras (`pip install agentscore-commerce[fastapi,stripe]` etc.). Underlying packages: `x402[evm]`, `pympp[server,tempo,stripe]`, `stripe`, `cdp-sdk` (the `coinbase` extra — only needed when `facilitator="coinbase"`). Missing peer dep raises a guiding `ImportError` with the install command. +Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime; vendors install only what they use via extras (`pip install agentscore-commerce[fastapi,stripe]` etc.). Underlying packages: `x402[evm]`, `pympp[server,tempo,stripe]`, `stripe`, `cdp-sdk` (the `coinbase` extra; only needed when `facilitator="coinbase"`). Missing peer dep raises a guiding `ImportError` with the install command. ## Examples -`examples/` contains full single-file FastAPI apps for the most common merchant scenarios — copy-paste templates, not frameworks: +`examples/` contains full single-file FastAPI apps for the most common merchant scenarios; copy-paste templates, not frameworks: | Example | Scenario | |---|---| @@ -43,28 +43,28 @@ Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime — | `multi_rail_merchant.py` | Full agent-commerce: identity + Tempo MPP + x402 + Stripe SPT | | `stripe_multichain_merchant.py` | Stripe-anchored multichain (PaymentIntent → tempo/base/solana deposit addresses) | | `variable_cost_merchant.py` | Pay-per-actual-usage on **two protocols**: x402 upto (Permit2 + Settlement-Overrides) AND MPP tempo session (channel + SSE + mid-stream vouchers) | -| `compliance_merchant.py` | Regulated-goods merchant — full compliance gate + custom `on_denied` composing the denial helpers (`verification_agent_instructions`, `is_fixable_denial`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `denial_reason_to_body`/`denial_reason_status`) | +| `compliance_merchant.py` | Regulated-goods merchant: full compliance gate + custom `on_denied` composing the denial helpers (`verification_agent_instructions`, `is_fixable_denial`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `denial_reason_to_body`/`denial_reason_status`) | | `per_product_policy_merchant.py` | Multi-product merchant where each row carries its own compliance policy. One product hard-gates KYC + age + state; another is anonymous; a third uses `enforcement="soft"` (request KYC but don't block sale). Demonstrates `PolicyBlock`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed`. | ## 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 `agentscore_commerce/identity/address.py`: 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 `agentscore_commerce/identity/address.py`: EVM lowercased, Solana base58 preserved verbatim. Used for cache keys, wallet→operator resolves, and signer-match comparisons. `DenialReason` 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 carry a structured `agent_instructions` JSON block describing concrete recovery actions. See `agentscore_commerce/identity/_response.py` for the canned action copy. `create_session_on_missing` auto-mints a verification session when no identity is present and returns 403 with `verify_url` + poll instructions. `verify_wallet_signer_match` (per-adapter) compares the recovered signer against `linked_wallets[]` for cross-chain wallet-stack matching. -Captured wallets: `capture_wallet(...)` is fire-and-forget — reads `operator_token` stashed during gating and POSTs to `/v1/credentials/wallets`. No-ops for wallet-authenticated requests. +Captured wallets: `capture_wallet(...)` 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: `verify_wallet_signer_match` / `averify_wallet_signer_match` 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 `signer_match_by_signer` sub-dict and skip the API entirely. Falls back to a 2-resolve path when the API doesn't emit `signer_match` (canary rollout safety). +Wallet-signer-match: `verify_wallet_signer_match` / `averify_wallet_signer_match` 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 `signer_match_by_signer` sub-dict and skip the API entirely. Falls back to a 2-resolve path when the API doesn't emit `signer_match` (canary rollout safety). ### Fail-open (opt-in) -`fail_open=True` on `AgentScoreGate(...)` (or `agentscore_gate(app, ...)`) flips infra-failure handling: 429 / 5xx / network-timeout pass through to the handler with the gate state stamped `degraded=True` + `infra_reason="quota_exceeded" | "api_error" | "network_timeout"`. `get_gate_degraded_state(request)` (Flask: `get_gate_degraded_state()` — reads from `g`) returns `{"degraded": bool, "infra_reason"?: str}` for merchant logging/alerting. Default stays `fail_open=False` — regulated commerce should keep it. Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of the flag. The gate's `try` wraps only the AgentScore call — never the downstream user handler. +`fail_open=True` on `AgentScoreGate(...)` (or `agentscore_gate(app, ...)`) flips infra-failure handling: 429 / 5xx / network-timeout pass through to the handler with the gate state stamped `degraded=True` + `infra_reason="quota_exceeded" | "api_error" | "network_timeout"`. `get_gate_degraded_state(request)` (Flask: `get_gate_degraded_state()`, reads from `g`) returns `{"degraded": bool, "infra_reason"?: str}` for merchant logging/alerting. Default stays `fail_open=False`; regulated commerce should keep it. Compliance denials (sanctions, age, jurisdiction, signer-mismatch) still deny regardless of the flag. The gate's `try` wraps only the AgentScore call, never the downstream user handler. ### Mount posture: gate-first vs gate-conditional -`AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic) is mounted directly when the route is AgentScore-only — 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(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic) is mounted directly when the route is AgentScore-only; 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: ```python _gate = AgentScoreGate(api_key=..., require_kyc=True, ...) @@ -87,20 +87,20 @@ Anonymous POST flows through to the handler unauthenticated and gets a 402 with ### `compatible_clients` field on emitted 402s -`build_agent_instructions` emits a `compatible_clients` field in the 402 body, derived automatically from `how_to_pay` — per-rail list of CLIs the AgentScore team has smoke-verified end-to-end. Vendors override with `BuildAgentInstructionsInput(compatible_clients={...})` to add their own tested clients. Set to an empty dict `{}` to suppress the default. Same data is published as `core/docs/integrations/x402-clients.mdx` for human-side rationale + per-rail commands. +`build_agent_instructions` emits a `compatible_clients` field in the 402 body, derived automatically from `how_to_pay`: per-rail list of CLIs the AgentScore team has smoke-verified end-to-end. Vendors override with `BuildAgentInstructionsInput(compatible_clients={...})` to add their own tested clients. Set to an empty dict `{}` to suppress the default. Same data is published as `core/docs/integrations/x402-clients.mdx` for human-side rationale + per-rail commands. ## Tooling -- **uv** — package manager. -- **ruff** — linting + formatting. -- **ty** — type checker (Astral). -- **vulture** — dead code detection. -- **pytest** — tests. -- **Lefthook** — pre-commit ruff, pre-push ty + vulture (parallel). +- **uv**: package manager. +- **ruff**: linting + formatting. +- **ty**: type checker (Astral). +- **vulture**: dead code detection. +- **pytest**: tests. +- **Lefthook**: pre-commit ruff, pre-push ty + vulture (parallel). ```bash uv sync --all-extras -uv run lefthook install # one-time per clone — wires pre-commit + pre-push +uv run lefthook install # one-time per clone; wires pre-commit + pre-push uv run ruff check . uv run ruff format . uv run ty check agentscore_commerce/ @@ -112,16 +112,16 @@ uv run pytest tests/ 1. Create a branch 2. Make changes 3. Lefthook runs ruff on commit, ty + vulture 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 +- **Use PRs**: never push directly to main - **Helpers are protocol translations + configurable opinions, not opinionated frameworks** -- **Cross-language API parity** — keep the surface area identical between the node and python flavors so vendors switching languages have the same mental model +- **Cross-language API parity**: keep the surface area identical between the node and python flavors so vendors switching languages have the same mental model ## Releasing diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index b59db24..f2353d8 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 bbf0cf6..ce39ae7 100644 --- a/README.md +++ b/README.md @@ -3,7 +3,7 @@ [![PyPI version](https://img.shields.io/pypi/v/agentscore-commerce.svg)](https://pypi.org/project/agentscore-commerce/) [![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE) -The full merchant-side SDK for [AgentScore](https://agentscore.sh) in Python — agent commerce in one install. Identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI), payment helpers, 402 challenge builders, MPP discovery, and Stripe multichain support. +The full merchant-side SDK for [AgentScore](https://agentscore.sh) in Python: agent commerce in one install. Identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI), payment helpers, 402 challenge builders, MPP discovery, and Stripe multichain support. ## Install @@ -25,12 +25,12 @@ pip install 'agentscore-commerce[fastapi,x402,coinbase]' | Submodule | What it provides | |---|---| | `agentscore_commerce.identity.{fastapi,flask,django,aiohttp,sanic,middleware}` | Trust gate middleware: KYC, sanctions, age, jurisdiction. `AgentScoreGate(...)` (or `agentscore_gate(app, ...)` on Flask/Sanic), `get_assess_data(...)`, `capture_wallet(...)`, `verify_wallet_signer_match(...)`. | -| `agentscore_commerce.identity` (package level) | Re-exports the denial helpers: `denial_reason_status`, `denial_reason_to_body`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `verification_agent_instructions`, `is_fixable_denial`, `FIXABLE_DENIAL_REASONS`. Also re-exports the per-product policy helpers: `PolicyBlock`, `GateResult`, `EnforcementMode`, `IdentityStatus`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed` — for multi-product merchants where each product carries its own compliance config (hard gate vs soft vs none, per-product shipping allowlists). | -| `agentscore_commerce.payment` | `networks`, `USDC`, `rails` registries; `payment_directive`, `build_payment_directive`, `www_authenticate_header`, `payment_required_header`, `alias_amount_fields` (v1↔v2 amount field shim — emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlement_override_header`, `dispatch_settlement_by_network`, `extract_payment_signer` (returns `PaymentSigner({address, network})`), `register_x402_schemes_v1_v2`; drop-in x402 helpers: `validate_x402_network_config` (boot-time guard), `verify_x402_request` (parse + validate inbound X-Payment), `process_x402_settle` (verify-then-settle with one call), `classify_x402_settle_result` (maps the tagged settle result to a recommended HTTP status / code / next_steps so merchants get a controlled envelope without coupling to facilitator-specific error text). | -| `agentscore_commerce.discovery` | `is_discovery_probe_request`, `build_discovery_probe_response` (with optional `x402_sample` for x402-aware crawlers — `awal x402 details` etc.), `sample_x402_accept_for_network` (USDC sample-accept builder for known CAIP-2 networks), `build_well_known_mpp`, `build_llms_txt` + `llms_txt_identity_section` + `llms_txt_payment_section` (compact + verbose modes), `build_skill_md` (Claude-Skill-compatible `/skill.md` agent-discovery manifest — strictly agent-facing data only, no internal posture), `agentscore_openapi_snippets`, `build_bazaar_discovery_payload`, `NoindexNonDiscoveryMiddleware` (ASGI 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 `is_discovery_path` + `DEFAULT_DISCOVERY_PATHS` for non-ASGI frameworks). | -| `agentscore_commerce.challenge` | `build_402_body`, `build_accepted_methods`, `build_identity_metadata`, `build_how_to_pay`, `build_agent_instructions` (auto-emits per-rail `compatible_clients` — smoke-verified CLIs the agent should use; vendor override supported), `build_pricing_block` (cents → dollar-string with optional shipping/tax), `first_encounter_agent_memory` (cross-merchant hint, returns the canonical block or `None` based on a per-merchant first-seen flag), `OrderReceipt` (dataclass for the post-settlement 200 response shape); `respond_402` — drop-in 402 emit that preserves pympp's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `build_validation_error` — 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. | +| `agentscore_commerce.identity` (package level) | Re-exports the denial helpers: `denial_reason_status`, `denial_reason_to_body`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `verification_agent_instructions`, `is_fixable_denial`, `FIXABLE_DENIAL_REASONS`. Also re-exports the per-product policy helpers: `PolicyBlock`, `GateResult`, `EnforcementMode`, `IdentityStatus`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed` (for multi-product merchants where each product carries its own compliance config: hard gate vs soft vs none, per-product shipping allowlists). | +| `agentscore_commerce.payment` | `networks`, `USDC`, `rails` registries; `payment_directive`, `build_payment_directive`, `www_authenticate_header`, `payment_required_header`, `alias_amount_fields` (v1↔v2 amount field shim that emits both `amount` and `maxAmountRequired` so v1-only x402 parsers like Coinbase awal can read v2 bodies), `settlement_override_header`, `dispatch_settlement_by_network`, `extract_payment_signer` (returns `PaymentSigner({address, network})`), `register_x402_schemes_v1_v2`; drop-in x402 helpers: `validate_x402_network_config` (boot-time guard), `verify_x402_request` (parse + validate inbound X-Payment), `process_x402_settle` (verify-then-settle with one call), `classify_x402_settle_result` (maps the tagged settle result to a recommended HTTP status / code / next_steps so merchants get a controlled envelope without coupling to facilitator-specific error text). | +| `agentscore_commerce.discovery` | `is_discovery_probe_request`, `build_discovery_probe_response` (with optional `x402_sample` for x402-aware crawlers like `awal x402 details`), `sample_x402_accept_for_network` (USDC sample-accept builder for known CAIP-2 networks), `build_well_known_mpp`, `build_llms_txt` + `llms_txt_identity_section` + `llms_txt_payment_section` (compact + verbose modes), `build_skill_md` (Claude-Skill-compatible `/skill.md` agent-discovery manifest; strictly agent-facing data only, no internal posture), `agentscore_openapi_snippets`, `build_bazaar_discovery_payload`, `NoindexNonDiscoveryMiddleware` (ASGI 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 `is_discovery_path` + `DEFAULT_DISCOVERY_PATHS` for non-ASGI frameworks). | +| `agentscore_commerce.challenge` | `build_402_body`, `build_accepted_methods`, `build_identity_metadata`, `build_how_to_pay`, `build_agent_instructions` (auto-emits per-rail `compatible_clients`: smoke-verified CLIs the agent should use; vendor override supported), `build_pricing_block` (cents to dollar-string with optional shipping/tax), `first_encounter_agent_memory` (cross-merchant hint, returns the canonical block or `None` based on a per-merchant first-seen flag), `OrderReceipt` (dataclass for the post-settlement 200 response shape); `respond_402`, a drop-in 402 emit that preserves pympp's `WWW-Authenticate` and layers x402's `PAYMENT-REQUIRED`. `build_validation_error`: 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. | | `agentscore_commerce.stripe_multichain` | `create_multichain_payment_intent`, `get_deposit_address`, `simulate_crypto_deposit`; `create_pi_cache` (TTL'd PI / deposit-address cache, Redis-backed when `redis_url` set, in-memory otherwise), `simulate_deposit_if_test_mode` (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`. | -| `agentscore_commerce.api` | Everything from `agentscore-py` re-exported in one place: `AgentScore` + `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` + `is_agentscore_test_address`. **Don't add `agentscore-py` as a separate dep** — the two can drift versions and cause subtle type mismatches. | +| `agentscore_commerce.api` | Everything from `agentscore-py` re-exported in one place: `AgentScore` + `AgentScoreError`, `AGENTSCORE_TEST_ADDRESSES` + `is_agentscore_test_address`. **Don't add `agentscore-py` as a separate dep**: the two can drift versions and cause subtle type mismatches. | ## Quick start (FastAPI) @@ -52,7 +52,7 @@ _gate = AgentScoreGate( ) -# 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. @@ -99,7 +99,7 @@ directives = [ ] www_auth = www_authenticate_header(directives) -# Recover the on-chain signer (EVM) from an x402 header — returns PaymentSigner | None +# Recover the on-chain signer (EVM) from an x402 header. Returns PaymentSigner | None. signer = extract_payment_signer(request.headers.get("x-payment")) if signer: print(signer.address, signer.network) # ('0x...', 'evm') @@ -189,10 +189,10 @@ from agentscore_commerce.identity import ( build_ucp_profile, ) -# 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. card = build_a2a_agent_card(name="My Service", url=base_url, capabilities=A2AAgentCardCapabilities(...), data=assess_result) -# Google Universal Commerce Protocol — publish at /.well-known/ucp +# Google Universal Commerce Protocol. Publish at /.well-known/ucp. profile = build_ucp_profile( name="My Service", services=[UCPService(type="rest", url=base_url)], @@ -240,7 +240,7 @@ jwks = build_jwks_response([key.public_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 to prevent a swap-after-sign attack. -ACP (Stripe + OpenAI Agentic Commerce Protocol) is a transactional checkout protocol with no identity-publishing surface — ACP merchants integrate via the existing `build_402_body` + `build_payment_headers` + 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 `build_402_body` + `build_payment_headers` + Stripe SPT rail. ## Stripe multichain @@ -268,15 +268,15 @@ result = create_multichain_payment_intent(CreateMultichainPaymentIntentInput( base_address = get_deposit_address(result, "base") solana_address = get_deposit_address(result, "solana") -# PI / deposit-address cache. Redis-backed when REDIS_URL is set, in-memory otherwise — -# multi-instance deployments need Redis so a deposit lands on whichever instance settles it. +# PI / deposit-address cache. Redis-backed when REDIS_URL is set, in-memory otherwise. +# Multi-instance deployments need Redis so a deposit lands on whichever instance settles it. pi_cache = create_pi_cache(PiCacheOptions(redis_url=os.environ.get("REDIS_URL"))) for addr in result.deposit_addresses.values(): await pi_cache.cache_address(addr) pi_cache.cache_payment_intent(addr, result.payment_intent_id) pi_cache.cache_network_addresses(result.payment_intent_id, result.deposit_addresses) -# 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 simulate_deposit_if_test_mode(SimulateDepositIfTestModeInput( get_payment_intent_id=pi_cache.get_payment_intent_id, deposit_address=base_address, @@ -316,12 +316,12 @@ from agentscore_commerce.payment import ( verify_x402_request, ) -# Boot-time guard — raises if a configured network isn't supported. +# Boot-time guard. Raises if a configured network isn't supported. validate_x402_network_config(ValidateX402NetworkConfigInput(base_network=X402_BASE)) @app.post("/purchase") async def purchase(request: Request): - # Path A — agent presented an x402 X-Payment header + # Path A: agent presented an x402 X-Payment header if request.headers.get("payment-signature") or request.headers.get("x-payment"): verified = await verify_x402_request(VerifyX402RequestInput( headers=dict(request.headers), @@ -349,7 +349,7 @@ async def purchase(request: Request): headers = {"payment-response": settle.payment_response_header} if settle.payment_response_header else {} return JSONResponse({"ok": True}, headers=headers) - # Path B — cold call (or Authorization: Payment for pympp). After pympp.compose() returns 402, + # Path B: cold call (or Authorization: Payment for pympp). After pympp.compose() returns 402, # respond_402 PRESERVES pympp's WWW-Authenticate and ADDS x402's PAYMENT-REQUIRED. result = respond_402(Respond402Input( mppx_challenge_headers=pympp_challenge_headers, @@ -374,14 +374,14 @@ gate = AgentScoreGate(api_key=os.environ["AGENTSCORE_API_KEY"], fail_open=True) async def purchase(request: Request): state = get_gate_degraded_state(request) if state["degraded"]: - # Compliance was NOT enforced this request — log/alert/refund-async/etc. + # Compliance was NOT enforced this request: log/alert/refund-async/etc. logger.warning("gate degraded: %s", state["infra_reason"]) # ...rest of handler ``` -When `fail_open=True` AND the failure is infra-shape, the gate state carries `degraded=True` + `infra_reason="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 `fail_open`** — `fail_open` only covers "AgentScore couldn't tell us," never "AgentScore said no." +When `fail_open=True` AND the failure is infra-shape, the gate state carries `degraded=True` + `infra_reason="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 `fail_open`**; `fail_open` only covers "AgentScore couldn't tell us," never "AgentScore said no." -For regulated commerce (alcohol, age-gated, sanctioned-jurisdiction-relevant) keep the default `fail_open=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 `fail_open=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 `get_gate_degraded_state` helper is exported by every framework adapter (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI middleware) and reads from the framework-appropriate request state. The signature takes a request argument everywhere except Flask, which reads from `g` and takes no arguments. @@ -391,7 +391,7 @@ The [examples/](./examples) directory has 7 runnable single-file FastAPI apps co ## Stability -`agentscore-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. +`agentscore-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. ## Documentation diff --git a/examples/README.md b/examples/README.md index b42ee37..0a10827 100644 --- a/examples/README.md +++ b/examples/README.md @@ -4,13 +4,13 @@ Runnable, copy-pasteable example integrations covering the most common merchant | Example | Scenario | What it shows | |---|---|---| -| [`identity_only.py`](./identity_only.py) | Compliance gate without payment | Minimal — wraps any endpoint with KYC + age + jurisdiction checks. Vendor handles their own payment. | +| [`identity_only.py`](./identity_only.py) | Compliance gate without payment | Minimal: wraps any endpoint with KYC + age + jurisdiction checks. Vendor handles their own payment. | | [`api_provider.py`](./api_provider.py) | API provider (Exa-style) | Per-call billing on multiple rails: Tempo MPP + x402 (Base + Solana). Discovery probe responder + multi-rail 402 challenge. No identity gate. | | [`multi_rail_merchant.py`](./multi_rail_merchant.py) | Full agent-commerce merchant | Identity gate + Tempo MPP + x402 (Base + Solana) + Stripe SPT, all rails accepted, full 402 builder using `build_402_body` + `build_accepted_methods` + `build_how_to_pay` + `build_agent_instructions`. | | [`stripe_multichain_merchant.py`](./stripe_multichain_merchant.py) | Stripe-anchored multi-chain | Stripe PaymentIntent with deposit_options for tempo/base/solana; crypto deposits flow through Stripe. Includes testnet `simulate_crypto_deposit` helper. | | [`variable_cost_merchant.py`](./variable_cost_merchant.py) | 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. | | [`compliance_merchant.py`](./compliance_merchant.py) | Regulated-goods merchant (wine, cannabis, etc.) | Full compliance gate + custom `on_denied` composing commerce helpers: `verification_agent_instructions`, `is_fixable_denial`, `build_contact_support_next_steps`, `denial_reason_to_body`/`denial_reason_status`, `build_signer_mismatch_body`. Shows how vendors write only the business-specific branches and let commerce handle the rest. | -| [`per_product_policy_merchant.py`](./per_product_policy_merchant.py) | Multi-product merchant with mixed compliance needs | One product carries a hard gate (wine — KYC + 21 + US-state allowlist), another has no gate at all (anonymous merch, ships anywhere), a third uses `enforcement="soft"` (request KYC as a fraud signal but accept anonymous sales, stamping `identity_status="unverified"` on the order). Uses `PolicyBlock`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed`. | +| [`per_product_policy_merchant.py`](./per_product_policy_merchant.py) | Multi-product merchant with mixed compliance needs | One product carries a hard gate (wine: KYC + 21 + US-state allowlist), another has no gate at all (anonymous merch, ships anywhere), a third uses `enforcement="soft"` (request KYC as a fraud signal but accept anonymous sales, stamping `identity_status="unverified"` on the order). Uses `PolicyBlock`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed`. | ## How to use @@ -19,7 +19,7 @@ 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 `uvicorn examples.:app --port 3000` -6. Iterate — these are templates, not frameworks +6. Iterate; these are templates, not frameworks ## Patterns @@ -39,16 +39,15 @@ These examples are intentionally thin on domain logic. Vendors plug in their own - Order storage (Postgres, 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. ## Differences from node-commerce examples -Python doesn't have peer-dep equivalents for `@x402/core`, `@x402/evm`, `@solana/mpp`, or `mppx` — those are TypeScript-only ecosystems today. Three implications: +Python wraps `x402[evm]` and `pympp[server,tempo,stripe]` as peer deps; `@solana/mpp` has no Python equivalent today. Two implications: -1. **No `create_x402_server` / `create_mppx_server` factories.** The commerce package exposes `register_x402_schemes_v1_v2` for the x402 v1+v2 dispatch helper, but happy-path setup (registering the facilitator, schemes, etc.) is something Python merchants do via direct HTTP calls to their facilitator of choice. -2. **`extract_payment_signer` returns EVM only.** Solana SPL Token payer recovery requires a Solana SDK (`solders` / `solana-py`) which isn't bundled. Pass the recovered Solana payer via `signer=...` to `verify_wallet_signer_match` directly. -3. **Streaming session payments (variable_cost_merchant.py)** sketches the protocol but doesn't ship a working tempo session implementation — there's no pip-installable `mppx` equivalent. The example shows the response shape; vendors using session payments today should check the [tempo session protocol docs](https://mpp.dev/guides/streamed-payments) and bind to a Solana wallet library directly. +1. **`extract_payment_signer` returns EVM only.** Solana SPL Token payer recovery requires a Solana SDK (`solders` / `solana-py`) which isn't bundled. Pass the recovered Solana payer via `signer=...` to `verify_wallet_signer_match` directly. +2. **Streaming session payments (variable_cost_merchant.py)** sketches the protocol but doesn't ship a working tempo session implementation; there's no pip-installable `mppx` equivalent. The example shows the response shape; vendors using session payments today should check the [tempo session protocol docs](https://mpp.dev/guides/streamed-payments) and bind to a Solana wallet library directly. -For Python merchants on x402 alone (Base or Solana), every other helper (directives, headers, dispatch, settle-overrides, signer extraction for EVM, accepted_methods, agent_instructions, how_to_pay) is fully native. +For Python merchants on x402 alone (Base or Solana), every helper (`create_x402_server`, `create_mppx_server`, directives, headers, dispatch, settle-overrides, signer extraction for EVM, accepted_methods, agent_instructions, how_to_pay) is fully native. From 7144c526d392ccaa02e2ca38b96e7a647ee69d83 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 08:45:53 -0700 Subject: [PATCH 26/37] hardening(examples): sanitize UCP self-test exception, drop unused Any import CodeQL flagged the `/_selftest/ucp` route returning `str(exc)` to the HTTP response body as information exposure. Match the round-26 sanitization pattern used in core/store: log the full exception server-side via logger.exception, expose only `type(exc).__name__` + the structured verification code to the caller. The error class name is enough for an operator to triage without revealing internal verification machinery. Also drop the unused `Any` import from tests/test_ucp.py; the only cast() target gets retyped to `OperatorVerification` (the actual field type), which keeps the typed-empty-wins-over-raw test intent intact. Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/signed_ucp_merchant.py | 6 +++++- tests/test_ucp.py | 4 ++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/examples/signed_ucp_merchant.py b/examples/signed_ucp_merchant.py index f6bce3f..392b88b 100644 --- a/examples/signed_ucp_merchant.py +++ b/examples/signed_ucp_merchant.py @@ -141,4 +141,8 @@ async def selftest() -> JSONResponse: verify_ucp_profile(profile, jwks) return JSONResponse({"ok": True, "kid": profile["signing_keys"][0]["kid"]}) except UCPVerificationError as exc: - return JSONResponse({"ok": False, "code": exc.code, "message": str(exc)}, status_code=500) + logger.exception("UCP self-test verification failed") + return JSONResponse( + {"ok": False, "code": exc.code, "error": type(exc).__name__}, + status_code=500, + ) diff --git a/tests/test_ucp.py b/tests/test_ucp.py index 1f2aa61..52b4ce5 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -1,6 +1,6 @@ """Tests for build_ucp_profile.""" -from typing import Any, cast +from typing import cast import pytest @@ -386,7 +386,7 @@ def test_typed_empty_operator_verification_wins_over_raw() -> None: allow=True, resolved_operator="op_xyz", # Empty dict is a valid typed value (means "operator block returned empty"). - operator_verification=cast("Any", {}), + operator_verification=cast("OperatorVerification", {}), raw={"operator_verification": {"level": "enhanced", "verified_at": "2026-01-01T00:00:00Z"}}, ) profile = build_ucp_profile(**_base_kwargs(), data=result) From 674532077a7631167b06f3fbce4edf69a8e079f4 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 10:08:17 -0700 Subject: [PATCH 27/37] 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.py`` orchestrator. Hand-crafted ``*-capability.json`` fixtures bumped to the new name to keep the corpus honest about what callers should publish. 871 tests pass at 95.20% coverage. Cross-lang verify against the node sibling's regenerated ``node-*`` corpus passes byte-identically for the data-driven and typed-claims fixtures. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- agentscore_commerce/identity/ucp.py | 10 +- agentscore_commerce/identity/ucp_jwks.py | 13 +- pyproject.toml | 2 +- scripts/regenerate_cross_lang_fixtures.py | 263 ++++++++++++++++++ .../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/test_ucp.py | 3 +- tests/test_ucp_jwks.py | 10 +- uv.lock | 2 +- 28 files changed, 376 insertions(+), 107 deletions(-) create mode 100644 scripts/regenerate_cross_lang_fixtures.py diff --git a/README.md b/README.md index ce39ae7..88c5dbe 100644 --- a/README.md +++ b/README.md @@ -230,7 +230,7 @@ signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=ke jwks = build_jwks_response([key.public_jwk]) ``` -`verify_ucp_profile` 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 raise `UCPVerificationError` (a `ValueError` subclass) with a discriminated `code` attribute (`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`). +`verify_ucp_profile` 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 raise `UCPVerificationError` (a `ValueError` subclass) with a discriminated `code` attribute (`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`). `sign_ucp_profile` rejects profiles containing `float` values and `int` values whose magnitude exceeds `Number.MAX_SAFE_INTEGER` (2^53 - 1): cross-language float canonicalization is not stable, and Python's arbitrary-width ints lose precision when JS verifiers reparse the canonical body. Use decimal strings (e.g. `"9.99"`) for monetary or fractional fields and for any integer that may exceed the safe range. diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index dcb3e06..8501ac8 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -28,7 +28,11 @@ _DEFAULT_VERSION = "2026-04-17" _SPEC_URL = "https://ucp.dev/" -AGENTSCORE_UCP_CAPABILITY = "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. +AGENTSCORE_UCP_CAPABILITY = "sh.agentscore.identity" """Capability name AgentScore registers in the UCP profile. Consumers filter on this to find verified-buyer claims attached to the profile.""" @@ -241,7 +245,7 @@ def build_ucp_profile( """Compose a UCP profile body for ``/.well-known/ucp`` publication. Merges AgentScore identity claims into ``capabilities`` as an - ``agentscore-identity`` capability when ``data`` carries a resolved operator. + ``sh.agentscore.identity`` capability when ``data`` carries a resolved operator. Consumers reading the profile can opt into the AgentScore claims by filtering on the capability name. @@ -325,7 +329,7 @@ async def ucp_profile(): UCPCapability( name=AGENTSCORE_UCP_CAPABILITY, version=_AGENTSCORE_CAPABILITY_VERSION, - schema=agentscore_schema_url or "https://agentscore.sh/schemas/ucp/agentscore-identity.v1.json", + schema=agentscore_schema_url or "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", extras={"claims": claims}, ), ) diff --git a/agentscore_commerce/identity/ucp_jwks.py b/agentscore_commerce/identity/ucp_jwks.py index 4425111..42d6a1a 100644 --- a/agentscore_commerce/identity/ucp_jwks.py +++ b/agentscore_commerce/identity/ucp_jwks.py @@ -39,7 +39,10 @@ ) _ALLOWED_ALGS = ("EdDSA", "ES256") -_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. +_PROFILE_TYP = "agentscore-profile+jws" _MAX_SAFE_INT = 2**53 - 1 @@ -307,7 +310,7 @@ def sign_ucp_profile( raise ValueError(msg) canonical_body = _canonicalize_profile(profile) - header = {"alg": alg, "kid": kid, "typ": _UCP_TYP} + header = {"alg": alg, "kid": kid, "typ": _PROFILE_TYP} # joserfc treats EdDSA as "not recommended" by default; UCP §6 explicitly accepts # both EdDSA and ES256, so allow both. registry = JWSRegistry(algorithms=list(_ALLOWED_ALGS)) @@ -347,7 +350,7 @@ def verify_ucp_profile( """Verify a signed UCP profile against a JWKS. Returns ``True`` when: - * the JWS protected header carries ``kid`` + ``typ='ucp-profile+jws'`` + a + * the JWS protected header carries ``kid`` + ``typ='agentscore-profile+jws'`` + a registered ``alg`` (EdDSA or ES256), * the JWKS contains exactly one key with the matching ``kid``, * the JWS signature validates against that key, @@ -394,10 +397,10 @@ def verify_ucp_profile( # Pre-deserialize header checks — joserfc's deserialize_compact accepts kid-less # JWSs (it iterates the KeySet) so we enforce kid/typ/alg ourselves. header = _peek_jws_header(sig) - if header.get("typ") != _UCP_TYP: + if header.get("typ") != _PROFILE_TYP: raise UCPVerificationError( "wrong_typ", - f"UCP signature typ must be {_UCP_TYP!r}; got {header.get('typ')!r}.", + f"UCP signature typ must be {_PROFILE_TYP!r}; got {header.get('typ')!r}.", ) if header.get("alg") not in _ALLOWED_ALGS: raise UCPVerificationError( diff --git a/pyproject.toml b/pyproject.toml index 6560c1c..b3cb966 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "hatchling.build" [project] name = "agentscore-commerce" -version = "1.3.7" +version = "1.4.0" description = "Agent commerce SDK for Python — identity middleware (FastAPI, Flask, Django, AIOHTTP, Sanic, ASGI) + payment helpers + 402 builders + discovery + Stripe multichain. The full merchant-side toolkit for AgentScore-powered agent commerce." readme = "README.md" license = "MIT" diff --git a/scripts/regenerate_cross_lang_fixtures.py b/scripts/regenerate_cross_lang_fixtures.py new file mode 100644 index 0000000..4e4d91f --- /dev/null +++ b/scripts/regenerate_cross_lang_fixtures.py @@ -0,0 +1,263 @@ +"""Regenerate the full cross-lang fixture corpus (Python side). + +Writes all ``py-*.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/test_ucp_cross_lang.py`` (and the Node sibling) pulls these +in alongside the ``node-*`` fixtures generated by the Node sibling. +""" + +from __future__ import annotations + +import json +from pathlib import Path +from typing import Any + +from agentscore_commerce.identity import ( + AssessResult, + OperatorVerification, + UCPCapability, + UCPPaymentHandler, + UCPService, + UCPSigningKey, + build_ucp_profile, +) +from agentscore_commerce.identity.ucp_jwks import ( + build_jwks_response, + generate_ucp_signing_key, + sign_ucp_profile, +) + +OUT_DIR = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "cross-lang" + + +def _write(name: str, env: dict[str, Any]) -> None: + out = OUT_DIR / f"{name}.json" + out.write_text(json.dumps(env, indent=2, ensure_ascii=False) + "\n") + print(f"wrote {out}") + + +def _envelope(signed: dict[str, Any], public_jwk: dict[str, Any], alg: str, kid: str) -> dict[str, Any]: + return { + "profile": signed, + "jwks": build_jwks_response([public_jwk]), + "alg": alg, + "kid": kid, + "generator": "python", + } + + +def main() -> None: + # py-minimal + kid = "py-minimal-EdDSA" + key = generate_ucp_signing_key(kid=kid) + profile = build_ucp_profile( + name="Minimal Merchant", + services=[UCPService(type="rest", url="https://m.example.com")], + payment_handlers=[], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-minimal", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-es256-rails + kid = "py-es256-rails-ES256" + key = generate_ucp_signing_key(kid=kid, alg="ES256") + profile = build_ucp_profile( + name="ES256 Merchant", + services=[ + UCPService(type="rest", url="https://a.example.com"), + UCPService(type="a2a", url="https://a.example.com/agent-card.json"), + ], + payment_handlers=[ + UCPPaymentHandler(name="tempo", config={"rail": "tempo-mainnet", "chain_id": 4217}), + UCPPaymentHandler(name="x402", config={"networks": ["base-8453"]}), + ], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid, alg="ES256") + _write("py-es256-rails", _envelope(signed, key.public_jwk, "ES256", kid)) + + # py-extras-int + kid = "py-extras-int-EdDSA" + key = generate_ucp_signing_key(kid=kid) + profile = build_ucp_profile( + name="Extras Merchant", + services=[UCPService(type="rest", url="https://e.example.com")], + payment_handlers=[UCPPaymentHandler(name="stripe", config={"profile_id": "abc", "count": 7})], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-extras-int", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-capability — 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). + kid = "py-capability-EdDSA" + key = generate_ucp_signing_key(kid=kid) + profile = build_ucp_profile( + name="Capability Merchant", + services=[UCPService(type="rest", url="https://c.example.com")], + capabilities=[ + UCPCapability( + name="sh.agentscore.identity", + schema="https://agentscore.sh/schema/identity/1", + version="1", + extras={"kyc_required": True}, + ), + ], + payment_handlers=[ + UCPPaymentHandler(name="tempo", config={"rail": "tempo-mainnet", "chain_id": 4217}), + ], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-capability", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-unicode + kid = "py-unicode-EdDSA" + key = generate_ucp_signing_key(kid=kid) + profile = build_ucp_profile( + name="Café 日本 🍷 Merchant", + services=[UCPService(type="rest", url="https://日本.example.com")], + payment_handlers=[UCPPaymentHandler(name="tempo", config={"note": "メモ"})], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-unicode", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-multikey — JWKS with two keys, signed by the newer one. + old_key = generate_ucp_signing_key(kid="py-multikey-old") + new_key = generate_ucp_signing_key(kid="py-multikey-new") + profile = build_ucp_profile( + name="Multi-Key Merchant", + services=[UCPService(type="rest", url="https://mk.example.com")], + payment_handlers=[UCPPaymentHandler(name="tempo", config={"rail": "tempo-mainnet"})], + signing_keys=[ + UCPSigningKey.from_jwk(old_key.public_jwk), + UCPSigningKey.from_jwk(new_key.public_jwk), + ], + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=new_key.private_key, kid="py-multikey-new") + _write( + "py-multikey", + { + "profile": signed, + "jwks": build_jwks_response([old_key.public_jwk, new_key.public_jwk]), + "alg": "EdDSA", + "kid": "py-multikey-new", + "generator": "python", + }, + ) + + # py-emoji-keys — extras with non-ASCII object keys (BMP private use, CJK + # compatibility, supplementary plane). Exercises codepoint-vs-UTF-16 sort. + kid = "py-emoji-keys-EdDSA" + key = generate_ucp_signing_key(kid=kid) + profile = build_ucp_profile( + name="Emoji Keys Merchant", + services=[UCPService(type="rest", url="https://emoji.example.com")], + payment_handlers=[UCPPaymentHandler(name="tempo", config={})], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + extras={ + "extras": { + "a": 1, + "豈": 2, + "": 3, + "🍷": 4, + }, + }, + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-emoji-keys", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-int-boundary — exercises Number.MAX_SAFE_INTEGER round-trip. + kid = "py-int-boundary-EdDSA" + key = generate_ucp_signing_key(kid=kid) + profile = build_ucp_profile( + name="Int Boundary Merchant", + services=[UCPService(type="rest", url="https://i.example.com")], + payment_handlers=[], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + extras={ + "extras": { + "max_safe_int": 9007199254740991, + "min_safe_int": -9007199254740991, + "small_int": 42, + "neg_small_int": -42, + "zero": 0, + }, + }, + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-int-boundary", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-data-driven-claims — exercises the build_ucp_profile data path with + # API-shape "missing" sentinels (empty string + None). Both languages MUST + # emit identical canonical bytes for this input. + kid = "py-data-driven-claims-EdDSA" + key = generate_ucp_signing_key(kid=kid) + result = AssessResult( + allow=True, + resolved_operator="op_data_driven", + verify_url="https://agentscore.sh/verify/op_data_driven", + raw={ + "account_verification": { + "kyc_level": "", + "sanctions_clear": False, + "age_bracket": None, + "jurisdiction": None, + "verified_at": None, + }, + }, + ) + profile = build_ucp_profile( + name="Data Driven Claims Merchant", + services=[UCPService(type="rest", url="https://d.example.com")], + payment_handlers=[], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + data=result, + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-data-driven-claims", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + # py-typed-claims — exercises the typed AssessResult fields (no raw + # fallback). Cross-lang parity check for the typed-field-only call site. + kid = "py-typed-claims-EdDSA" + key = generate_ucp_signing_key(kid=kid) + result = AssessResult( + allow=True, + resolved_operator="op_typed_claims", + verify_url="https://agentscore.sh/verify/op_typed_claims", + operator_verification=OperatorVerification( + 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", + }, + raw=None, + ) + profile = build_ucp_profile( + name="Typed Claims Merchant", + services=[UCPService(type="rest", url="https://t.example.com")], + payment_handlers=[], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + data=result, + ) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) + _write("py-typed-claims", _envelope(signed, key.public_jwk, "EdDSA", kid)) + + +if __name__ == "__main__": + main() 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/test_ucp.py b/tests/test_ucp.py index 52b4ce5..3aa0f02 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -60,7 +60,8 @@ def test_appends_agentscore_capability_when_data_provided(): assert len(matching) == 1 cap = matching[0] assert cap["version"] == "1" - assert "agentscore-identity.v1.json" in cap["schema"] + assert cap["name"] == "sh.agentscore.identity" + assert "sh-agentscore-identity-v1.json" in cap["schema"] claims = cap["claims"] assert claims["operator_id"] == "op_abc" assert claims["kyc_level"] == "enhanced" diff --git a/tests/test_ucp_jwks.py b/tests/test_ucp_jwks.py index a0e8556..2d28589 100644 --- a/tests/test_ucp_jwks.py +++ b/tests/test_ucp_jwks.py @@ -166,7 +166,7 @@ def test_rejects_kid_less_jws(self) -> None: ) registry = JWSRegistry(algorithms=["EdDSA", "ES256"]) kid_less_sig = jws.serialize_compact( - {"alg": "EdDSA", "typ": "ucp-profile+jws"}, + {"alg": "EdDSA", "typ": "agentscore-profile+jws"}, canonical, signer.private_key, registry=registry, @@ -211,7 +211,7 @@ def test_rejects_unsupported_alg(self) -> None: oct_key = OctKey.generate_key(parameters={"kid": "real", "alg": "HS256", "use": "sig"}) registry = JWSRegistry(algorithms=["HS256"]) evil_sig = jws.serialize_compact( - {"alg": "HS256", "kid": "real", "typ": "ucp-profile+jws"}, + {"alg": "HS256", "kid": "real", "typ": "agentscore-profile+jws"}, canonical, oct_key, registry=registry, @@ -544,7 +544,7 @@ def test_verify_wraps_unrecognized_critical_header(self) -> None: canonical = ( __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") ) - header = {"alg": "EdDSA", "kid": "k", "typ": "ucp-profile+jws", "crit": ["fakething"], "fakething": "x"} + header = {"alg": "EdDSA", "kid": "k", "typ": "agentscore-profile+jws", "crit": ["fakething"], "fakething": "x"} header_b64 = ( base64.urlsafe_b64encode(__import__("json").dumps(header, separators=(",", ":")).encode()) .rstrip(b"=") @@ -578,7 +578,7 @@ def test_verify_crit_with_missing_kid_emits_unrecognized_critical_header(self) - header = { "alg": "EdDSA", "kid": "nonexistent", - "typ": "ucp-profile+jws", + "typ": "agentscore-profile+jws", "crit": ["fakething"], "fakething": "x", } @@ -610,7 +610,7 @@ def _hand_craft_jws_with_crit(self, key: GeneratedUCPKey, profile: dict, crit_va canonical = ( __import__("json").dumps(profile, sort_keys=True, ensure_ascii=False, separators=(",", ":")).encode("utf-8") ) - header = {"alg": "EdDSA", "kid": "real", "typ": "ucp-profile+jws", "crit": crit_value} + header = {"alg": "EdDSA", "kid": "real", "typ": "agentscore-profile+jws", "crit": crit_value} header_b64 = ( base64.urlsafe_b64encode(__import__("json").dumps(header, separators=(",", ":")).encode()) .rstrip(b"=") diff --git a/uv.lock b/uv.lock index 1b2e0d6..7d5c58e 100644 --- a/uv.lock +++ b/uv.lock @@ -10,7 +10,7 @@ resolution-markers = [ [[package]] name = "agentscore-commerce" -version = "1.3.7" +version = "1.4.0" source = { editable = "." } dependencies = [ { name = "agentscore-py" }, From 09e0f71d02a1557db8d3ffd338d75951960ff6c2 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 10:34:09 -0700 Subject: [PATCH 28/37] 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 build_ucp_profile 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. Also bump the README stability section to reflect the current 1.4.0 version. Co-Authored-By: Claude Opus 4.7 (1M context) --- README.md | 2 +- scripts/regenerate_cross_lang_fixtures.py | 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 +++--- 22 files changed, 72 insertions(+), 72 deletions(-) diff --git a/README.md b/README.md index 88c5dbe..53f7efb 100644 --- a/README.md +++ b/README.md @@ -391,7 +391,7 @@ The [examples/](./examples) directory has 7 runnable single-file FastAPI apps co ## Stability -`agentscore-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. +`agentscore-commerce@1.4.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. ## Documentation diff --git a/scripts/regenerate_cross_lang_fixtures.py b/scripts/regenerate_cross_lang_fixtures.py index 4e4d91f..47db421 100644 --- a/scripts/regenerate_cross_lang_fixtures.py +++ b/scripts/regenerate_cross_lang_fixtures.py @@ -106,7 +106,7 @@ def main() -> None: capabilities=[ UCPCapability( name="sh.agentscore.identity", - schema="https://agentscore.sh/schema/identity/1", + schema="https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", version="1", extras={"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 bd81dfe9eceb01c051351a3208b7426664f2c227 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 10:50:08 -0700 Subject: [PATCH 29/37] fix(identity): unwrap doubly-nested extras in py emoji-keys + int-boundary cross-lang regenerator The Python regenerator passed `extras={"extras": {...}}` for the emoji-keys and int-boundary scenarios, so the test keys ended up nested under a literal "extras" key in the profile body. The Node sibling passes `extras={...}` directly and `buildUCPProfile` flattens via `Object.assign(profile, extras)`, so node fixtures had the keys at profile root interleaved with canonical fields. Net: a regression in either language's top-level key-sort that fires only when non-ASCII keys interleave with canonical fields would have surfaced in node fixtures but not py fixtures, defeating the cross-lang corpus. Also clarify the typed-empty-wins-over-raw behavior in `build_ucp_profile` as Python-only (Node has no raw fallback at all, so the asymmetry covers both directions: raw fallback AND the empty-dict-suppresses-fallback rule). Both flavors of py-emoji-keys.json and py-int-boundary.json now match node-* canonical bodies byte-for-byte (modulo per-run keypair material). Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp.py | 8 +++++-- scripts/regenerate_cross_lang_fixtures.py | 22 ++++++++----------- .../fixtures/cross-lang/node-capability.json | 6 ++--- .../cross-lang/node-data-driven-claims.json | 6 ++--- .../fixtures/cross-lang/node-emoji-keys.json | 6 ++--- .../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 | 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 ++--- .../fixtures/cross-lang/py-int-boundary.json | 18 +++++++-------- tests/fixtures/cross-lang/py-minimal.json | 6 ++--- tests/fixtures/cross-lang/py-multikey.json | 10 ++++----- .../fixtures/cross-lang/py-typed-claims.json | 6 ++--- tests/fixtures/cross-lang/py-unicode.json | 6 ++--- 22 files changed, 92 insertions(+), 96 deletions(-) diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 8501ac8..a6c7e78 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -282,8 +282,12 @@ async def ucp_profile(): # ``data.raw["operator_verification"]`` / ``data.raw["account_verification"]`` # only when the typed field is ``None``; this is a Python-only legacy # escape hatch for callers who hand-construct ``AssessResult(raw=..., typed=None)``. - # Node has no equivalent fallback, so profiles built via the raw-only path - # may not verify cross-language. Production callers should populate typed fields. + # Node has no raw fallback at all (it reads typed fields directly via + # optional chaining), so the typed-empty-wins-over-raw behavior is also + # Python-only: a Python caller who passes ``account_verification={}`` + # explicitly suppresses the raw fallback (empty dict is None-distinguished + # via ``is None``). Production callers populate typed fields consistently, + # so this asymmetry is theoretical for typical usage. typed_op = data.operator_verification operator_verification: dict[str, Any] if typed_op is None: diff --git a/scripts/regenerate_cross_lang_fixtures.py b/scripts/regenerate_cross_lang_fixtures.py index 47db421..5f6e499 100644 --- a/scripts/regenerate_cross_lang_fixtures.py +++ b/scripts/regenerate_cross_lang_fixtures.py @@ -165,12 +165,10 @@ def main() -> None: payment_handlers=[UCPPaymentHandler(name="tempo", config={})], signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], extras={ - "extras": { - "a": 1, - "豈": 2, - "": 3, - "🍷": 4, - }, + "a": 1, + "豈": 2, + "": 3, + "🍷": 4, }, ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) @@ -185,13 +183,11 @@ def main() -> None: payment_handlers=[], signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], extras={ - "extras": { - "max_safe_int": 9007199254740991, - "min_safe_int": -9007199254740991, - "small_int": 42, - "neg_small_int": -42, - "zero": 0, - }, + "max_safe_int": 9007199254740991, + "min_safe_int": -9007199254740991, + "small_int": 42, + "neg_small_int": -42, + "zero": 0, }, ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) 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 644fdc2d65da59dbeba9e89a51f91cf5fdc1425e Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 11:03:26 -0700 Subject: [PATCH 30/37] fix(identity): align UCPPaymentHandler config emission with Node optional convention Two cross-language parity fixes: 1. UCPPaymentHandler.to_dict() now omits the `config` key when empty. Node's `UCPPaymentHandler.config` is a TypeScript optional property and `buildUCPProfile` passes the array verbatim, so a Node caller writing `{ name: 'tempo' }` shipped a wire profile WITHOUT `config`. Python was force-emitting `config: {}` for that same input, producing different canonical bytes between SDKs for the same logical input. Explicit `config={}` is semantically identical to absent and follows the same omit rule. 2. Standalone int-boundary fixture script now uses build_ucp_profile(extras=...) instead of hand-building a profile dict with a top-level `extras` key. The orchestrator was already correct; the standalone version (used for one-off debug regen of a single fixture) was producing canonical bytes with a nested `extras` block while the orchestrator-produced fixture was flat. Mirrors how the data-driven-claims and typed-claims standalone scripts already work. Cross-lang fixture corpus regenerated; new tests cover the omit behavior. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp.py | 17 ++++++++----- scripts/generate_int_boundary_fixture.py | 24 ++++++++++--------- tests/fixtures/cross-lang/py-capability.json | 6 ++--- .../cross-lang/py-data-driven-claims.json | 6 ++--- tests/fixtures/cross-lang/py-emoji-keys.json | 9 ++++--- 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 | 6 ++--- tests/fixtures/cross-lang/py-minimal.json | 6 ++--- tests/fixtures/cross-lang/py-multikey.json | 10 ++++---- .../fixtures/cross-lang/py-typed-claims.json | 6 ++--- tests/fixtures/cross-lang/py-unicode.json | 6 ++--- tests/test_ucp.py | 19 ++++++++------- 13 files changed, 69 insertions(+), 62 deletions(-) diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index a6c7e78..2cd95f3 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -169,12 +169,17 @@ class UCPPaymentHandler: config: dict[str, Any] = field(default_factory=dict) def to_dict(self) -> dict[str, Any]: - # Always emit `config` (even when empty) so a Python-built handler matches - # the Node sibling byte-for-byte: TypeScript serializes - # `{name: 'tempo', config: {}}` with `config` preserved, and the dataclass - # default is `field(default_factory=dict)` so the field is always a dict. - # Cross-language verify drifts otherwise on explicit `config={}` callers. - return {"name": self.name, "config": self.config} + # Match Node SDK: omit `config` when empty (TypeScript optional-property + # convention). Node's `UCPPaymentHandler.config` is `Record?` + # and `buildUCPProfile` passes the array verbatim, so a Node caller writing + # `{ name: 'tempo' }` ships a wire profile WITHOUT the `config` key. Python + # must do the same or the same logical input produces different canonical + # bytes between SDKs. Callers who explicitly pass `config={}` get the same + # treatment because an empty dict is semantically identical to "absent". + out: dict[str, Any] = {"name": self.name} + if self.config: + out["config"] = self.config + return out @dataclass diff --git a/scripts/generate_int_boundary_fixture.py b/scripts/generate_int_boundary_fixture.py index 8fe8dd8..9ed1c86 100644 --- a/scripts/generate_int_boundary_fixture.py +++ b/scripts/generate_int_boundary_fixture.py @@ -12,6 +12,11 @@ import json from pathlib import Path +from agentscore_commerce.identity import ( + UCPService, + UCPSigningKey, + build_ucp_profile, +) from agentscore_commerce.identity.ucp_jwks import ( build_jwks_response, generate_ucp_signing_key, @@ -26,24 +31,21 @@ def main() -> None: key = generate_ucp_signing_key(kid=KID) - 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": [key.public_jwk], - "extras": { + profile = build_ucp_profile( + name="Int Boundary Merchant", + services=[UCPService(type="rest", url="https://i.example.com")], + payment_handlers=[], + signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + extras={ "max_safe_int": 9007199254740991, "min_safe_int": -9007199254740991, "small_int": 42, "neg_small_int": -42, "zero": 0, }, - } + ) - signed = sign_ucp_profile(profile, signing_key=key.private_key, kid=KID) + signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=KID) fixture = { "profile": signed, diff --git a/tests/fixtures/cross-lang/py-capability.json b/tests/fixtures/cross-lang/py-capability.json index b1c3c6c..25dacc2 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": "TMBp_r4E06Pdu0h-53QW7ncMNSYwPXLem6kMjY-3We8" + "x": "mrV_IopnC9X6Y7B7sVWRa0hauxmLjiGMtmBpcoD9Nrg" } ], "name": "Capability Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwidmVyc2lvbiI6IjEifV0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1jYXBhYmlsaXR5LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IlRNQnBfcjRFMDZQZHUwaC01M1FXN25jTU5TWXdQWExlbTZrTWpZLTNXZTgifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.tFu690kDz0E2Iy45Y0MgpUS3G2ocaBgeWFUwwPQSjsXXroi1T4GFZROrA_6MntaKb07CfjLgWoMh9z8Cl3ecBA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwidmVyc2lvbiI6IjEifV0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1jYXBhYmlsaXR5LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Im1yVl9Jb3BuQzlYNlk3QjdzVldSYTBoYXV4bUxqaUdNdG1CcGNvRDlOcmcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.P38VWqceyAly7lFhP7vYluyI8PK0_9GbPX9AccaODPtxanuloyOaajrV_O_efVyw8PUsFyyo5AkIDWrApTvIDg" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "TMBp_r4E06Pdu0h-53QW7ncMNSYwPXLem6kMjY-3We8", + "x": "mrV_IopnC9X6Y7B7sVWRa0hauxmLjiGMtmBpcoD9Nrg", "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..4d1dab0 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": "DAaVG_-gxUtcNYUOizP4YJNRwhHhnDWn-stsD7jPO4w" + "x": "NwAu8PpEQhrEI9Nqdm8GUCMVHpReceNUlBHxZozOf3g" } ], "name": "Data Driven Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IkRBYVZHXy1neFV0Y05ZVU9pelA0WUpOUndoSGhuRFduLXN0c0Q3alBPNHcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.Bo9sH1eHbuXfl6XMp43smJDrGd6KFFoEMejcmgAYTScOiBzgRk9bs7s7YgNSjM4QXnZ-2YXtrI58d1n7tu-4BA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ik53QXU4UHBFUWhyRUk5TnFkbThHVUNNVkhwUmVjZU5VbEJIeFpvek9mM2cifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.zo-SMwEWYEGiqerQZJ9m8eAZpdzN-hFfSLAhx8R8JTlCArnfaxdmH6XjLj_glztXUhy4LHs740ikLAhNDOEyDw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "DAaVG_-gxUtcNYUOizP4YJNRwhHhnDWn-stsD7jPO4w", + "x": "NwAu8PpEQhrEI9Nqdm8GUCMVHpReceNUlBHxZozOf3g", "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..044cb9b 100644 --- a/tests/fixtures/cross-lang/py-emoji-keys.json +++ b/tests/fixtures/cross-lang/py-emoji-keys.json @@ -11,8 +11,7 @@ "capabilities": [], "payment_handlers": [ { - "name": "tempo", - "config": {} + "name": "tempo" } ], "signing_keys": [ @@ -22,7 +21,7 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "rI2JPvoFJRRLd3EbGgoiKut82R3us1TTAIpwhp97BSY" + "x": "kf2p5k_gqFL8d38h9gdlAtdvPnYIPITUcLLjzGrwgjo" } ], "name": "Emoji Keys Merchant", @@ -30,13 +29,13 @@ "豈": 2, "": 3, "🍷": 4, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJhIjoxLCJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7ImNvbmZpZyI6e30sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InJJMkpQdm9GSlJSTGQzRWJHZ29pS3V0ODJSM3VzMVRUQUlwd2hwOTdCU1kifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTciLCLosYgiOjIsIu6AgCI6Mywi8J-NtyI6NH0.1zOHof4cekmU2iphy-mp5DQHS66klN45KOrJ87bKsQYnO6_o2cL6iCEH6GOuQ9qBFTBUfzOWapK1hCAIX3vOAQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJhIjoxLCJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7Im5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImtmMnA1a19ncUZMOGQzOGg5Z2RsQXRkdlBuWUlQSVRVY0xManpHcndnam8ifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTciLCLosYgiOjIsIu6AgCI6Mywi8J-NtyI6NH0.-Gzvmm1BCE8IvcvM6D9Th7wlTl0bHn3hfdEwu9KaHRLdVjdmlWfFLkTnls6r1-tQQK_NGbNYDs7YMmE_IEHiCQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "rI2JPvoFJRRLd3EbGgoiKut82R3us1TTAIpwhp97BSY", + "x": "kf2p5k_gqFL8d38h9gdlAtdvPnYIPITUcLLjzGrwgjo", "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..d54829d 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": "_2HJ2FjbKQFI-uVpxIY8ZIS3htpNb-92IGfIY8kpQhk", - "y": "0KlKsjssFxnAPoozOr2CvYXbuoXih_IzMv-wHQCsBXA" + "x": "2Ghew5w2FeQeIYwRUBpI3sWQGhzI1SEBiqMhQhhuNZo", + "y": "6LmrMRZkHKGADmavYFDDSYn2PJID4vY-D6XNXwQPE3E" } ], "name": "ES256 Merchant", - "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiXzJISjJGamJLUUZJLXVWcHhJWThaSVMzaHRwTmItOTJJR2ZJWThrcFFoayIsInkiOiIwS2xLc2pzc0Z4bkFQb296T3IyQ3ZZWGJ1b1hpaF9Jek12LXdIUUNzQlhBIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.MmqXHAzDEAPjH6wmOvwDq-yyq-0TCyMXVDwZlgryCN0O2E1sDS4jVeOb69F05uWdiPrLBP-HpJGTxruvHJre-g" + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiMkdoZXc1dzJGZVFlSVl3UlVCcEkzc1dRR2h6STFTRUJpcU1oUWhodU5abyIsInkiOiI2TG1yTVJaa0hLR0FEbWF2WUZERFNZbjJQSklENHZZLUQ2WE5Yd1FQRTNFIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.2lhHbFsTMxpK4Vrm8zE3wLPQna6syr7hiI0hSV7C-_gtGcf4UruXjeHaEq58jP5D1bdh-ftyFkZQwx0naBIASw" }, "jwks": { "keys": [ { "crv": "P-256", - "x": "_2HJ2FjbKQFI-uVpxIY8ZIS3htpNb-92IGfIY8kpQhk", - "y": "0KlKsjssFxnAPoozOr2CvYXbuoXih_IzMv-wHQCsBXA", + "x": "2Ghew5w2FeQeIYwRUBpI3sWQGhzI1SEBiqMhQhhuNZo", + "y": "6LmrMRZkHKGADmavYFDDSYn2PJID4vY-D6XNXwQPE3E", "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..9733c48 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": "rSdbpACnhv_GVb7R01lDjmO7kUUvZR6GKBYR0AhW4go" + "x": "hvngLt3fgbEzbSmgFeeUpV1t5M-SU6T0M5bicVxPDJU" } ], "name": "Extras Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1leHRyYXMtaW50LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6InJTZGJwQUNuaHZfR1ZiN1IwMWxEam1PN2tVVXZaUjZHS0JZUjBBaFc0Z28ifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.SW6CJuYzFh5PJ_AQJ89iUINqhW7O1kZvDQo1zuhqtjtU-Gj48XI2pykZph04lBcBS8r4mVEvNrUzVxVi44hXCw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1leHRyYXMtaW50LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Imh2bmdMdDNmZ2JFemJTbWdGZWVVcFYxdDVNLVNVNlQwTTViaWNWeFBESlUifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.5rf3kusXgoEIqGzovksKlA9nYgszetmSefSRRycOLcRELqhVr88Di37CivlGravHNtXEXS_vheCEtb2uWXobAg" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "rSdbpACnhv_GVb7R01lDjmO7kUUvZR6GKBYR0AhW4go", + "x": "hvngLt3fgbEzbSmgFeeUpV1t5M-SU6T0M5bicVxPDJU", "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..b18ae16 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": "2hWtYbSpkVrFTzo_rccrGWAYf_jrreq8wB1D_z_IZOc" + "x": "3xm2hM4-PRbWmQhJ8fDZGIwBoTf1aVXJp11uSgbvDbs" } ], "name": "Int Boundary Merchant", @@ -26,13 +26,13 @@ "small_int": 42, "neg_small_int": -42, "zero": 0, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktaW50LWJvdW5kYXJ5LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjJoV3RZYlNwa1ZyRlR6b19yY2NyR1dBWWZfanJyZXE4d0IxRF96X0laT2MifV0sInNtYWxsX2ludCI6NDIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTciLCJ6ZXJvIjowfQ.1qtHiT04A_A0asQx3jJWeeNEfY9lQ6haBf6JDMoaOz3NmWLg70Yvad0wStR8wQcaDDqPgi7-mmMZMnu8XxvyCQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktaW50LWJvdW5kYXJ5LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjN4bTJoTTQtUFJiV21RaEo4ZkRaR0l3Qm9UZjFhVlhKcDExdVNnYnZEYnMifV0sInNtYWxsX2ludCI6NDIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTciLCJ6ZXJvIjowfQ.853XGWyMf4YL7K0sGA_j0BYCjrX8cdHvzyHX5GSyEEYHL4ZMtSENQR3lw5LszLo2HwU6J93MNKA6d4qx0v1aAQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "2hWtYbSpkVrFTzo_rccrGWAYf_jrreq8wB1D_z_IZOc", + "x": "3xm2hM4-PRbWmQhJ8fDZGIwBoTf1aVXJp11uSgbvDbs", "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..e6d886f 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": "Jal9VgyjKMP3MguusxxOZJDOb6U7nToLMa7C3hCqu2o" + "x": "_oUdVFWSZkqVC-uORY2G34I0xuVXJ78ywa3qYywtWc8" } ], "name": "Minimal Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJKYWw5Vmd5aktNUDNNZ3V1c3h4T1pKRE9iNlU3blRvTE1hN0MzaENxdTJvIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.isXdp4CcyRr9lh9yHQwjAH-MDgj6ZqUfJjLr3rj6KTBaE_nRIjR_HO4hM44uMnh8RUYm5-JCNVh8m1-lsLGxDQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJfb1VkVkZXU1prcVZDLXVPUlkyRzM0STB4dVZYSjc4eXdhM3FZeXd0V2M4In1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.8hLu64spf73Z1aDO95MEqkpsZ2uFP0rHzIWFsOkZkrNq81xcAEcwGYTU1MCtoh7DCfdsb6jXrhvmrFxdhGxPAw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "Jal9VgyjKMP3MguusxxOZJDOb6U7nToLMa7C3hCqu2o", + "x": "_oUdVFWSZkqVC-uORY2G34I0xuVXJ78ywa3qYywtWc8", "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..04471e2 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": "Jk-XKLK-B6PQDrH5muAusj5s64a_jYGScTY7sR5zwTU" + "x": "x5_EjdNmcCe6a9GlHrd9QHWS2bRkT_MAAVM5ZJIKokc" }, { "kid": "py-multikey-new", @@ -32,17 +32,17 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "0k8GV3ctomf1kxJIdg8cIpFDrFdWBE6fNW1kRp5j_ew" + "x": "1bxXK_gPLaOEs4PHCFxdW1vsAWjsuwh1ys94A1st7lI" } ], "name": "Multi-Key Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1vbGQiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiSmstWEtMSy1CNlBRRHJINW11QXVzajVzNjRhX2pZR1NjVFk3c1I1endUVSJ9LHsiYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjBrOEdWM2N0b21mMWt4SklkZzhjSXBGRHJGZFdCRTZmTlcxa1JwNWpfZXcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.5sp4yAhsPgmO6F4CvLSADiJ78rk5_KO83r1NkJYKg2bB3flvz1RdkwpodiH66wKOu8XOEDx2Xmr9_RWUb2W4DA" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1vbGQiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoieDVfRWpkTm1jQ2U2YTlHbEhyZDlRSFdTMmJSa1RfTUFBVk01WkpJS29rYyJ9LHsiYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjFieFhLX2dQTGFPRXM0UEhDRnhkVzF2c0FXanN1d2gxeXM5NEExc3Q3bEkifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.gfiKKlcD1dWp6q-Cg20cbXEyqXsHhSFemxkzwEhM-K-xuMoatp5BX_ZCMePxJyo78nFq6IGHTCAnv4wUk5lEDg" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "Jk-XKLK-B6PQDrH5muAusj5s64a_jYGScTY7sR5zwTU", + "x": "x5_EjdNmcCe6a9GlHrd9QHWS2bRkT_MAAVM5ZJIKokc", "kid": "py-multikey-old", "alg": "EdDSA", "use": "sig", @@ -50,7 +50,7 @@ }, { "crv": "Ed25519", - "x": "0k8GV3ctomf1kxJIdg8cIpFDrFdWBE6fNW1kRp5j_ew", + "x": "1bxXK_gPLaOEs4PHCFxdW1vsAWjsuwh1ys94A1st7lI", "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..4da4373 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": "wNeB1hL1l7cml2x2miyjUChAxvveRYkuuMug1XJkq64" + "x": "NXGO-eKr-ctG46sGb2btycvvmFyjAaNRLGQT2a5hJ0E" } ], "name": "Typed Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IndOZUIxaEwxbDdjbWwyeDJtaXlqVUNoQXh2dmVSWWt1dU11ZzFYSmtxNjQifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.RJjoaDeclNkgVS18FQ4zS7vpoyXV3p0tO1J2YCnrqxe6qM6XQtuCyl90zJQXXFvdz41EEC9et7ZquSUviY2BDQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ik5YR08tZUtyLWN0RzQ2c0diMmJ0eWN2dm1GeWpBYU5STEdRVDJhNWhKMEUifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.UcpNbaK9FXLzCC-zHCeZjV9HWYoFymFBqApZeliLur3sSYeW3U6JoNJlzTEu-iaZT_TOC5NCbIiVQJ02V44TCg" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "wNeB1hL1l7cml2x2miyjUChAxvveRYkuuMug1XJkq64", + "x": "NXGO-eKr-ctG46sGb2btycvvmFyjAaNRLGQT2a5hJ0E", "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..fe388d0 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": "zYxsKbVzECrjQUcCCoXQRJ4_PPA5512kajrQHLUu8nE" + "x": "sMS1O_Zfgj0a9_pByNKOC7v9K8wvUejvwvnGXBg5sP4" } ], "name": "Café 日本 🍷 Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4Ijoiell4c0tiVnpFQ3JqUVVjQ0NvWFFSSjRfUFBBNTUxMmthanJRSExVdThuRSJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.q50JBjCcGT3wY0CdcAatQfVlo3K6o_SnEm9UQUPYdSApQwBT8pLofmATVdMoX0NZlr6TbIMaU29EEVl8XjzuBQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4Ijoic01TMU9fWmZnajBhOV9wQnlOS09DN3Y5Szh3dlVlanZ3dm5HWEJnNXNQNCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.cUCqReLQvenkFtKE1xDaMljqLjXubZEO3a4lCbEKtPiQHcP_7sIqRZMvUx3mgdwQY0ph_6ui0ZWgKb3EwnvPCQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "zYxsKbVzECrjQUcCCoXQRJ4_PPA5512kajrQHLUu8nE", + "x": "sMS1O_Zfgj0a9_pByNKOC7v9K8wvUejvwvnGXBg5sP4", "kid": "py-unicode-EdDSA", "alg": "EdDSA", "use": "sig", diff --git a/tests/test_ucp.py b/tests/test_ucp.py index 3aa0f02..e36b775 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -340,19 +340,20 @@ def test_ucp_signing_key_extras_non_reserved_pass_through() -> None: assert out == {"kid": "me", "kty": "EC", "alg": "ES256", "crv": "P-256", "x": "abc", "y": "def"} -# UCPPaymentHandler.to_dict always emits `config`. The Node sibling serializes -# `{name: 'tempo', config: {}}` with `config` preserved (TypeScript optional -# field initialized to a new object). Cross-language byte-parity requires the -# Python emitter to do the same — even when the dataclass default -# `field(default_factory=dict)` left config empty. +# UCPPaymentHandler.to_dict omits `config` when empty. Node's +# `UCPPaymentHandler.config` is optional (`Record?`), so a Node +# caller writing `{name: 'tempo'}` ships a wire profile WITHOUT the `config` key. +# Python must do the same or the same logical input produces different canonical +# bytes between SDKs. Explicit `config={}` is semantically identical to absent +# and follows the same omit rule. -def test_ucp_payment_handler_to_dict_preserves_empty_config() -> None: - assert UCPPaymentHandler(name="tempo").to_dict() == {"name": "tempo", "config": {}} +def test_ucp_payment_handler_to_dict_omits_default_empty_config() -> None: + assert UCPPaymentHandler(name="tempo").to_dict() == {"name": "tempo"} -def test_ucp_payment_handler_to_dict_preserves_explicit_empty_config() -> None: - assert UCPPaymentHandler(name="tempo", config={}).to_dict() == {"name": "tempo", "config": {}} +def test_ucp_payment_handler_to_dict_omits_explicit_empty_config() -> None: + assert UCPPaymentHandler(name="tempo", config={}).to_dict() == {"name": "tempo"} def test_ucp_payment_handler_to_dict_preserves_populated_config() -> None: From d487d61a6e054c9c5cb87f56e8939d4b3a4ff724 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 11:17:24 -0700 Subject: [PATCH 31/37] chore(scripts): remove standalone fixture generators shadowed by orchestrator The cross-lang fixture orchestrator (regenerate_cross_lang_fixtures.py) already covers int-boundary, data-driven-claims, and typed-claims; the per-scenario one-shots duplicate the work with subtly different json.dumps flags (no ensure_ascii=False), which would silently drift the on-disk fixture if a future unicode field landed at the relevant levels. Drop them so the orchestrator is the single source of truth. Co-Authored-By: Claude Opus 4.7 (1M context) --- .../generate_data_driven_claims_fixture.py | 80 ------------------ scripts/generate_int_boundary_fixture.py | 63 -------------- scripts/generate_typed_claims_fixture.py | 82 ------------------- 3 files changed, 225 deletions(-) delete mode 100644 scripts/generate_data_driven_claims_fixture.py delete mode 100644 scripts/generate_int_boundary_fixture.py delete mode 100644 scripts/generate_typed_claims_fixture.py diff --git a/scripts/generate_data_driven_claims_fixture.py b/scripts/generate_data_driven_claims_fixture.py deleted file mode 100644 index 3122928..0000000 --- a/scripts/generate_data_driven_claims_fixture.py +++ /dev/null @@ -1,80 +0,0 @@ -"""One-shot generator for the data-driven-claims cross-lang fixture (Python side). - -Writes ``tests/fixtures/cross-lang/py-data-driven-claims.json``. Unlike the -other cross-lang fixtures (which hand-craft the ``agentscore-identity`` -capability), this one EXERCISES ``build_ucp_profile``'s data path: it -constructs a synthetic ``AssessResult`` with the API-shape "missing" sentinels -(empty string for kyc_level, None 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. -""" - -from __future__ import annotations - -import json -from pathlib import Path - -from agentscore_commerce.identity import ( - AssessResult, - UCPService, - UCPSigningKey, - build_ucp_profile, -) -from agentscore_commerce.identity.ucp_jwks import ( - build_jwks_response, - generate_ucp_signing_key, - sign_ucp_profile, -) - -OUT = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "cross-lang" / "py-data-driven-claims.json" - -KID = "py-data-driven-claims-EdDSA" - - -def main() -> None: - key = generate_ucp_signing_key(kid=KID) - - result = AssessResult( - allow=True, - resolved_operator="op_data_driven", - verify_url="https://agentscore.sh/verify/op_data_driven", - raw={ - "account_verification": { - # Empty string is the API's "set but unknown" shape for some - # columns; None 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": None, - "jurisdiction": None, - "verified_at": None, - }, - }, - ) - - profile = build_ucp_profile( - name="Data Driven Claims Merchant", - services=[UCPService(type="rest", url="https://d.example.com")], - payment_handlers=[], - signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], - data=result, - ) - - signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=KID) - - fixture = { - "profile": signed, - "jwks": build_jwks_response([key.public_jwk]), - "alg": "EdDSA", - "kid": KID, - "generator": "python", - } - - OUT.write_text(json.dumps(fixture, indent=2) + "\n") - print(f"wrote {OUT}") - - -if __name__ == "__main__": - main() diff --git a/scripts/generate_int_boundary_fixture.py b/scripts/generate_int_boundary_fixture.py deleted file mode 100644 index 9ed1c86..0000000 --- a/scripts/generate_int_boundary_fixture.py +++ /dev/null @@ -1,63 +0,0 @@ -"""One-shot generator for the int-boundary cross-lang fixture (Python side). - -Writes ``tests/fixtures/cross-lang/py-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. -""" - -from __future__ import annotations - -import json -from pathlib import Path - -from agentscore_commerce.identity import ( - UCPService, - UCPSigningKey, - build_ucp_profile, -) -from agentscore_commerce.identity.ucp_jwks import ( - build_jwks_response, - generate_ucp_signing_key, - sign_ucp_profile, -) - -OUT = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "cross-lang" / "py-int-boundary.json" - -KID = "py-int-boundary-EdDSA" - - -def main() -> None: - key = generate_ucp_signing_key(kid=KID) - - profile = build_ucp_profile( - name="Int Boundary Merchant", - services=[UCPService(type="rest", url="https://i.example.com")], - payment_handlers=[], - signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], - extras={ - "max_safe_int": 9007199254740991, - "min_safe_int": -9007199254740991, - "small_int": 42, - "neg_small_int": -42, - "zero": 0, - }, - ) - - signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=KID) - - fixture = { - "profile": signed, - "jwks": build_jwks_response([key.public_jwk]), - "alg": "EdDSA", - "kid": KID, - "generator": "python", - } - - OUT.write_text(json.dumps(fixture, indent=2) + "\n") - print(f"wrote {OUT}") - - -if __name__ == "__main__": - main() diff --git a/scripts/generate_typed_claims_fixture.py b/scripts/generate_typed_claims_fixture.py deleted file mode 100644 index a446627..0000000 --- a/scripts/generate_typed_claims_fixture.py +++ /dev/null @@ -1,82 +0,0 @@ -"""One-shot generator for the typed-claims cross-lang fixture (Python side). - -Writes ``tests/fixtures/cross-lang/py-typed-claims.json``. Sibling to -``generate_data_driven_claims_fixture.py`` but exercises the **typed** -``AssessResult.account_verification`` / ``AssessResult.operator_verification`` -read path (with ``raw=None``) instead of the raw-dict fallback. This catches -drift in typed-field-only callers — production code populates both, but a -hand-constructed AssessResult with only typed fields must produce a profile -that the Node sibling verifies byte-for-byte, since Node's -``buildUCPProfile`` reads the typed fields directly without ever consulting -``raw``. -""" - -from __future__ import annotations - -import json -from pathlib import Path - -from agentscore_commerce.identity import ( - AssessResult, - OperatorVerification, - UCPService, - UCPSigningKey, - build_ucp_profile, -) -from agentscore_commerce.identity.ucp_jwks import ( - build_jwks_response, - generate_ucp_signing_key, - sign_ucp_profile, -) - -OUT = Path(__file__).resolve().parent.parent / "tests" / "fixtures" / "cross-lang" / "py-typed-claims.json" - -KID = "py-typed-claims-EdDSA" - - -def main() -> None: - key = generate_ucp_signing_key(kid=KID) - - result = AssessResult( - allow=True, - resolved_operator="op_typed_claims", - verify_url="https://agentscore.sh/verify/op_typed_claims", - operator_verification=OperatorVerification( - 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", - }, - raw=None, - ) - - profile = build_ucp_profile( - name="Typed Claims Merchant", - services=[UCPService(type="rest", url="https://t.example.com")], - payment_handlers=[], - signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], - data=result, - ) - - signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=KID) - - fixture = { - "profile": signed, - "jwks": build_jwks_response([key.public_jwk]), - "alg": "EdDSA", - "kid": KID, - "generator": "python", - } - - OUT.write_text(json.dumps(fixture, indent=2) + "\n") - print(f"wrote {OUT}") - - -if __name__ == "__main__": - main() From c516f430a06aaccfa0d374a5184175c0e3025afc Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sat, 9 May 2026 11:44:52 -0700 Subject: [PATCH 32/37] fix(identity): coerce both-empty verified_at to None for cross-lang parity Python's chained `or` returns the last falsy value, so `account_verification.get("verified_at") or operator_verification.get("verified_at")` returned `""` when both source values were empty strings. The Node sibling returned `null` via `a || b || null`. Cross-language byte-parity divergence on a corner the doc comment explicitly calls out (API may emit either null or "" for un-set fields). Add a trailing `or None` to match Node, plus a regression test covering the both-empty corner. Also add a per-element shape parity note near the dataclasses describing how Node's TypeScript-interface + flat-extras approach reaches the same net contract as Python's dataclass-with-extras- slot + runtime collision guard. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp.py | 13 ++++++++++++- tests/test_ucp.py | 22 ++++++++++++++++++++-- 2 files changed, 32 insertions(+), 3 deletions(-) diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 2cd95f3..2a6bfc5 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -39,6 +39,17 @@ _AGENTSCORE_CAPABILITY_VERSION = "1" +# UCP per-element shape note (applies to UCPSigningKey, UCPService, UCPCapability): +# The Node sibling models these as TypeScript interfaces that accept canonical +# fields plus arbitrary vendor extras flat via `[k: string]: unknown`, with no +# runtime collision guard; required-field types prevent collisions at typed +# call sites at compile time, and JSON-deserialized inputs simply overwrite. +# Python models them as dataclasses with an explicit `extras: dict` slot, and +# each `to_dict()` rejects extras keys that collide with reserved canonical +# names at runtime. Net contract is the same; cross-language fixtures don't +# exercise the divergent corner. + + @dataclass class UCPSigningKey: """JWK entry for the profile's ``signing_keys`` array. @@ -330,7 +341,7 @@ async def ucp_profile(): "sanctions_clear": account_verification.get("sanctions_clear") is True, "age_bracket": account_verification.get("age_bracket") or "unknown", "jurisdiction": account_verification.get("jurisdiction") or "", - "verified_at": account_verification.get("verified_at") or operator_verification.get("verified_at"), + "verified_at": account_verification.get("verified_at") or operator_verification.get("verified_at") or None, "verify_url": data.verify_url, "issuer": "https://agentscore.sh", } diff --git a/tests/test_ucp.py b/tests/test_ucp.py index e36b775..6f8e347 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -145,11 +145,14 @@ def test_extras_reserved_collision_rejected(key: str) -> None: # shape so a profile signed in one language verifies in the other. -def _claims_of(account_verification: dict) -> dict: +def _claims_of(account_verification: dict, operator_verification: dict | None = None) -> dict: + raw: dict = {"account_verification": account_verification} + if operator_verification is not None: + raw["operator_verification"] = operator_verification result = AssessResult( allow=True, resolved_operator="op_abc", - raw={"account_verification": account_verification}, + raw=raw, ) profile = build_ucp_profile(**_base_kwargs(), data=result) d = profile.to_dict() @@ -185,6 +188,21 @@ def test_coerces_empty_string_verified_at_to_none() -> None: assert _claims_of({"verified_at": ""})["verified_at"] is None +def test_both_empty_string_verified_at_normalizes_to_none() -> None: + """Both account_verification + operator_verification with verified_at='' + must normalize to None for cross-language byte parity with Node SDK. + Without the trailing ``or None``, Python's chained ``or`` returns the last + falsy value (``""``); Node's ``a || b || null`` returns ``null``. + """ + assert ( + _claims_of( + {"verified_at": ""}, + operator_verification={"verified_at": ""}, + )["verified_at"] + is None + ) + + # Typed-field fallback: production callers populate `data.raw`, but a # hand-constructed AssessResult (no raw) should still surface the verification # block via the typed `AssessResult.operator_verification` / From fa0bc9a6d56c37602b98715523dc00932878337d Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 06:10:17 -0700 Subject: [PATCH 33/37] 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 Mirrors node-commerce's spec-compliance refactor. The pre-refactor `build_ucp_profile` emitted a flat top-level body with services / capabilities / payment_handlers as LISTS, which doesn't match the UCP §6 spec. Verified against the live Pura Vida reference profile at puravidabracelets.com/.well-known/ucp. Output shape change: - Top-level: {"ucp": {"version", "services", "capabilities", "payment_handlers", "name?", "supported_versions?"}, "signing_keys": [...], "signature?": "..."} - services: dict[str, list[UCPServiceBinding]] keyed by service name - capabilities: dict[str, list[UCPCapabilityBinding]] keyed by capability name - payment_handlers: dict[str, list[UCPPaymentHandlerBinding]] keyed by handler reverse-DNS name - Each binding carries spec-required fields Class renames: - UCPService → UCPServiceBinding - UCPCapability → UCPCapabilityBinding - UCPPaymentHandler → UCPPaymentHandlerBinding - New: UCPProfileBody (inner `ucp` envelope dataclass) 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` kwarg, 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 @agent-score/commerce node sibling. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/__init__.py | 14 +- agentscore_commerce/identity/ucp.py | 383 ++++++++++++++--------- tests/test_ucp.py | 340 ++++++++++++-------- 3 files changed, 439 insertions(+), 298 deletions(-) diff --git a/agentscore_commerce/identity/__init__.py b/agentscore_commerce/identity/__init__.py index 57643de..27dbcd1 100644 --- a/agentscore_commerce/identity/__init__.py +++ b/agentscore_commerce/identity/__init__.py @@ -47,10 +47,11 @@ ) from agentscore_commerce.identity.ucp import ( AGENTSCORE_UCP_CAPABILITY, - UCPCapability, - UCPPaymentHandler, + UCPCapabilityBinding, + UCPPaymentHandlerBinding, UCPProfile, - UCPService, + UCPProfileBody, + UCPServiceBinding, UCPSigningKey, build_ucp_profile, ) @@ -109,10 +110,11 @@ def _load_asgi_middleware() -> tuple[Any, Any]: "OperatorVerification", "PolicyBlock", "ScoreDetail", - "UCPCapability", - "UCPPaymentHandler", + "UCPCapabilityBinding", + "UCPPaymentHandlerBinding", "UCPProfile", - "UCPService", + "UCPProfileBody", + "UCPServiceBinding", "UCPSigningKey", "UCPVerificationError", "VerifyWalletSignerMatchOptions", diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 2a6bfc5..10fc2c8 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -1,60 +1,48 @@ """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 name. Verified against the live production reference at +``https://puravidabracelets.com/.well-known/ucp`` (Shopify's UCP integration). -Why publish: UCP is the Google-led cross-vendor standard (announced Jan 2026 with -broad ecosystem support). 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). -Spec reference: https://ucp.dev/ +Pass the unsigned profile through :func:`sign_ucp_profile` 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/ """ from __future__ import annotations from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, cast +from typing import TYPE_CHECKING, Any, Literal, cast if TYPE_CHECKING: from agentscore_commerce.identity.types import AssessResult _DEFAULT_VERSION = "2026-04-17" -_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. + +# Reverse-DNS namespacing per UCP convention. 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. AGENTSCORE_UCP_CAPABILITY = "sh.agentscore.identity" """Capability name AgentScore registers in the UCP profile. Consumers filter on this to find verified-buyer claims attached to the profile.""" _AGENTSCORE_CAPABILITY_VERSION = "1" - - -# UCP per-element shape note (applies to UCPSigningKey, UCPService, UCPCapability): -# The Node sibling models these as TypeScript interfaces that accept canonical -# fields plus arbitrary vendor extras flat via `[k: string]: unknown`, with no -# runtime collision guard; required-field types prevent collisions at typed -# call sites at compile time, and JSON-deserialized inputs simply overwrite. -# Python models them as dataclasses with an explicit `extras: dict` slot, and -# each `to_dict()` rejects extras keys that collide with reserved canonical -# names at runtime. Net contract is the same; cross-language fixtures don't -# exercise the divergent corner. +_AGENTSCORE_DEFAULT_SPEC_URL = "https://agentscore.sh/specification/identity" +_AGENTSCORE_DEFAULT_SCHEMA_URL = "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json" @dataclass class UCPSigningKey: """JWK entry for the profile's ``signing_keys`` array. - Pass through the public key material verbatim — UCP requires JWKS-format keys. + Pass through public key material verbatim; UCP requires JWKS-format keys. """ kid: str @@ -89,9 +77,6 @@ def from_jwk(cls, jwk: dict[str, Any]) -> UCPSigningKey: Routes the JWK's known fields (kid/kty/alg/use/crv) onto the dataclass and captures any other fields (x/y/n/e/etc.) into ``extras``. Use this when publishing the output of :func:`generate_ucp_signing_key` directly. - - Rejects symmetric (``oct``) keys and JWKs missing required fields with a - typed ``ValueError`` rather than a bare ``KeyError``. """ if not isinstance(jwk, dict): msg = f"UCPSigningKey.from_jwk expected a dict; got {type(jwk).__name__}." @@ -110,7 +95,6 @@ def from_jwk(cls, jwk: dict[str, Any]) -> UCPSigningKey: "the trust-mode UCP flow." ) raise ValueError(msg) - known = {"kid", "kty", "alg", "use", "crv"} return cls( kid=jwk["kid"], @@ -123,124 +107,181 @@ def from_jwk(cls, jwk: dict[str, Any]) -> UCPSigningKey: @dataclass -class UCPService: - """Transport binding entry.""" +class UCPServiceBinding: + """Transport binding entry — keyed under a service name (e.g., ``dev.ucp.shopping``).""" - type: str - url: str | None = None - version: str | None = None + version: str + spec: str + transport: Literal["rest", "mcp", "a2a", "embedded"] + endpoint: str | None = None + schema: str | None = None + id: str | None = None + config: dict[str, Any] | None = None extras: dict[str, Any] = field(default_factory=dict) - _RESERVED = frozenset({"type", "url", "version"}) + _RESERVED = frozenset({"version", "spec", "transport", "endpoint", "schema", "id", "config"}) def to_dict(self) -> dict[str, Any]: - out: dict[str, Any] = {"type": self.type} - if self.url is not None: - out["url"] = self.url - if self.version is not None: - out["version"] = self.version + out: dict[str, Any] = { + "version": self.version, + "spec": self.spec, + "transport": self.transport, + } + if self.endpoint is not None: + out["endpoint"] = self.endpoint + if self.schema is not None: + out["schema"] = self.schema + if self.id is not None: + out["id"] = self.id + if self.config: + out["config"] = self.config for k, v in self.extras.items(): if k in self._RESERVED: - msg = f"UCPService.extras key {k!r} collides with a reserved field; rejected." + msg = f"UCPServiceBinding.extras key {k!r} collides with a reserved field; rejected." raise ValueError(msg) out[k] = v return out @dataclass -class UCPCapability: - """Capability entry — name + schema URL + version + claims.""" - - name: str - schema: str | None = None - version: str | None = None +class UCPCapabilityBinding: + """Capability binding entry — keyed under a capability name (e.g., ``dev.ucp.shopping.checkout``).""" + + version: str + spec: str + schema: str + id: str | None = None + config: dict[str, Any] | None = None + extends: str | list[str] | None = None + requires: dict[str, Any] | None = None extras: dict[str, Any] = field(default_factory=dict) - _RESERVED = frozenset({"name", "schema", "version"}) + _RESERVED = frozenset({"version", "spec", "schema", "id", "config", "extends", "requires"}) def to_dict(self) -> dict[str, Any]: - out: dict[str, Any] = {"name": self.name} - if self.schema is not None: - out["schema"] = self.schema - if self.version is not None: - out["version"] = self.version + out: dict[str, Any] = { + "version": self.version, + "spec": self.spec, + "schema": self.schema, + } + if self.id is not None: + out["id"] = self.id + if self.config: + out["config"] = self.config + if self.extends is not None: + out["extends"] = self.extends + if self.requires is not None: + out["requires"] = self.requires for k, v in self.extras.items(): if k in self._RESERVED: - msg = f"UCPCapability.extras key {k!r} collides with a reserved field; rejected." + msg = f"UCPCapabilityBinding.extras key {k!r} collides with a reserved field; rejected." raise ValueError(msg) out[k] = v return out @dataclass -class UCPPaymentHandler: - """Payment handler entry — name + config.""" +class UCPPaymentHandlerBinding: + """Payment handler binding entry — keyed under a handler reverse-DNS name (e.g., ``com.google.pay``).""" + + id: str + version: str + spec: str + schema: str + available_instruments: list[dict[str, Any]] | None = None + config: dict[str, Any] | None = None + extras: dict[str, Any] = field(default_factory=dict) - name: str - config: dict[str, Any] = field(default_factory=dict) + _RESERVED = frozenset({"id", "version", "spec", "schema", "available_instruments", "config"}) def to_dict(self) -> dict[str, Any]: - # Match Node SDK: omit `config` when empty (TypeScript optional-property - # convention). Node's `UCPPaymentHandler.config` is `Record?` - # and `buildUCPProfile` passes the array verbatim, so a Node caller writing - # `{ name: 'tempo' }` ships a wire profile WITHOUT the `config` key. Python - # must do the same or the same logical input produces different canonical - # bytes between SDKs. Callers who explicitly pass `config={}` get the same - # treatment because an empty dict is semantically identical to "absent". - out: dict[str, Any] = {"name": self.name} + out: dict[str, Any] = { + "id": self.id, + "version": self.version, + "spec": self.spec, + "schema": self.schema, + } + if self.available_instruments is not None: + out["available_instruments"] = self.available_instruments if self.config: out["config"] = self.config + for k, v in self.extras.items(): + if k in self._RESERVED: + msg = f"UCPPaymentHandlerBinding.extras key {k!r} collides with a reserved field; rejected." + raise ValueError(msg) + out[k] = v return out @dataclass -class UCPProfile: - """UCP profile body for ``/.well-known/ucp``. - - Use :meth:`to_dict` to serialize. Sign + envelope with your JWKS-backed signing - flow before publishing. - """ +class UCPProfileBody: + """UCP body — nested under the ``ucp`` key of the published profile.""" - services: list[UCPService] = field(default_factory=list) - capabilities: list[UCPCapability] = field(default_factory=list) - payment_handlers: list[UCPPaymentHandler] = field(default_factory=list) - signing_keys: list[UCPSigningKey] = field(default_factory=list) - name: str | None = None version: str = _DEFAULT_VERSION - spec: str = _SPEC_URL + services: dict[str, list[UCPServiceBinding]] = field(default_factory=dict) + capabilities: dict[str, list[UCPCapabilityBinding]] = field(default_factory=dict) + payment_handlers: dict[str, list[UCPPaymentHandlerBinding]] = field(default_factory=dict) + name: str | None = None + supported_versions: dict[str, str] | None = None extras: dict[str, Any] = field(default_factory=dict) - def to_dict(self) -> dict[str, Any]: - out: dict[str, Any] = { - "version": self.version, - "spec": self.spec, - "services": [s.to_dict() for s in self.services], - "capabilities": [c.to_dict() for c in self.capabilities], - "payment_handlers": [h.to_dict() for h in self.payment_handlers], - "signing_keys": [k.to_dict() for k in self.signing_keys], - } - if self.name is not None: - out["name"] = self.name - # Filter `extras` so a caller passing - # ``extras={"signing_keys": [...]}`` can't silently destroy the - # explicit field. ``__proto__`` / ``constructor`` / ``prototype`` - # match the node-commerce reserved set so a Node-signed profile - # carrying those keys is rejected identically by both SDKs. - reserved = { + _RESERVED = frozenset( + { "version", - "spec", + "name", "services", "capabilities", "payment_handlers", - "signing_keys", - "name", - "signature", + "supported_versions", "__proto__", "constructor", "prototype", + }, + ) + + def to_dict(self) -> dict[str, Any]: + out: dict[str, Any] = { + "version": self.version, + "services": {k: [s.to_dict() for s in bindings] for k, bindings in self.services.items()}, + "capabilities": {k: [c.to_dict() for c in bindings] for k, bindings in self.capabilities.items()}, + "payment_handlers": {k: [h.to_dict() for h in bindings] for k, bindings in self.payment_handlers.items()}, } + if self.name is not None: + out["name"] = self.name + if self.supported_versions is not None: + out["supported_versions"] = self.supported_versions for k, v in self.extras.items(): - if k in reserved: + if k in self._RESERVED: + msg = f"UCPProfileBody.extras key {k!r} collides with a reserved `ucp` field; rejected." + raise ValueError(msg) + out[k] = v + return out + + +@dataclass +class UCPProfile: + """UCP profile body for ``/.well-known/ucp``. + + Top-level shape: ``{"ucp": {...}, "signing_keys": [...], "signature?": "..."}``. + Use :meth:`to_dict` to serialize. Pass through :func:`sign_ucp_profile` to attach + the JWS signature. + """ + + ucp: UCPProfileBody = field(default_factory=UCPProfileBody) + signing_keys: list[UCPSigningKey] = field(default_factory=list) + extras: dict[str, Any] = field(default_factory=dict) + + _RESERVED = frozenset( + {"ucp", "signing_keys", "signature", "__proto__", "constructor", "prototype"}, + ) + + def to_dict(self) -> dict[str, Any]: + out: dict[str, Any] = { + "ucp": self.ucp.to_dict(), + "signing_keys": [k.to_dict() for k in self.signing_keys], + } + for k, v in self.extras.items(): + if k in self._RESERVED: msg = f"UCPProfile.extras key {k!r} collides with a reserved profile field; rejected." raise ValueError(msg) out[k] = v @@ -248,29 +289,37 @@ def to_dict(self) -> dict[str, Any]: def build_ucp_profile( - services: list[UCPService], - signing_keys: list[UCPSigningKey], - capabilities: list[UCPCapability] | None = None, - payment_handlers: list[UCPPaymentHandler] | None = None, + services: dict[str, list[UCPServiceBinding]] | None = None, + signing_keys: list[UCPSigningKey] | None = None, + *, + capabilities: dict[str, list[UCPCapabilityBinding]] | None = None, + payment_handlers: dict[str, list[UCPPaymentHandlerBinding]] | None = None, name: str | None = None, version: str = _DEFAULT_VERSION, data: AssessResult | None = None, agentscore_schema_url: str | None = None, + agentscore_spec_url: str | None = None, + supported_versions: dict[str, str] | None = None, + ucp_extras: dict[str, Any] | None = None, extras: dict[str, Any] | None = None, ) -> UCPProfile: """Compose a UCP profile body for ``/.well-known/ucp`` publication. - Merges AgentScore identity claims into ``capabilities`` as an - ``sh.agentscore.identity`` capability when ``data`` carries a resolved operator. - Consumers reading the profile can opt into the AgentScore claims by filtering - on the capability name. + Returns the spec-compliant shape: ``{"ucp": {...}, "signing_keys": [...]}`` + with ``services`` / ``capabilities`` / ``payment_handlers`` as maps keyed by + reverse-DNS name. Pass through :func:`sign_ucp_profile` to attach a JWS + signature for trust-mode verifiers. + + Auto-injects ``sh.agentscore.identity`` as a vendor capability when ``data`` + carries a resolved operator. Verifiers that recognize the AgentScore namespace + can parse the ``claims`` extra; vanilla UCP agents see a normal capability. Example:: from agentscore_commerce.identity.ucp import ( - UCPService, + UCPServiceBinding, UCPSigningKey, - UCPPaymentHandler, + UCPPaymentHandlerBinding, build_ucp_profile, ) @@ -278,32 +327,48 @@ def build_ucp_profile( async def ucp_profile(): result = await client.acheck(identity) return build_ucp_profile( + services={ + "dev.ucp.shopping": [ + UCPServiceBinding( + 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=[UCPSigningKey.from_jwk(public_jwk)], + payment_handlers={ + "sh.agentscore.payment.tempo": [ + 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": TEMPO_ADDR}, + ), + ], + }, name="Example Merchant", - services=[UCPService(type="rest", url="https://agents.example.com")], - payment_handlers=[ - UCPPaymentHandler(name="tempo", config={"recipient": TEMPO_ADDR}), - UCPPaymentHandler(name="stripe", config={"profile_id": STRIPE_PROFILE_ID}), - ], - signing_keys=[ - UCPSigningKey(kid="merchant-2026-04", kty="EC", alg="ES256", crv="P-256", - extras={"x": "...", "y": "..."}), - ], data=result, ).to_dict() """ - base_capabilities = list(capabilities or []) + services = services if services is not None else {} + signing_keys = signing_keys if signing_keys is not None else [] + + # Deep-copy the capabilities map so we can safely mutate (auto-inject the + # AgentScore identity capability) without altering the caller's input. + base_capabilities: dict[str, list[UCPCapabilityBinding]] = { + k: list(bindings) for k, bindings in (capabilities or {}).items() + } if data is not None and data.resolved_operator: - # Read typed AssessResult fields first (the canonical path). Fall back to + # Read typed AssessResult fields first (canonical path). Fall back to # ``data.raw["operator_verification"]`` / ``data.raw["account_verification"]`` - # only when the typed field is ``None``; this is a Python-only legacy - # escape hatch for callers who hand-construct ``AssessResult(raw=..., typed=None)``. - # Node has no raw fallback at all (it reads typed fields directly via - # optional chaining), so the typed-empty-wins-over-raw behavior is also - # Python-only: a Python caller who passes ``account_verification={}`` - # explicitly suppresses the raw fallback (empty dict is None-distinguished - # via ``is None``). Production callers populate typed fields consistently, - # so this asymmetry is theoretical for typical usage. + # only when the typed field is ``None`` (Python-only legacy escape hatch + # for callers who hand-construct ``AssessResult(raw=..., typed=None)``). + # Node has no raw fallback at all. typed_op = data.operator_verification operator_verification: dict[str, Any] if typed_op is None: @@ -313,7 +378,6 @@ async def ucp_profile(): elif isinstance(typed_op, dict): operator_verification = cast("dict[str, Any]", typed_op) else: - # Convert OperatorVerification dataclass to a plain dict. operator_verification = { "level": getattr(typed_op, "level", None), "operator_type": getattr(typed_op, "operator_type", None), @@ -329,12 +393,10 @@ async def ucp_profile(): account_verification = data.account_verification else: account_verification = {} + # `dict.get(k) or DEFAULT` (not `dict.get(k, DEFAULT)`) coerces both a # missing key AND a present-but-falsy (None / "") value to the default, - # matching the node sibling's `||` semantics. 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. + # matching the node sibling's `||` semantics. claims = { "operator_id": data.resolved_operator, "kyc_level": account_verification.get("kyc_level") or operator_verification.get("level") or "none", @@ -345,32 +407,45 @@ async def ucp_profile(): "verify_url": data.verify_url, "issuer": "https://agentscore.sh", } - base_capabilities.append( - UCPCapability( - name=AGENTSCORE_UCP_CAPABILITY, - version=_AGENTSCORE_CAPABILITY_VERSION, - schema=agentscore_schema_url or "https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", - extras={"claims": claims}, - ), + # Multi-parent extension matching Shopify's `dev.shopify.catalog.storefront` + # and UCP-canonical `dev.ucp.shopping.discount` (extends [checkout, cart]). + # `claims` lives in `extras` so it serializes as a vendor field on the binding. + binding = UCPCapabilityBinding( + version=_AGENTSCORE_CAPABILITY_VERSION, + spec=agentscore_spec_url or _AGENTSCORE_DEFAULT_SPEC_URL, + schema=agentscore_schema_url or _AGENTSCORE_DEFAULT_SCHEMA_URL, + extends=["dev.ucp.shopping.checkout", "dev.ucp.shopping.cart"], + extras={"claims": claims}, ) + if AGENTSCORE_UCP_CAPABILITY in base_capabilities: + base_capabilities[AGENTSCORE_UCP_CAPABILITY].append(binding) + else: + base_capabilities[AGENTSCORE_UCP_CAPABILITY] = [binding] - return UCPProfile( + body = UCPProfileBody( + version=version, services=services, capabilities=base_capabilities, - payment_handlers=list(payment_handlers or []), - signing_keys=signing_keys, + payment_handlers=payment_handlers if payment_handlers is not None else {}, name=name, - version=version, + supported_versions=supported_versions, + extras=ucp_extras or {}, + ) + + return UCPProfile( + ucp=body, + signing_keys=signing_keys, extras=extras or {}, ) __all__ = [ "AGENTSCORE_UCP_CAPABILITY", - "UCPCapability", - "UCPPaymentHandler", + "UCPCapabilityBinding", + "UCPPaymentHandlerBinding", "UCPProfile", - "UCPService", + "UCPProfileBody", + "UCPServiceBinding", "UCPSigningKey", "build_ucp_profile", ] diff --git a/tests/test_ucp.py b/tests/test_ucp.py index 6f8e347..95be163 100644 --- a/tests/test_ucp.py +++ b/tests/test_ucp.py @@ -1,4 +1,4 @@ -"""Tests for build_ucp_profile.""" +"""Tests for build_ucp_profile (spec-compliant shape).""" from typing import cast @@ -8,9 +8,10 @@ AGENTSCORE_UCP_CAPABILITY, AssessResult, OperatorVerification, - UCPCapability, - UCPPaymentHandler, - UCPService, + UCPCapabilityBinding, + UCPPaymentHandlerBinding, + UCPProfileBody, + UCPServiceBinding, UCPSigningKey, build_ucp_profile, ) @@ -33,35 +34,53 @@ def _full_result() -> AssessResult: ) +def _sample_service() -> UCPServiceBinding: + return 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", + ) + + def _base_kwargs(): return { - "services": [UCPService(type="rest", url="https://agents.example")], + "services": {"dev.ucp.shopping": [_sample_service()]}, "signing_keys": [ UCPSigningKey(kid="me", kty="EC", alg="ES256", crv="P-256", extras={"x": "x", "y": "y"}), ], } -def test_base_profile_has_required_fields(): +def _agentscore_cap(d: dict) -> dict: + return d["ucp"]["capabilities"][AGENTSCORE_UCP_CAPABILITY][0] + + +def test_emits_spec_envelope_with_ucp_body_and_outer_signing_keys(): profile = build_ucp_profile(**_base_kwargs()) d = profile.to_dict() - assert d["spec"] == "https://ucp.dev/" - assert "version" in d - assert d["services"][0]["url"] == "https://agents.example" + assert "ucp" in d + assert "signing_keys" in d + # No top-level `spec` field per UCP spec — spec lives per-binding. + assert "spec" not in d + assert "version" not in d # version lives under `ucp` + assert d["ucp"]["version"] + assert d["ucp"]["services"]["dev.ucp.shopping"][0]["transport"] == "mcp" + assert d["ucp"]["capabilities"] == {} + assert d["ucp"]["payment_handlers"] == {} assert d["signing_keys"][0]["kid"] == "me" - assert d["capabilities"] == [] - assert d["payment_handlers"] == [] def test_appends_agentscore_capability_when_data_provided(): profile = build_ucp_profile(**_base_kwargs(), data=_full_result()) d = profile.to_dict() - matching = [c for c in d["capabilities"] if c["name"] == AGENTSCORE_UCP_CAPABILITY] - assert len(matching) == 1 - cap = matching[0] + cap = _agentscore_cap(d) assert cap["version"] == "1" - assert cap["name"] == "sh.agentscore.identity" assert "sh-agentscore-identity-v1.json" in cap["schema"] + # Multi-parent extends — matches Shopify's dev.shopify.catalog.storefront pattern + # and UCP-canonical dev.ucp.shopping.discount (extends [checkout, cart]). + assert cap["extends"] == ["dev.ucp.shopping.checkout", "dev.ucp.shopping.cart"] claims = cap["claims"] assert claims["operator_id"] == "op_abc" assert claims["kyc_level"] == "enhanced" @@ -72,39 +91,66 @@ def test_appends_agentscore_capability_when_data_provided(): def test_skips_agentscore_capability_when_no_resolved_operator(): profile = build_ucp_profile(**_base_kwargs(), data=AssessResult(allow=True, resolved_operator=None)) d = profile.to_dict() - assert all(c["name"] != AGENTSCORE_UCP_CAPABILITY for c in d["capabilities"]) + assert AGENTSCORE_UCP_CAPABILITY not in d["ucp"]["capabilities"] def test_preserves_caller_capabilities_and_appends_agentscore(): + checkout_binding = 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", + ) profile = build_ucp_profile( **_base_kwargs(), - capabilities=[UCPCapability(name="checkout", version="2")], + capabilities={"dev.ucp.shopping.checkout": [checkout_binding]}, data=_full_result(), ) d = profile.to_dict() - assert d["capabilities"][0]["name"] == "checkout" - assert d["capabilities"][1]["name"] == AGENTSCORE_UCP_CAPABILITY + assert d["ucp"]["capabilities"]["dev.ucp.shopping.checkout"][0]["version"] == "2026-04-08" + assert _agentscore_cap(d)["version"] == "1" def test_passes_through_name_payment_handlers_extras(): + tempo_handler = 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"}, + ) profile = build_ucp_profile( **_base_kwargs(), name="Example Merchant", - payment_handlers=[ - UCPPaymentHandler(name="tempo", config={"recipient": "0xtempo"}), - UCPPaymentHandler(name="stripe", config={"profile_id": "prof_x"}), - ], - extras={"custom_field": "custom_value"}, + payment_handlers={"sh.agentscore.payment.tempo": [tempo_handler]}, + extras={"custom_top_level": "top_value"}, + ucp_extras={"custom_ucp_field": "ucp_value"}, + ) + d = profile.to_dict() + assert d["ucp"]["name"] == "Example Merchant" + assert d["ucp"]["payment_handlers"]["sh.agentscore.payment.tempo"][0]["id"] == "tempo" + assert d["custom_top_level"] == "top_value" + assert d["ucp"]["custom_ucp_field"] == "ucp_value" + + +def test_payment_handler_omits_config_when_caller_does_not_set_it(): + handler = UCPPaymentHandlerBinding( + id="tempo", + version="2026-04-08", + spec="https://agentscore.sh/specification/payment-handlers/tempo", + schema="https://agentscore.sh/schemas/payment-handlers/tempo.json", + ) + profile = build_ucp_profile( + **_base_kwargs(), + payment_handlers={"sh.agentscore.payment.tempo": [handler]}, ) d = profile.to_dict() - assert d["name"] == "Example Merchant" - assert len(d["payment_handlers"]) == 2 - assert d["custom_field"] == "custom_value" + serialized = d["ucp"]["payment_handlers"]["sh.agentscore.payment.tempo"][0] + assert "config" not in serialized def test_respects_version_override(): profile = build_ucp_profile(**_base_kwargs(), version="2026-12-31") - assert profile.version == "2026-12-31" + assert profile.ucp.version == "2026-12-31" def test_respects_agentscore_schema_url_override(): @@ -113,51 +159,76 @@ def test_respects_agentscore_schema_url_override(): data=_full_result(), agentscore_schema_url="https://custom.example/schema.json", ) - cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) + cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] assert cap.schema == "https://custom.example/schema.json" +def test_respects_agentscore_spec_url_override(): + profile = build_ucp_profile( + **_base_kwargs(), + data=_full_result(), + agentscore_spec_url="https://custom.example/spec", + ) + cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] + assert cap.spec == "https://custom.example/spec" + + +def test_emits_supported_versions_map_when_supplied(): + profile = build_ucp_profile( + **_base_kwargs(), + 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", + }, + ) + d = profile.to_dict() + assert d["ucp"]["supported_versions"]["2026-04-08"].endswith("/2026-04-08") + + +@pytest.mark.parametrize( + "key", + ["ucp", "signing_keys", "signature", "__proto__", "constructor", "prototype"], +) +def test_extras_top_level_reserved_collision_rejected(key: str) -> None: + profile = build_ucp_profile(**_base_kwargs(), extras={key: "attacker"}) + with pytest.raises(ValueError, match="collides with a reserved profile field"): + profile.to_dict() + + @pytest.mark.parametrize( "key", [ "version", - "spec", + "name", "services", "capabilities", "payment_handlers", - "signing_keys", - "name", - "signature", + "supported_versions", "__proto__", "constructor", "prototype", ], ) -def test_extras_reserved_collision_rejected(key: str) -> None: - profile = build_ucp_profile(**_base_kwargs(), extras={key: "attacker"}) - with pytest.raises(ValueError, match="collides with a reserved profile field"): +def test_ucp_extras_reserved_collision_rejected(key: str) -> None: + profile = build_ucp_profile(**_base_kwargs(), ucp_extras={key: "attacker"}) + with pytest.raises(ValueError, match="collides with a reserved `ucp` field"): profile.to_dict() -# 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. def _claims_of(account_verification: dict, operator_verification: dict | None = None) -> dict: raw: dict = {"account_verification": account_verification} if operator_verification is not None: raw["operator_verification"] = operator_verification - result = AssessResult( - allow=True, - resolved_operator="op_abc", - raw=raw, - ) + result = AssessResult(allow=True, resolved_operator="op_abc", raw=raw) profile = build_ucp_profile(**_base_kwargs(), data=result) d = profile.to_dict() - cap = next(c for c in d["capabilities"] if c["name"] == AGENTSCORE_UCP_CAPABILITY) - return cap["claims"] + return _agentscore_cap(d)["claims"] def test_coerces_empty_string_kyc_level_to_none() -> None: @@ -189,10 +260,8 @@ def test_coerces_empty_string_verified_at_to_none() -> None: def test_both_empty_string_verified_at_normalizes_to_none() -> None: - """Both account_verification + operator_verification with verified_at='' + """Both account_verification + operator_verification with verified_at="" must normalize to None for cross-language byte parity with Node SDK. - Without the trailing ``or None``, Python's chained ``or`` returns the last - falsy value (``""``); Node's ``a || b || null`` returns ``null``. """ assert ( _claims_of( @@ -223,17 +292,13 @@ def test_typed_operator_verification_fallback_when_raw_is_none() -> None: ) profile = build_ucp_profile(**_base_kwargs(), data=result) d = profile.to_dict() - cap = next(c for c in d["capabilities"] if c["name"] == AGENTSCORE_UCP_CAPABILITY) - claims = cap["claims"] + claims = _agentscore_cap(d)["claims"] assert claims["operator_id"] == "op_typed" assert claims["kyc_level"] == "enhanced" assert claims["verified_at"] == "2026-04-01T00:00:00Z" def test_typed_account_verification_fallback_when_raw_is_none() -> None: - # `AssessResult.account_verification` is a typed optional field; a - # hand-constructed result populates it directly via the constructor and the - # builder reads it without consulting `raw`. result = AssessResult( allow=True, resolved_operator="op_typed", @@ -248,8 +313,7 @@ def test_typed_account_verification_fallback_when_raw_is_none() -> None: ) profile = build_ucp_profile(**_base_kwargs(), data=result) d = profile.to_dict() - cap = next(c for c in d["capabilities"] if c["name"] == AGENTSCORE_UCP_CAPABILITY) - claims = cap["claims"] + claims = _agentscore_cap(d)["claims"] assert claims["kyc_level"] == "verified" assert claims["age_bracket"] == "21+" assert claims["jurisdiction"] == "US" @@ -257,13 +321,6 @@ def test_typed_account_verification_fallback_when_raw_is_none() -> None: def test_typed_takes_precedence_over_raw() -> None: - # When the typed `operator_verification` / `account_verification` fields - # disagree with `data.raw`, the typed values win. Mirrors the node sibling - # which reads `input.data.operator_verification` directly without - # consulting `raw`. Production callers populate raw and the typed fields - # stay in sync; pinning typed-precedence keeps a hand-constructed - # AssessResult from emitting a profile that one language verifies and the - # other rejects. result = AssessResult( allow=True, resolved_operator="op_xyz", @@ -275,17 +332,11 @@ def test_typed_takes_precedence_over_raw() -> None: }, ) profile = build_ucp_profile(**_base_kwargs(), data=result) - cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) - # Typed `account_verification.kyc_level == 'verified'` wins over the - # `none` value carried in `data.raw`. + cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] assert cap.extras["claims"]["kyc_level"] == "verified" def test_raw_fallback_used_when_typed_missing() -> None: - # When typed `operator_verification` / `account_verification` are absent, - # the builder falls back to `data.raw`. This is the production path: - # `AgentScoreClient` populates both, but legacy or ad-hoc callers may - # only set raw. result = AssessResult( allow=True, resolved_operator="op_raw", @@ -296,48 +347,93 @@ def test_raw_fallback_used_when_typed_missing() -> None: }, ) profile = build_ucp_profile(**_base_kwargs(), data=result) - cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) - # `kyc_level` falls back to raw `account_verification.kyc_level`. + cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] assert cap.extras["claims"]["kyc_level"] == "enhanced" -# Per-element to_dict reserved-key collision guard. Mirrors the parent -# UCPProfile.to_dict guard so vendor extras can't silently overwrite a canonical -# field on UCPService / UCPCapability / UCPSigningKey via `out.update(extras)`. +# Per-element to_dict reserved-key collision guard. Vendor extras can't silently +# overwrite a canonical field on the new binding dataclasses. -def test_ucp_service_extras_collision_with_type_rejected() -> None: - svc = UCPService(type="rest", extras={"type": "different"}) - with pytest.raises(ValueError, match=r"UCPService\.extras key 'type' collides"): +def test_ucp_service_binding_extras_collision_rejected() -> None: + svc = UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/spec", + transport="rest", + extras={"transport": "different"}, + ) + with pytest.raises(ValueError, match=r"UCPServiceBinding\.extras key 'transport' collides"): svc.to_dict() -def test_ucp_service_extras_collision_with_url_rejected() -> None: - svc = UCPService(type="rest", url="https://x.example", extras={"url": "https://attacker.example"}) - with pytest.raises(ValueError, match=r"UCPService\.extras key 'url' collides"): - svc.to_dict() +def test_ucp_service_binding_extras_non_reserved_pass_through() -> None: + svc = UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/spec", + transport="rest", + endpoint="https://x.example", + extras={"region": "us-west-1"}, + ) + out = svc.to_dict() + assert out["region"] == "us-west-1" + assert out["endpoint"] == "https://x.example" -def test_ucp_service_extras_non_reserved_pass_through() -> None: - svc = UCPService(type="rest", url="https://x.example", extras={"region": "us-west-1"}) - assert svc.to_dict() == {"type": "rest", "url": "https://x.example", "region": "us-west-1"} +def test_ucp_capability_binding_extras_collision_rejected() -> None: + cap = UCPCapabilityBinding( + version="1", + spec="https://x/spec", + schema="https://x/schema", + extras={"schema": "https://attacker"}, + ) + with pytest.raises(ValueError, match=r"UCPCapabilityBinding\.extras key 'schema' collides"): + cap.to_dict() -def test_ucp_capability_extras_collision_with_name_rejected() -> None: - cap = UCPCapability(name="checkout", extras={"name": "different"}) - with pytest.raises(ValueError, match=r"UCPCapability\.extras key 'name' collides"): - cap.to_dict() +def test_ucp_capability_binding_claims_extra_passes_through() -> None: + cap = UCPCapabilityBinding( + version="1", + spec="https://x/spec", + schema="https://x/schema", + extras={"claims": {"k": "v"}}, + ) + out = cap.to_dict() + assert out["claims"] == {"k": "v"} -def test_ucp_capability_extras_collision_with_schema_rejected() -> None: - cap = UCPCapability(name="checkout", schema="https://x/y", extras={"schema": "https://attacker"}) - with pytest.raises(ValueError, match=r"UCPCapability\.extras key 'schema' collides"): - cap.to_dict() +def test_ucp_payment_handler_binding_omits_default_empty_config() -> None: + h = UCPPaymentHandlerBinding( + id="tempo", + version="1", + spec="https://x", + schema="https://x", + ) + out = h.to_dict() + assert "config" not in out + assert out["id"] == "tempo" + + +def test_ucp_payment_handler_binding_omits_explicit_empty_config() -> None: + h = UCPPaymentHandlerBinding( + id="tempo", + version="1", + spec="https://x", + schema="https://x", + config={}, + ) + assert "config" not in h.to_dict() -def test_ucp_capability_extras_non_reserved_pass_through() -> None: - cap = UCPCapability(name="checkout", extras={"claims": {"k": "v"}}) - assert cap.to_dict() == {"name": "checkout", "claims": {"k": "v"}} +def test_ucp_payment_handler_binding_preserves_populated_config() -> None: + h = UCPPaymentHandlerBinding( + id="tempo", + version="1", + spec="https://x", + schema="https://x", + config={"recipient": "0xabc"}, + ) + out = h.to_dict() + assert out["config"] == {"recipient": "0xabc"} def test_ucp_signing_key_extras_collision_with_kid_rejected() -> None: @@ -346,47 +442,12 @@ def test_ucp_signing_key_extras_collision_with_kid_rejected() -> None: sk.to_dict() -def test_ucp_signing_key_extras_collision_with_kty_rejected() -> None: - sk = UCPSigningKey(kid="me", kty="EC", extras={"kty": "RSA"}) - with pytest.raises(ValueError, match=r"UCPSigningKey\.extras key 'kty' collides"): - sk.to_dict() - - def test_ucp_signing_key_extras_non_reserved_pass_through() -> None: sk = UCPSigningKey(kid="me", kty="EC", alg="ES256", crv="P-256", extras={"x": "abc", "y": "def"}) out = sk.to_dict() assert out == {"kid": "me", "kty": "EC", "alg": "ES256", "crv": "P-256", "x": "abc", "y": "def"} -# UCPPaymentHandler.to_dict omits `config` when empty. Node's -# `UCPPaymentHandler.config` is optional (`Record?`), so a Node -# caller writing `{name: 'tempo'}` ships a wire profile WITHOUT the `config` key. -# Python must do the same or the same logical input produces different canonical -# bytes between SDKs. Explicit `config={}` is semantically identical to absent -# and follows the same omit rule. - - -def test_ucp_payment_handler_to_dict_omits_default_empty_config() -> None: - assert UCPPaymentHandler(name="tempo").to_dict() == {"name": "tempo"} - - -def test_ucp_payment_handler_to_dict_omits_explicit_empty_config() -> None: - assert UCPPaymentHandler(name="tempo", config={}).to_dict() == {"name": "tempo"} - - -def test_ucp_payment_handler_to_dict_preserves_populated_config() -> None: - assert UCPPaymentHandler(name="tempo", config={"recipient": "0xabc"}).to_dict() == { - "name": "tempo", - "config": {"recipient": "0xabc"}, - } - - -# Typed-vs-raw read order: `data.account_verification == {}` means "API -# explicitly returned an empty block" and must win over `data.raw`. Only when -# the typed field is `None` does the builder fall back to raw. Mirrors the Node -# sibling, which reads the typed field directly without consulting raw. - - def test_typed_empty_account_verification_wins_over_raw() -> None: result = AssessResult( allow=True, @@ -395,9 +456,7 @@ def test_typed_empty_account_verification_wins_over_raw() -> None: raw={"account_verification": {"kyc_level": "verified"}}, ) profile = build_ucp_profile(**_base_kwargs(), data=result) - cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) - # Empty typed dict suppresses the raw fallback; kyc_level falls through to - # the schema default "none" instead of bleeding the raw "verified" value. + cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] assert cap.extras["claims"]["kyc_level"] == "none" @@ -405,11 +464,16 @@ def test_typed_empty_operator_verification_wins_over_raw() -> None: result = AssessResult( allow=True, resolved_operator="op_xyz", - # Empty dict is a valid typed value (means "operator block returned empty"). operator_verification=cast("OperatorVerification", {}), raw={"operator_verification": {"level": "enhanced", "verified_at": "2026-01-01T00:00:00Z"}}, ) profile = build_ucp_profile(**_base_kwargs(), data=result) - cap = next(c for c in profile.capabilities if c.name == AGENTSCORE_UCP_CAPABILITY) - # Empty typed dict suppresses raw fallback; verified_at falls through to None. + cap = profile.ucp.capabilities[AGENTSCORE_UCP_CAPABILITY][0] assert cap.extras["claims"]["verified_at"] is None + + +def test_ucp_profile_body_can_be_constructed_directly() -> None: + """UCPProfileBody is exported so callers can pre-build the body if they want.""" + body = UCPProfileBody(version="2026-04-08") + assert body.to_dict()["version"] == "2026-04-08" + assert body.to_dict()["services"] == {} From bcd10600e08656a756781b95d13b96c8ebeaa960 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 06:16:04 -0700 Subject: [PATCH 34/37] 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 dicts keyed by reverse-DNS name (matches the build_ucp_profile 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 | 39 +++++++++++++++++++++++++++------ examples/signed_ucp_merchant.py | 28 +++++++++++++++++++---- 2 files changed, 56 insertions(+), 11 deletions(-) diff --git a/README.md b/README.md index 53f7efb..c836804 100644 --- a/README.md +++ b/README.md @@ -181,9 +181,9 @@ headers = build_payment_headers(BuildPaymentHeadersInput( ```python from agentscore_commerce.identity import ( - UCPService, + UCPServiceBinding, UCPSigningKey, - UCPPaymentHandler, + UCPPaymentHandlerBinding, A2AAgentCardCapabilities, build_a2a_agent_card, build_ucp_profile, @@ -193,16 +193,41 @@ from agentscore_commerce.identity import ( card = build_a2a_agent_card(name="My Service", url=base_url, capabilities=A2AAgentCardCapabilities(...), data=assess_result) # Google Universal Commerce Protocol. Publish at /.well-known/ucp. +# Output shape: {"ucp": {"version", "services", "capabilities", +# "payment_handlers", "name?", "supported_versions?"}, "signing_keys": [...]} +# — 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. profile = build_ucp_profile( name="My Service", - services=[UCPService(type="rest", url=base_url)], - payment_handlers=[UCPPaymentHandler(name="tempo", config={"recipient": TEMPO_ADDR})], + services={ + "dev.ucp.shopping": [ + UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/2026-04-08/specification/overview", + transport="mcp", + endpoint=f"{base_url}/api/ucp/mcp", + schema="https://ucp.dev/services/shopping/openrpc.json", + ), + ], + }, + payment_handlers={ + "sh.agentscore.payment.tempo": [ + 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": TEMPO_ADDR}, + ), + ], + }, signing_keys=[UCPSigningKey(kid="me", kty="EC", alg="ES256")], data=assess_result, ) ``` -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 `joserfc` extra (tested against joserfc v1.x; pin `joserfc>=1.0.0,<2`): +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 `joserfc` extra (tested against joserfc v1.x; pin `joserfc>=1.0.0,<2`): ```bash pip install agentscore-commerce[ucp] @@ -222,8 +247,8 @@ from agentscore_commerce.identity import ( key = generate_ucp_signing_key(kid="merchant-2026-05") profile = build_ucp_profile( name="My Service", - services=[...], - payment_handlers=[...], + services={...}, + payment_handlers={...}, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=key.public_jwk["kid"], alg="EdDSA") diff --git a/examples/signed_ucp_merchant.py b/examples/signed_ucp_merchant.py index 392b88b..0db9296 100644 --- a/examples/signed_ucp_merchant.py +++ b/examples/signed_ucp_merchant.py @@ -34,8 +34,8 @@ from fastapi.responses import JSONResponse from agentscore_commerce.identity import ( - UCPPaymentHandler, - UCPService, + UCPPaymentHandlerBinding, + UCPServiceBinding, UCPSigningKey, UCPVerificationError, build_jwks_response, @@ -105,8 +105,28 @@ async def well_known_ucp() -> JSONResponse: key = await load_signing_key() profile = build_ucp_profile( name="My Agent Service", - services=[UCPService(type="rest", url="https://agents.example.com")], - payment_handlers=[UCPPaymentHandler(name="tempo", config={"recipient": "0xfeedface"})], + services={ + "dev.ucp.shopping": [ + UCPServiceBinding( + 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": [ + 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": "0xfeedface"}, + ), + ], + }, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], ) signed = sign_ucp_profile( From 408d6ee80b9d67ebad76c8ebc06f16b11bb725a5 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 06:25:14 -0700 Subject: [PATCH 35/37] 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 Mirror of node-commerce. Updated regen script to use the spec-compliant input (services / capabilities / payment_handlers as dicts keyed by reverse-DNS name) and regenerated all 10 py-* fixtures + synced the 10 node-* fixtures from the node sibling. Also corrected the stale docstring in examples/signed_ucp_merchant.py 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). test_ucp_cross_lang.py: 21/21 pass. The corpus now actually tests cross-language byte-parity for the spec-compliant shape. Co-Authored-By: Claude Opus 4.7 (1M context) --- examples/signed_ucp_merchant.py | 13 +- scripts/regenerate_cross_lang_fixtures.py | 209 ++++++++++++------ .../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 | 49 ++-- 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 +++-- 22 files changed, 839 insertions(+), 550 deletions(-) diff --git a/examples/signed_ucp_merchant.py b/examples/signed_ucp_merchant.py index 0db9296..6a53811 100644 --- a/examples/signed_ucp_merchant.py +++ b/examples/signed_ucp_merchant.py @@ -1,9 +1,14 @@ """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:: diff --git a/scripts/regenerate_cross_lang_fixtures.py b/scripts/regenerate_cross_lang_fixtures.py index 5f6e499..49c41d9 100644 --- a/scripts/regenerate_cross_lang_fixtures.py +++ b/scripts/regenerate_cross_lang_fixtures.py @@ -1,14 +1,18 @@ """Regenerate the full cross-lang fixture corpus (Python side). -Writes all ``py-*.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 +Writes all ``py-*.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/test_ucp_cross_lang.py`` (and the Node sibling) pulls these -in alongside the ``node-*`` fixtures generated by the Node 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/test_ucp_cross_lang.py`` (and the Node sibling) pulls these in alongside the +``node-*`` fixtures generated by the Node sibling. + +Run: ``uv run python scripts/regenerate_cross_lang_fixtures.py`` """ from __future__ import annotations @@ -20,9 +24,9 @@ from agentscore_commerce.identity import ( AssessResult, OperatorVerification, - UCPCapability, - UCPPaymentHandler, - UCPService, + UCPCapabilityBinding, + UCPPaymentHandlerBinding, + UCPServiceBinding, UCPSigningKey, build_ucp_profile, ) @@ -51,82 +55,139 @@ def _envelope(signed: dict[str, Any], public_jwk: dict[str, Any], alg: str, kid: } +# Spec-compliant binding helpers — each scenario uses these (or variants) so the +# fixtures cover the full set of canonical UCP fields per binding type. + + +def _shop_service_mcp(host: str) -> UCPServiceBinding: + return UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/2026-04-08/specification/overview", + transport="mcp", + endpoint=f"{host}/api/ucp/mcp", + schema="https://ucp.dev/services/shopping/openrpc.json", + ) + + +def _shop_service_a2a(host: str) -> UCPServiceBinding: + return UCPServiceBinding( + version="2026-04-08", + spec="https://ucp.dev/2026-04-08/specification/overview", + transport="a2a", + endpoint=f"{host}/.well-known/agent-card.json", + ) + + +def _tempo_handler(config: dict[str, Any] | None = None) -> UCPPaymentHandlerBinding: + h = 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 is not None: + h.config = config + return h + + +def _x402_handler(networks: list[str]) -> UCPPaymentHandlerBinding: + return UCPPaymentHandlerBinding( + 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": networks}, + ) + + +def _stripe_handler(config: dict[str, Any]) -> UCPPaymentHandlerBinding: + return UCPPaymentHandlerBinding( + 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=config, + ) + + def main() -> None: - # py-minimal + # py-minimal — empty maps; just metadata + signing keys. kid = "py-minimal-EdDSA" key = generate_ucp_signing_key(kid=kid) profile = build_ucp_profile( - name="Minimal Merchant", - services=[UCPService(type="rest", url="https://m.example.com")], - payment_handlers=[], + services={"dev.ucp.shopping": [_shop_service_mcp("https://m.example.com")]}, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + name="Minimal Merchant", ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-minimal", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-es256-rails + # py-es256-rails — multi-transport service + multi-rail + ES256 signing key. kid = "py-es256-rails-ES256" key = generate_ucp_signing_key(kid=kid, alg="ES256") profile = build_ucp_profile( - name="ES256 Merchant", - services=[ - UCPService(type="rest", url="https://a.example.com"), - UCPService(type="a2a", url="https://a.example.com/agent-card.json"), - ], - payment_handlers=[ - UCPPaymentHandler(name="tempo", config={"rail": "tempo-mainnet", "chain_id": 4217}), - UCPPaymentHandler(name="x402", config={"networks": ["base-8453"]}), - ], + services={ + "dev.ucp.shopping": [ + _shop_service_mcp("https://a.example.com"), + _shop_service_a2a("https://a.example.com"), + ], + }, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + payment_handlers={ + "sh.agentscore.payment.tempo": [_tempo_handler({"rail": "tempo-mainnet", "chain_id": 4217})], + "sh.agentscore.payment.x402": [_x402_handler(["base-8453"])], + }, + name="ES256 Merchant", ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid, alg="ES256") _write("py-es256-rails", _envelope(signed, key.public_jwk, "ES256", kid)) - # py-extras-int + # py-extras-int — payment_handler config with int + string fields. kid = "py-extras-int-EdDSA" key = generate_ucp_signing_key(kid=kid) profile = build_ucp_profile( - name="Extras Merchant", - services=[UCPService(type="rest", url="https://e.example.com")], - payment_handlers=[UCPPaymentHandler(name="stripe", config={"profile_id": "abc", "count": 7})], + services={"dev.ucp.shopping": [_shop_service_mcp("https://e.example.com")]}, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + payment_handlers={ + "sh.agentscore.payment.stripe-spt": [_stripe_handler({"profile_id": "abc", "count": 7})], + }, + name="Extras Merchant", ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-extras-int", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-capability — 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). + # py-capability — hand-crafted vendor capability under sh.agentscore.identity. kid = "py-capability-EdDSA" key = generate_ucp_signing_key(kid=kid) + custom_capability = 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. + extras={"kyc_required": True}, + ) profile = build_ucp_profile( - name="Capability Merchant", - services=[UCPService(type="rest", url="https://c.example.com")], - capabilities=[ - UCPCapability( - name="sh.agentscore.identity", - schema="https://agentscore.sh/schemas/ucp/sh-agentscore-identity-v1.json", - version="1", - extras={"kyc_required": True}, - ), - ], - payment_handlers=[ - UCPPaymentHandler(name="tempo", config={"rail": "tempo-mainnet", "chain_id": 4217}), - ], + services={"dev.ucp.shopping": [_shop_service_mcp("https://c.example.com")]}, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + capabilities={"sh.agentscore.identity": [custom_capability]}, + payment_handlers={ + "sh.agentscore.payment.tempo": [_tempo_handler({"rail": "tempo-mainnet", "chain_id": 4217})], + }, + name="Capability Merchant", ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-capability", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-unicode + # py-unicode — multi-byte UTF-8 in name / endpoint / config. kid = "py-unicode-EdDSA" key = generate_ucp_signing_key(kid=kid) profile = build_ucp_profile( - name="Café 日本 🍷 Merchant", - services=[UCPService(type="rest", url="https://日本.example.com")], - payment_handlers=[UCPPaymentHandler(name="tempo", config={"note": "メモ"})], + services={"dev.ucp.shopping": [_shop_service_mcp("https://日本.example.com")]}, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + payment_handlers={ + "sh.agentscore.payment.tempo": [_tempo_handler({"note": "メモ"})], + }, + name="Café 日本 🍷 Merchant", ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-unicode", _envelope(signed, key.public_jwk, "EdDSA", kid)) @@ -135,13 +196,15 @@ def main() -> None: old_key = generate_ucp_signing_key(kid="py-multikey-old") new_key = generate_ucp_signing_key(kid="py-multikey-new") profile = build_ucp_profile( - name="Multi-Key Merchant", - services=[UCPService(type="rest", url="https://mk.example.com")], - payment_handlers=[UCPPaymentHandler(name="tempo", config={"rail": "tempo-mainnet"})], + services={"dev.ucp.shopping": [_shop_service_mcp("https://mk.example.com")]}, signing_keys=[ UCPSigningKey.from_jwk(old_key.public_jwk), UCPSigningKey.from_jwk(new_key.public_jwk), ], + payment_handlers={ + "sh.agentscore.payment.tempo": [_tempo_handler({"rail": "tempo-mainnet"})], + }, + name="Multi-Key Merchant", ) signed = sign_ucp_profile(profile.to_dict(), signing_key=new_key.private_key, kid="py-multikey-new") _write( @@ -155,33 +218,35 @@ def main() -> None: }, ) - # py-emoji-keys — extras with non-ASCII object keys (BMP private use, CJK - # compatibility, supplementary plane). Exercises codepoint-vs-UTF-16 sort. + # py-emoji-keys — extras at top-level (outside the `ucp` envelope) with non-ASCII + # object keys (BMP private use, CJK compatibility, supplementary plane). + # Exercises codepoint-vs-UTF-16 sort. kid = "py-emoji-keys-EdDSA" key = generate_ucp_signing_key(kid=kid) profile = build_ucp_profile( - name="Emoji Keys Merchant", - services=[UCPService(type="rest", url="https://emoji.example.com")], - payment_handlers=[UCPPaymentHandler(name="tempo", config={})], + services={"dev.ucp.shopping": [_shop_service_mcp("https://emoji.example.com")]}, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + payment_handlers={ + "sh.agentscore.payment.tempo": [_tempo_handler()], + }, + name="Emoji Keys Merchant", extras={ "a": 1, "豈": 2, - "": 3, + "": 3, "🍷": 4, }, ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-emoji-keys", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-int-boundary — exercises Number.MAX_SAFE_INTEGER round-trip. + # py-int-boundary — exercises Number.MAX_SAFE_INTEGER round-trip via top-level extras. kid = "py-int-boundary-EdDSA" key = generate_ucp_signing_key(kid=kid) profile = build_ucp_profile( - name="Int Boundary Merchant", - services=[UCPService(type="rest", url="https://i.example.com")], - payment_handlers=[], + services={"dev.ucp.shopping": [_shop_service_mcp("https://i.example.com")]}, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + name="Int Boundary Merchant", extras={ "max_safe_int": 9007199254740991, "min_safe_int": -9007199254740991, @@ -193,9 +258,9 @@ def main() -> None: signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-int-boundary", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-data-driven-claims — exercises the build_ucp_profile data path with - # API-shape "missing" sentinels (empty string + None). Both languages MUST - # emit identical canonical bytes for this input. + # py-data-driven-claims — exercises build_ucp_profile data path with API-shape + # "missing" sentinels (empty string + None). Both languages MUST emit identical + # canonical bytes for this input. kid = "py-data-driven-claims-EdDSA" key = generate_ucp_signing_key(kid=kid) result = AssessResult( @@ -213,17 +278,16 @@ def main() -> None: }, ) profile = build_ucp_profile( - name="Data Driven Claims Merchant", - services=[UCPService(type="rest", url="https://d.example.com")], - payment_handlers=[], + services={"dev.ucp.shopping": [_shop_service_mcp("https://d.example.com")]}, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + name="Data Driven Claims Merchant", data=result, ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) _write("py-data-driven-claims", _envelope(signed, key.public_jwk, "EdDSA", kid)) - # py-typed-claims — exercises the typed AssessResult fields (no raw - # fallback). Cross-lang parity check for the typed-field-only call site. + # py-typed-claims — exercises typed AssessResult fields (no raw fallback). + # Cross-lang parity check for the typed-field-only call site. kid = "py-typed-claims-EdDSA" key = generate_ucp_signing_key(kid=kid) result = AssessResult( @@ -245,10 +309,9 @@ def main() -> None: raw=None, ) profile = build_ucp_profile( - name="Typed Claims Merchant", - services=[UCPService(type="rest", url="https://t.example.com")], - payment_handlers=[], + services={"dev.ucp.shopping": [_shop_service_mcp("https://t.example.com")]}, signing_keys=[UCPSigningKey.from_jwk(key.public_jwk)], + name="Typed Claims Merchant", data=result, ) signed = sign_ucp_profile(profile.to_dict(), signing_key=key.private_key, kid=kid) 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 25dacc2..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": "mrV_IopnC9X6Y7B7sVWRa0hauxmLjiGMtmBpcoD9Nrg" + "x": "TikhC4jSghoLfPC6j9KBytlHrgyFvZVVm5OUjG7bYCM" } ], - "name": "Capability Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOlt7Imt5Y19yZXF1aXJlZCI6dHJ1ZSwibmFtZSI6InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy91Y3Avc2gtYWdlbnRzY29yZS1pZGVudGl0eS12MS5qc29uIiwidmVyc2lvbiI6IjEifV0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNoYWluX2lkIjo0MjE3LCJyYWlsIjoidGVtcG8tbWFpbm5ldCJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1jYXBhYmlsaXR5LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Im1yVl9Jb3BuQzlYNlk3QjdzVldSYTBoYXV4bUxqaUdNdG1CcGNvRDlOcmcifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.P38VWqceyAly7lFhP7vYluyI8PK0_9GbPX9AccaODPtxanuloyOaajrV_O_efVyw8PUsFyyo5AkIDWrApTvIDg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWNhcGFiaWxpdHktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiVGlraEM0alNnaG9MZlBDNmo5S0J5dGxIcmd5RnZaVlZtNU9Vakc3YllDTSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6eyJzaC5hZ2VudHNjb3JlLmlkZW50aXR5IjpbeyJreWNfcmVxdWlyZWQiOnRydWUsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vaWRlbnRpdHkiLCJ2ZXJzaW9uIjoiMSJ9XX0sIm5hbWUiOiJDYXBhYmlsaXR5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6eyJzaC5hZ2VudHNjb3JlLnBheW1lbnQudGVtcG8iOlt7ImNvbmZpZyI6eyJjaGFpbl9pZCI6NDIxNywicmFpbCI6InRlbXBvLW1haW5uZXQifSwiaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vYy5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0._31-NgZEBmN2c8qyxQvOaEBrhycJ6MULjhfN3sgVp5UqiUduGp66XHQC0HI4Ni6W7CzNx2-ktZWdLWD0clPdDg" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "mrV_IopnC9X6Y7B7sVWRa0hauxmLjiGMtmBpcoD9Nrg", + "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 4d1dab0..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": "NwAu8PpEQhrEI9Nqdm8GUCMVHpReceNUlBHxZozOf3g" + "x": "g_RzTBbrZ0krF4_f4Rtm__flo_1RH2sxiTF9dLltpC8" } ], - "name": "Data Driven Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZGF0YS1kcml2ZW4tY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ik53QXU4UHBFUWhyRUk5TnFkbThHVUNNVkhwUmVjZU5VbEJIeFpvek9mM2cifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.zo-SMwEWYEGiqerQZJ9m8eAZpdzN-hFfSLAhx8R8JTlCArnfaxdmH6XjLj_glztXUhy4LHs740ikLAhNDOEyDw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWRhdGEtZHJpdmVuLWNsYWltcy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJnX1J6VEJiclowa3JGNF9mNFJ0bV9fZmxvXzFSSDJzeGlURjlkTGx0cEM4In1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6InVua25vd24iLCJpc3N1ZXIiOiJodHRwczovL2FnZW50c2NvcmUuc2giLCJqdXJpc2RpY3Rpb24iOiIiLCJreWNfbGV2ZWwiOiJub25lIiwib3BlcmF0b3JfaWQiOiJvcF9kYXRhX2RyaXZlbiIsInNhbmN0aW9uc19jbGVhciI6ZmFsc2UsInZlcmlmaWVkX2F0IjpudWxsLCJ2ZXJpZnlfdXJsIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3ZlcmlmeS9vcF9kYXRhX2RyaXZlbiJ9LCJleHRlbmRzIjpbImRldi51Y3Auc2hvcHBpbmcuY2hlY2tvdXQiLCJkZXYudWNwLnNob3BwaW5nLmNhcnQiXSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIxIn1dfSwibmFtZSI6IkRhdGEgRHJpdmVuIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vZC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.X7Xdu_60_sT2XpwD9SqF7Lpuf5OGlbG_t_sxaY1xf7rQID5fSR-4BEdB0Dppq04nuaedhcqGUeyTMZDfHe8YDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "NwAu8PpEQhrEI9Nqdm8GUCMVHpReceNUlBHxZozOf3g", + "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 044cb9b..209540d 100644 --- a/tests/fixtures/cross-lang/py-emoji-keys.json +++ b/tests/fixtures/cross-lang/py-emoji-keys.json @@ -1,19 +1,31 @@ { "profile": { - "version": "2026-04-17", - "spec": "https://ucp.dev/", - "services": [ - { - "type": "rest", - "url": "https://emoji.example.com" - } - ], - "capabilities": [], - "payment_handlers": [ - { - "name": "tempo" - } - ], + "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", @@ -21,21 +33,20 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "kf2p5k_gqFL8d38h9gdlAtdvPnYIPITUcLLjzGrwgjo" + "x": "t9o2BRiSJvI4c7a3KlzCqzKS1evXIyngTwB2GBxtZec" } ], - "name": "Emoji Keys Merchant", "a": 1, "豈": 2, - "": 3, + "": 3, "🍷": 4, - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJhIjoxLCJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRW1vamkgS2V5cyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOlt7Im5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9lbW9qaS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1lbW9qaS1rZXlzLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6ImtmMnA1a19ncUZMOGQzOGg5Z2RsQXRkdlBuWUlQSVRVY0xManpHcndnam8ifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTciLCLosYgiOjIsIu6AgCI6Mywi8J-NtyI6NH0.-Gzvmm1BCE8IvcvM6D9Th7wlTl0bHn3hfdEwu9KaHRLdVjdmlWfFLkTnls6r1-tQQK_NGbNYDs7YMmE_IEHiCQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWVtb2ppLWtleXMtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyIiOjMsImEiOjEsInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktZW1vamkta2V5cy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJ0OW8yQlJpU0p2STRjN2EzS2x6Q3F6S1MxZXZYSXluZ1R3QjJHQnh0WmVjIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7fSwibmFtZSI6IkVtb2ppIEtleXMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siaWQiOiJ0ZW1wbyIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8uanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3RlbXBvIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vZW1vamkuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In0sIuixiCI6Miwi8J-NtyI6NH0.JFxAqyuCgvA0HNAl2giJeb4MbHDuW5h7jBjGcrQxcSDCiCgXjzhaUSWJXjiB7GeHcL7CDMg3kj79VQ4Rsr1-Bw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "kf2p5k_gqFL8d38h9gdlAtdvPnYIPITUcLLjzGrwgjo", + "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 d54829d..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": "2Ghew5w2FeQeIYwRUBpI3sWQGhzI1SEBiqMhQhhuNZo", - "y": "6LmrMRZkHKGADmavYFDDSYn2PJID4vY-D6XNXwQPE3E" + "x": "NFS5qrSPV5sDQ5hHVag2zFqOSpTO6NBL-Hqf9EjBOco", + "y": "wtbEwX6TxEFid1IJvIwxkVfNic3Q_xEOq7j54Kje7aY" } ], - "name": "ES256 Merchant", - "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRVMyNTYgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9LHsiY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwibmFtZSI6Ing0MDIifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbSJ9LHsidHlwZSI6ImEyYSIsInVybCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hZ2VudC1jYXJkLmpzb24ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRVMyNTYiLCJjcnYiOiJQLTI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2Iiwia3R5IjoiRUMiLCJ1c2UiOiJzaWciLCJ4IjoiMkdoZXc1dzJGZVFlSVl3UlVCcEkzc1dRR2h6STFTRUJpcU1oUWhodU5abyIsInkiOiI2TG1yTVJaa0hLR0FEbWF2WUZERFNZbjJQSklENHZZLUQ2WE5Yd1FQRTNFIn1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.2lhHbFsTMxpK4Vrm8zE3wLPQna6syr7hiI0hSV7C-_gtGcf4UruXjeHaEq58jP5D1bdh-ftyFkZQwx0naBIASw" + "signature": "eyJhbGciOiJFUzI1NiIsImtpZCI6InB5LWVzMjU2LXJhaWxzLUVTMjU2IiwidHlwIjoiYWdlbnRzY29yZS1wcm9maWxlK2p3cyJ9.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVTMjU2IiwiY3J2IjoiUC0yNTYiLCJraWQiOiJweS1lczI1Ni1yYWlscy1FUzI1NiIsImt0eSI6IkVDIiwidXNlIjoic2lnIiwieCI6Ik5GUzVxclNQVjVzRFE1aEhWYWcyekZxT1NwVE82TkJMLUhxZjlFakJPY28iLCJ5Ijoid3RiRXdYNlR4RUZpZDFJSnZJd3hrVmZOaWMzUV94RU9xN2o1NEtqZTdhWSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJFUzI1NiBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJjb25maWciOnsiY2hhaW5faWQiOjQyMTcsInJhaWwiOiJ0ZW1wby1tYWlubmV0In0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dLCJzaC5hZ2VudHNjb3JlLnBheW1lbnQueDQwMiI6W3siY29uZmlnIjp7Im5ldHdvcmtzIjpbImJhc2UtODQ1MyJdfSwiaWQiOiJ4NDAyIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy94NDAyLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy94NDAyIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In0seyJlbmRwb2ludCI6Imh0dHBzOi8vYS5leGFtcGxlLmNvbS8ud2VsbC1rbm93bi9hZ2VudC1jYXJkLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6ImEyYSIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.Vo7XPWeW37oSI5Eub7oUVwb3ODY3g70PeNgYLODGQ9L2nrf-5K7yinG2QwHEh5GtIMq7fXp5fiQVk1KtFnL4Wg" }, "jwks": { "keys": [ { "crv": "P-256", - "x": "2Ghew5w2FeQeIYwRUBpI3sWQGhzI1SEBiqMhQhhuNZo", - "y": "6LmrMRZkHKGADmavYFDDSYn2PJID4vY-D6XNXwQPE3E", + "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 9733c48..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": "hvngLt3fgbEzbSmgFeeUpV1t5M-SU6T0M5bicVxPDJU" + "x": "El2ke55St-sfq6gYs6wYJyJX7TIw3-spyA1hlMiNhpM" } ], - "name": "Extras Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiRXh0cmFzIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7ImNvdW50Ijo3LCJwcm9maWxlX2lkIjoiYWJjIn0sIm5hbWUiOiJzdHJpcGUifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8vZS5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1leHRyYXMtaW50LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Imh2bmdMdDNmZ2JFemJTbWdGZWVVcFYxdDVNLVNVNlQwTTViaWNWeFBESlUifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.5rf3kusXgoEIqGzovksKlA9nYgszetmSefSRRycOLcRELqhVr88Di37CivlGravHNtXEXS_vheCEtb2uWXobAg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LWV4dHJhcy1pbnQtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiRWwya2U1NVN0LXNmcTZnWXM2d1lKeUpYN1RJdzMtc3B5QTFobE1pTmhwTSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJFeHRyYXMgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC5zdHJpcGUtc3B0IjpbeyJjb25maWciOnsiY291bnQiOjcsInByb2ZpbGVfaWQiOiJhYmMifSwiaWQiOiJzdHJpcGUiLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3N0cmlwZS1zcHQuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9wYXltZW50LWhhbmRsZXJzL3N0cmlwZS1zcHQiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9lLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.0DAtQpZ-9e8U3cmpzTHwWFZq2LmmchY6mz-rhRxybkNX4YDlqpPLcfAig7ybMzdo_O7afJ9QDNYfDERmCGtVDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "hvngLt3fgbEzbSmgFeeUpV1t5M-SU6T0M5bicVxPDJU", + "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 b18ae16..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": "3xm2hM4-PRbWmQhJ8fDZGIwBoTf1aVXJp11uSgbvDbs" + "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.eyJjYXBhYmlsaXRpZXMiOltdLCJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJuZWdfc21hbGxfaW50IjotNDIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL2kuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktaW50LWJvdW5kYXJ5LUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjN4bTJoTTQtUFJiV21RaEo4ZkRaR0l3Qm9UZjFhVlhKcDExdVNnYnZEYnMifV0sInNtYWxsX2ludCI6NDIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTciLCJ6ZXJvIjowfQ.853XGWyMf4YL7K0sGA_j0BYCjrX8cdHvzyHX5GSyEEYHL4ZMtSENQR3lw5LszLo2HwU6J93MNKA6d4qx0v1aAQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LWludC1ib3VuZGFyeS1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJtYXhfc2FmZV9pbnQiOjkwMDcxOTkyNTQ3NDA5OTEsIm1pbl9zYWZlX2ludCI6LTkwMDcxOTkyNTQ3NDA5OTEsIm5lZ19zbWFsbF9pbnQiOi00Miwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1pbnQtYm91bmRhcnktRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiYjVPbFVMeHNQMHhwUzhJa0xGNHRSYWlCMXU2eU9EUHhzUUpKWXYxaUI2cyJ9XSwic21hbGxfaW50Ijo0MiwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJJbnQgQm91bmRhcnkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7fSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL2kuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In0sInplcm8iOjB9.PsM9i8EXGN5eNPJI6_6Efk8P-aE-gQQvmXpNCr1vTFMtsjvUrwPO974mweqhbyogrdfm47UkAhJZ2tkGQ26YDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "3xm2hM4-PRbWmQhJ8fDZGIwBoTf1aVXJp11uSgbvDbs", + "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 e6d886f..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": "_oUdVFWSZkqVC-uORY2G34I0xuVXJ78ywa3qYywtWc8" + "x": "dZ6PLK4BfgrHTuRA0klbkcl6iHAXhyX3ACjRefxb8IA" } ], - "name": "Minimal Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTWluaW1hbCBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL20uZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbWluaW1hbC1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJfb1VkVkZXU1prcVZDLXVPUlkyRzM0STB4dVZYSjc4eXdhM3FZeXd0V2M4In1dLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LyIsInZlcnNpb24iOiIyMDI2LTA0LTE3In0.8hLu64spf73Z1aDO95MEqkpsZ2uFP0rHzIWFsOkZkrNq81xcAEcwGYTU1MCtoh7DCfdsb6jXrhvmrFxdhGxPAw" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LW1pbmltYWwtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiZFo2UExLNEJmZ3JIVHVSQTBrbGJrY2w2aUhBWGh5WDNBQ2pSZWZ4YjhJQSJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJNaW5pbWFsIE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6e30sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly9tLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.axue3k1ojtSWw0pZJbuDmx-HBt6DZTwtbD3DiHKwrrP3YSWjdlp_FBfBMT0jA-oQ6HqfdQ4fO9vuRAAIpBepCw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "_oUdVFWSZkqVC-uORY2G34I0xuVXJ78ywa3qYywtWc8", + "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 04471e2..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": "x5_EjdNmcCe6a9GlHrd9QHWS2bRkT_MAAVM5ZJIKokc" + "x": "lW7nqnsPzl7FVllMcMjTSHmAqaMVeBMJk4mEwgfY5Vo" }, { "kid": "py-multikey-new", @@ -32,17 +44,16 @@ "alg": "EdDSA", "use": "sig", "crv": "Ed25519", - "x": "1bxXK_gPLaOEs4PHCFxdW1vsAWjsuwh1ys94A1st7lI" + "x": "Kmwcte5hHWi17aQjekr9Zdw6fsBQl237_jllIAJBMnk" } ], - "name": "Multi-Key Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiTXVsdGktS2V5IE1lcmNoYW50IiwicGF5bWVudF9oYW5kbGVycyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sIm5hbWUiOiJ0ZW1wbyJ9XSwic2VydmljZXMiOlt7InR5cGUiOiJyZXN0IiwidXJsIjoiaHR0cHM6Ly9tay5leGFtcGxlLmNvbSJ9XSwic2lnbmluZ19rZXlzIjpbeyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1vbGQiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoieDVfRWpkTm1jQ2U2YTlHbEhyZDlRSFdTMmJSa1RfTUFBVk01WkpJS29rYyJ9LHsiYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktbXVsdGlrZXktbmV3Iiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6IjFieFhLX2dQTGFPRXM0UEhDRnhkVzF2c0FXanN1d2gxeXM5NEExc3Q3bEkifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.gfiKKlcD1dWp6q-Cg20cbXEyqXsHhSFemxkzwEhM-K-xuMoatp5BX_ZCMePxJyo78nFq6IGHTCAnv4wUk5lEDg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LW11bHRpa2V5LW5ldyIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LW11bHRpa2V5LW9sZCIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJsVzducW5zUHpsN0ZWbGxNY01qVFNIbUFxYU1WZUJNSms0bUV3Z2ZZNVZvIn0seyJhbGciOiJFZERTQSIsImNydiI6IkVkMjU1MTkiLCJraWQiOiJweS1tdWx0aWtleS1uZXciLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiS213Y3RlNWhIV2kxN2FRamVrcjlaZHc2ZnNCUWwyMzdfamxsSUFKQk1uayJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJNdWx0aS1LZXkgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjp7InNoLmFnZW50c2NvcmUucGF5bWVudC50ZW1wbyI6W3siY29uZmlnIjp7InJhaWwiOiJ0ZW1wby1tYWlubmV0In0sImlkIjoidGVtcG8iLCJzY2hlbWEiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc2NoZW1hcy9wYXltZW50LWhhbmRsZXJzL3RlbXBvLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NwZWNpZmljYXRpb24vcGF5bWVudC1oYW5kbGVycy90ZW1wbyIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwic2VydmljZXMiOnsiZGV2LnVjcC5zaG9wcGluZyI6W3siZW5kcG9pbnQiOiJodHRwczovL21rLmV4YW1wbGUuY29tL2FwaS91Y3AvbWNwIiwic2NoZW1hIjoiaHR0cHM6Ly91Y3AuZGV2L3NlcnZpY2VzL3Nob3BwaW5nL29wZW5ycGMuanNvbiIsInNwZWMiOiJodHRwczovL3VjcC5kZXYvMjAyNi0wNC0wOC9zcGVjaWZpY2F0aW9uL292ZXJ2aWV3IiwidHJhbnNwb3J0IjoibWNwIiwidmVyc2lvbiI6IjIwMjYtMDQtMDgifV19LCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9fQ.gBimQYPBcvQFutbEzKeJrLzjrkgqyClkbRuSVOaRAfzAvUsxZ5Zse1WmqhadHzv5DUZohfBiWUHjj96kToOPDQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "x5_EjdNmcCe6a9GlHrd9QHWS2bRkT_MAAVM5ZJIKokc", + "x": "lW7nqnsPzl7FVllMcMjTSHmAqaMVeBMJk4mEwgfY5Vo", "kid": "py-multikey-old", "alg": "EdDSA", "use": "sig", @@ -50,7 +61,7 @@ }, { "crv": "Ed25519", - "x": "1bxXK_gPLaOEs4PHCFxdW1vsAWjsuwh1ys94A1st7lI", + "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 4da4373..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": "NXGO-eKr-ctG46sGb2btycvvmFyjAaNRLGQT2a5hJ0E" + "x": "clSTIoRWvV4whYX40RYSSPGfcj2mL3YW-IkgYYM6SLQ" } ], - "name": "Typed Claims Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJjYXBhYmlsaXRpZXMiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJuYW1lIjoic2guYWdlbnRzY29yZS5pZGVudGl0eSIsInNjaGVtYSI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zY2hlbWFzL3VjcC9zaC1hZ2VudHNjb3JlLWlkZW50aXR5LXYxLmpzb24iLCJ2ZXJzaW9uIjoiMSJ9XSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOltdLCJzZXJ2aWNlcyI6W3sidHlwZSI6InJlc3QiLCJ1cmwiOiJodHRwczovL3QuZXhhbXBsZS5jb20ifV0sInNpZ25pbmdfa2V5cyI6W3siYWxnIjoiRWREU0EiLCJjcnYiOiJFZDI1NTE5Iiwia2lkIjoicHktdHlwZWQtY2xhaW1zLUVkRFNBIiwia3R5IjoiT0tQIiwidXNlIjoic2lnIiwieCI6Ik5YR08tZUtyLWN0RzQ2c0diMmJ0eWN2dm1GeWpBYU5STEdRVDJhNWhKMEUifV0sInNwZWMiOiJodHRwczovL3VjcC5kZXYvIiwidmVyc2lvbiI6IjIwMjYtMDQtMTcifQ.UcpNbaK9FXLzCC-zHCeZjV9HWYoFymFBqApZeliLur3sSYeW3U6JoNJlzTEu-iaZT_TOC5NCbIiVQJ02V44TCg" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsInR5cCI6ImFnZW50c2NvcmUtcHJvZmlsZStqd3MifQ.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXR5cGVkLWNsYWltcy1FZERTQSIsImt0eSI6Ik9LUCIsInVzZSI6InNpZyIsIngiOiJjbFNUSW9SV3ZWNHdoWVg0MFJZU1NQR2ZjajJtTDNZVy1Ja2dZWU02U0xRIn1dLCJ1Y3AiOnsiY2FwYWJpbGl0aWVzIjp7InNoLmFnZW50c2NvcmUuaWRlbnRpdHkiOlt7ImNsYWltcyI6eyJhZ2VfYnJhY2tldCI6IjIxKyIsImlzc3VlciI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaCIsImp1cmlzZGljdGlvbiI6IlVTIiwia3ljX2xldmVsIjoiZW5oYW5jZWQiLCJvcGVyYXRvcl9pZCI6Im9wX3R5cGVkX2NsYWltcyIsInNhbmN0aW9uc19jbGVhciI6dHJ1ZSwidmVyaWZpZWRfYXQiOiIyMDI2LTA0LTAxVDAwOjAwOjAwWiIsInZlcmlmeV91cmwiOiJodHRwczovL2FnZW50c2NvcmUuc2gvdmVyaWZ5L29wX3R5cGVkX2NsYWltcyJ9LCJleHRlbmRzIjpbImRldi51Y3Auc2hvcHBpbmcuY2hlY2tvdXQiLCJkZXYudWNwLnNob3BwaW5nLmNhcnQiXSwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvdWNwL3NoLWFnZW50c2NvcmUtaWRlbnRpdHktdjEuanNvbiIsInNwZWMiOiJodHRwczovL2FnZW50c2NvcmUuc2gvc3BlY2lmaWNhdGlvbi9pZGVudGl0eSIsInZlcnNpb24iOiIxIn1dfSwibmFtZSI6IlR5cGVkIENsYWltcyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnt9LCJzZXJ2aWNlcyI6eyJkZXYudWNwLnNob3BwaW5nIjpbeyJlbmRwb2ludCI6Imh0dHBzOi8vdC5leGFtcGxlLmNvbS9hcGkvdWNwL21jcCIsInNjaGVtYSI6Imh0dHBzOi8vdWNwLmRldi9zZXJ2aWNlcy9zaG9wcGluZy9vcGVucnBjLmpzb24iLCJzcGVjIjoiaHR0cHM6Ly91Y3AuZGV2LzIwMjYtMDQtMDgvc3BlY2lmaWNhdGlvbi9vdmVydmlldyIsInRyYW5zcG9ydCI6Im1jcCIsInZlcnNpb24iOiIyMDI2LTA0LTA4In1dfSwidmVyc2lvbiI6IjIwMjYtMDQtMTcifX0.0BQic1wyTNOk4TVcs2dJ6iRARokGtSjzMzbP9myAlMdF8zpnXfWAzZ6MwUsmH10eK7PQtRrj5D-St4_xxw6SBw" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "NXGO-eKr-ctG46sGb2btycvvmFyjAaNRLGQT2a5hJ0E", + "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 fe388d0..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": "sMS1O_Zfgj0a9_pByNKOC7v9K8wvUejvwvnGXBg5sP4" + "x": "Rk_x9yyAht9Xy_mKxxmdh0kyr12andlLUGHY2xh8-3w" } ], - "name": "Café 日本 🍷 Merchant", - "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJjYXBhYmlsaXRpZXMiOltdLCJuYW1lIjoiQ2Fmw6kg5pel5pysIPCfjbcgTWVyY2hhbnQiLCJwYXltZW50X2hhbmRsZXJzIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJuYW1lIjoidGVtcG8ifV0sInNlcnZpY2VzIjpbeyJ0eXBlIjoicmVzdCIsInVybCI6Imh0dHBzOi8v5pel5pysLmV4YW1wbGUuY29tIn1dLCJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4Ijoic01TMU9fWmZnajBhOV9wQnlOS09DN3Y5Szh3dlVlanZ3dm5HWEJnNXNQNCJ9XSwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0xNyJ9.cUCqReLQvenkFtKE1xDaMljqLjXubZEO3a4lCbEKtPiQHcP_7sIqRZMvUx3mgdwQY0ph_6ui0ZWgKb3EwnvPCQ" + "signature": "eyJhbGciOiJFZERTQSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJ0eXAiOiJhZ2VudHNjb3JlLXByb2ZpbGUrandzIn0.eyJzaWduaW5nX2tleXMiOlt7ImFsZyI6IkVkRFNBIiwiY3J2IjoiRWQyNTUxOSIsImtpZCI6InB5LXVuaWNvZGUtRWREU0EiLCJrdHkiOiJPS1AiLCJ1c2UiOiJzaWciLCJ4IjoiUmtfeDl5eUFodDlYeV9tS3h4bWRoMGt5cjEyYW5kbExVR0hZMnhoOC0zdyJ9XSwidWNwIjp7ImNhcGFiaWxpdGllcyI6e30sIm5hbWUiOiJDYWbDqSDml6XmnKwg8J-NtyBNZXJjaGFudCIsInBheW1lbnRfaGFuZGxlcnMiOnsic2guYWdlbnRzY29yZS5wYXltZW50LnRlbXBvIjpbeyJjb25maWciOnsibm90ZSI6IuODoeODoiJ9LCJpZCI6InRlbXBvIiwic2NoZW1hIjoiaHR0cHM6Ly9hZ2VudHNjb3JlLnNoL3NjaGVtYXMvcGF5bWVudC1oYW5kbGVycy90ZW1wby5qc29uIiwic3BlYyI6Imh0dHBzOi8vYWdlbnRzY29yZS5zaC9zcGVjaWZpY2F0aW9uL3BheW1lbnQtaGFuZGxlcnMvdGVtcG8iLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInNlcnZpY2VzIjp7ImRldi51Y3Auc2hvcHBpbmciOlt7ImVuZHBvaW50IjoiaHR0cHM6Ly_ml6XmnKwuZXhhbXBsZS5jb20vYXBpL3VjcC9tY3AiLCJzY2hlbWEiOiJodHRwczovL3VjcC5kZXYvc2VydmljZXMvc2hvcHBpbmcvb3BlbnJwYy5qc29uIiwic3BlYyI6Imh0dHBzOi8vdWNwLmRldi8yMDI2LTA0LTA4L3NwZWNpZmljYXRpb24vb3ZlcnZpZXciLCJ0cmFuc3BvcnQiOiJtY3AiLCJ2ZXJzaW9uIjoiMjAyNi0wNC0wOCJ9XX0sInZlcnNpb24iOiIyMDI2LTA0LTE3In19.fraS8Y7ecHdldvmqwIdCzvSlBqi2GvYatX4UmSnR0jBnKDY8qxQnfYErAbJQ8ywXnP8Ztsp7PvbaRd90GIZ0CQ" }, "jwks": { "keys": [ { "crv": "Ed25519", - "x": "sMS1O_Zfgj0a9_pByNKOC7v9K8wvUejvwvnGXBg5sP4", + "x": "Rk_x9yyAht9Xy_mKxxmdh0kyr12andlLUGHY2xh8-3w", "kid": "py-unicode-EdDSA", "alg": "EdDSA", "use": "sig", From 1b40810f6b6d2be538da8d7b1c63b4c7381f0b32 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 06:29:20 -0700 Subject: [PATCH 36/37] docs(claude): add signed_ucp_merchant.py to examples table (parity with node) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Python CLAUDE.md was missing the signed UCP merchant example row entirely. Added with the same vendor-extension framing applied to the node sibling: agentscore-profile+jws is opt-in for trust-mode verifiers, not a UCP §6 requirement. Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CLAUDE.md b/CLAUDE.md index 14574ad..e3907c2 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -45,6 +45,7 @@ Peer-dep pattern: payment/x402/mppx/stripe modules import lazily at runtime; ven | `variable_cost_merchant.py` | Pay-per-actual-usage on **two protocols**: x402 upto (Permit2 + Settlement-Overrides) AND MPP tempo session (channel + SSE + mid-stream vouchers) | | `compliance_merchant.py` | Regulated-goods merchant: full compliance gate + custom `on_denied` composing the denial helpers (`verification_agent_instructions`, `is_fixable_denial`, `build_signer_mismatch_body`, `build_contact_support_next_steps`, `denial_reason_to_body`/`denial_reason_status`) | | `per_product_policy_merchant.py` | Multi-product merchant where each row carries its own compliance policy. One product hard-gates KYC + age + state; another is anonymous; a third uses `enforcement="soft"` (request KYC but don't block sale). Demonstrates `PolicyBlock`, `build_gate_from_policy`, `run_gate_with_enforcement`, `shipping_country_allowed`, `shipping_state_allowed`. | +| `signed_ucp_merchant.py` | 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 `generate_ucp_signing_key`, `sign_ucp_profile`, `build_jwks_response`, `UCPSigningKey.from_jwk`, `UCPVerificationError`. | ## Identity model From 05018cc35f28cb8bdb5c69732a6615bdf16467f1 Mon Sep 17 00:00:00 2001 From: vvillait88 Date: Sun, 10 May 2026 06:42:22 -0700 Subject: [PATCH 37/37] fix(identity): validate kid/kty are non-empty strings in UCPSigningKey.from_jwk Previously only checked key presence, not type. A JWK with kid=None or kid="" would fall through to the dataclass and serialize to invalid JSON during JCS canonicalization (signing would still succeed, producing a profile that verifiers cannot parse). Tighter validation matches the node sibling's `typeof jwk.kid !== 'string' || !jwk.kid` check. Co-Authored-By: Claude Opus 4.7 (1M context) --- agentscore_commerce/identity/ucp.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/agentscore_commerce/identity/ucp.py b/agentscore_commerce/identity/ucp.py index 10fc2c8..d63f765 100644 --- a/agentscore_commerce/identity/ucp.py +++ b/agentscore_commerce/identity/ucp.py @@ -81,11 +81,11 @@ def from_jwk(cls, jwk: dict[str, Any]) -> UCPSigningKey: if not isinstance(jwk, dict): msg = f"UCPSigningKey.from_jwk expected a dict; got {type(jwk).__name__}." raise ValueError(msg) - if "kid" not in jwk: - msg = "UCPSigningKey.from_jwk: JWK missing required field `kid`." + if not isinstance(jwk.get("kid"), str) or not jwk["kid"]: + msg = "UCPSigningKey.from_jwk: JWK missing required field `kid` (or non-string/empty)." raise ValueError(msg) - if "kty" not in jwk: - msg = "UCPSigningKey.from_jwk: JWK missing required field `kty`." + if not isinstance(jwk.get("kty"), str) or not jwk["kty"]: + msg = "UCPSigningKey.from_jwk: JWK missing required field `kty` (or non-string/empty)." raise ValueError(msg) if jwk["kty"] not in {"OKP", "EC", "RSA"}: msg = (