diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5ffc77a..40f5de7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,6 +9,9 @@ on: jobs: lint: runs-on: ubuntu-latest + defaults: + run: + working-directory: python steps: - uses: actions/checkout@v4 @@ -28,6 +31,9 @@ jobs: test: runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: python strategy: matrix: os: [ubuntu-latest, macos-latest, windows-latest] @@ -45,3 +51,33 @@ jobs: - name: Run tests run: pytest tests/ -v + + typescript: + runs-on: ubuntu-latest + defaults: + run: + working-directory: typescript + steps: + - uses: actions/checkout@v4 + + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: "22" + cache: npm + cache-dependency-path: typescript/package-lock.json + + - name: Install dependencies + run: npm ci + + - name: Lint + run: npm run lint + + - name: Typecheck + run: npm run typecheck + + - name: Run tests + run: npm test + + - name: Build + run: npm run build diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 8ef9e26..49dd155 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -39,7 +39,8 @@ for this purpose. `fix: correct cart hash binding in autonomous mode`). Individual commit messages within a PR are not required to follow this format. - **Tests**: All code changes should include tests or demonstrate no regression - against the existing test suite. Run `pytest` from the repo root to verify. + against the existing test suite. Run `pytest` from the `python/` directory to + verify. - **DCO sign-off**: Every commit must include a `Signed-off-by` line. ### Specification Changes @@ -68,7 +69,7 @@ or OpenAPI spec where available. ```bash # Clone the repo git clone https://github.com/agent-intent/verifiable-intent.git -cd verifiable-intent +cd verifiable-intent/python # Install with dev dependencies (includes pytest) pip install -e ".[dev]" @@ -80,6 +81,14 @@ pytest python examples/autonomous_flow.py ``` +The TypeScript implementation lives in `typescript/`: + +```bash +cd typescript +npm install +npm test +``` + ## Reporting Issues Use [GitHub Issues](https://github.com/agent-intent/verifiable-intent/issues) diff --git a/README.md b/README.md index 61eac0b..fbfbcb5 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,19 @@ Verifiable Intent defines a layered SD-JWT credential format that creates a tamper-evident chain providing cryptographic evidence that an AI agent's actions were within the scope delegated by a human user. +## Repository layout + +This repository is a polyglot monorepo with reference implementations that share +one specification and one set of conformance vectors: + +| Path | Contents | +|------|----------| +| [`python/`](python) | Python reference implementation (`verifiable-intent` package), tests, and examples | +| [`typescript/`](typescript) | TypeScript port (`@verifiable-intent/core`), built on `@sd-jwt/*` | +| [`spec/`](spec) | Normative specification (credential format, constraints, security model, design rationale) | +| [`protocol-landscape/`](protocol-landscape) | Positioning relative to AP2, UCP, and ACP | +| [`docs/`](docs) | Documentation site source ([verifiableintent.dev](https://verifiableintent.dev)) | + ## The Problem When a human delegates a purchase to an AI agent, no party in the transaction diff --git a/protocol-landscape/glossary.md b/protocol-landscape/glossary.md index 5c9e355..f60b5b6 100644 --- a/protocol-landscape/glossary.md +++ b/protocol-landscape/glossary.md @@ -6,10 +6,14 @@ terms; this document maps terminology for implementers working with a specific protocol. > **Protocol versions**: Based on the Universal Commerce Protocol (UCP) specification -> as of 23 January 2026, the Agent Payments Protocol (AP2) specification as of -> 16 September 2025, and the Agentic Commerce Protocol (ACP) specification as of +> as of 23 January 2026, the Agent Payments Protocol (AP2) version 0.2.0 ("V2", +> 28 April 2026), and the Agentic Commerce Protocol (ACP) specification as of > 30 January 2026. Mappings reflect these versions; "no equivalent" statements are -> bounded by these dates. +> bounded by these dates. AP2 V2 introduced the Human-Not-Present model with Open +> Checkout/Open Payment Mandates, SD-JWT disclosures, and `cnf`-based agent key +> binding — natively defining much of the delegation mechanism VI was positioned +> to fill. Rows below that still read "no equivalent" against pre-V2 AP2 are +> bounded by the noted date and may now have an AP2 V2 analogue. **How to read the tables:** Each row starts with the canonical VI term as defined in [`spec/README.md`](../spec/README.md). The definition column @@ -40,7 +44,7 @@ differences. |---------|-----------|----------------------|----------------|-------| | **Layer 1 (L1)** | Credential Provider SD-JWT binding user identity to a public key via `cnf.jwk`. Long-lived (~1 year) | --- | --- | No equivalent in any protocol. AP2 assumes identity binding happens outside the VDC framework. ACP has no identity credential layer. L1 is a VI-specific contribution | | **Layer 2 (L2), Immediate** | User KB-SD-JWT with finalized checkout and payment mandates (`vct: "mandate.checkout.1"`, `vct: "mandate.payment.1"`). No `cnf` in mandates (no further delegation). Human present | Checkout mandate / `ap2.checkout_mandate` (UCP) / Cart Mandate VDC (AP2) | Checkout session at `ready_for_payment` | UCP's checkout mandate extension wraps AP2 Cart Mandates in UCP checkout sessions. AP2 Cart Mandate bundles cart details + payment request + merchant signature. ACP maps to the checkout session reaching `ready_for_payment`, where the buyer confirms final values before CompleteCheckout. VI L2 Immediate separates checkout and payment into distinct selectively-disclosable mandates | -| **Layer 2 (L2), Autonomous** | User KB-SD-JWT with constraint-bearing mandates (`vct: "mandate.checkout.open.1"`, `vct: "mandate.payment.open.1"`) and `cnf.jwk` binding the agent's key. Human not present | Pre-checkout delegation (UCP) / Intent Mandate VDC (AP2) | --- | UCP has no formal pre-checkout delegation credential but the concept maps to agent actions before checkout creation. AP2 Intent Mandate carries natural language description + merchant/SKU lists. No ACP equivalent; the agent creates and completes checkout sessions without a prior constraint credential. VI L2 Autonomous carries typed constraints (quantitative constraints are machine-enforceable; qualitative constraints are informational) | +| **Layer 2 (L2), Autonomous** | User KB-SD-JWT with constraint-bearing mandates (`vct: "mandate.checkout.open.1"`, `vct: "mandate.payment.open.1"`) and `cnf.jwk` binding the agent's key. Human not present | Pre-checkout delegation (UCP) / Open Checkout + Open Payment Mandate (AP2 V2) | --- | AP2 V2's **Open Checkout Mandate** + **Open Payment Mandate** are the structural twin of VI L2 Autonomous: same VCT strings (`mandate.checkout.open.1` / `mandate.payment.open.1`), same `cnf` agent-key binding, same `constraints[]` array, and the same `payment.reference.conditional_transaction_id` ("Digest of the associated Open Checkout Mandate"). AP2's **Intent Mandate** is a separate, higher-level human-present artifact (natural-language description + merchant/SKU lists), not this constraint credential. UCP has no formal pre-checkout delegation credential. No ACP equivalent. VI L2 Autonomous carries typed constraints (quantitative constraints are machine-enforceable; qualitative constraints are informational) | | **Layer 3 (L3a / L3b)** | Split Agent KB-SD-JWTs proving constraint satisfaction with finalized values. L3a (payment mandate) → payment network; L3b (checkout mandate) → merchant. Cross-referenced via `transaction_id` == `checkout_hash`. Short-lived (~5 min). Autonomous mode only | --- | --- | No equivalent in any protocol. AP2 assumes the agent's fulfillment is captured in the Cart Mandate after the agent shops. ACP has no agent fulfillment credential. L3 is a VI-specific contribution providing an auditable link between user constraints and agent actions | | **Checkout Mandate** | Selectively disclosable claim (`vct: "mandate.checkout.open.1"` Autonomous L2, `vct: "mandate.checkout.1"` Immediate L2 / L3b) describing allowed products (Autonomous) or finalized checkout (Immediate) | `line_items[]` in checkout (UCP) / Cart Mandate VDC (AP2) | `line_items[]` in checkout session | UCP models cart contents as `line_items[]` within checkout sessions. AP2 Cart Mandate includes payment details; VI checkout mandate is purely about products. ACP uses `line_items[]` in checkout sessions with a similar purpose. AP2 Intent Mandate `skus` field maps loosely to VI `mandate.checkout.line_items` constraint | | **Payment Mandate** | Selectively disclosable claim (`vct: "mandate.payment.open.1"` Autonomous L2, `vct: "mandate.payment.1"` Immediate L2 / L3a) describing allowed or final payment parameters | `payment.instruments` (UCP) / Payment Mandate VDC (AP2) | `payment_data` in CompleteCheckout + SPT `allowance` | UCP exposes payment instruments through the checkout session. ACP splits this across `payment_data` (in CompleteCheckout) and SPT `allowance` (for delegated execution). VI adds selective disclosure enforcement (merchant sees checkout, not payment; network sees payment, not checkout). AP2 shares the full Payment Mandate with the network | @@ -55,7 +59,7 @@ differences. | VI Term | Definition | UCP / AP2 Equivalent | ACP Equivalent | Notes | |---------|-----------|----------------------|----------------|-------| | **Immediate Mode** | 2-layer flow (L1 + L2). User confirms final values directly. No agent delegation | User-confirmed checkout (UCP) / Human-Present transaction (AP2) | Buyer-confirmed checkout | Same conceptual flow across all three protocols. UCP models this as the user confirming checkout session contents. ACP models this as a buyer-confirmed checkout where the session reaches `ready_for_payment` before CompleteCheckout. AP2 defines detailed step-by-step interactions between UA/SA, ME, CP, MPP. VI focuses on the credential artifacts produced at each step | -| **Autonomous Mode** | 3-layer flow (L1 + L2 + L3). User sets constraints; agent acts independently within bounds | Agent-driven checkout (UCP) / Human-Not-Present transaction (AP2) | Agent-driven checkout | UCP models this as the agent managing the checkout session end-to-end. ACP models this as the agent creating and completing the session independently, with SPT constraining payment execution. AP2's Human-Not-Present flow uses Intent Mandate + potential fallback to Cart Mandate. VI adds L3 as an auditable fulfillment layer and typed constraints (quantitative are machine-enforceable; qualitative are informational) | +| **Autonomous Mode** | 3-layer flow (L1 + L2 + L3). User sets constraints; agent acts independently within bounds | Agent-driven checkout (UCP) / Human-Not-Present transaction (AP2) | Agent-driven checkout | UCP models this as the agent managing the checkout session end-to-end. ACP models this as the agent creating and completing the session independently, with SPT constraining payment execution. AP2 V2's Human-Not-Present flow is approval of **Open Checkout/Open Payment Mandates** (the Intent Mandate is the upstream human-present intent object). VI adds L3 as an auditable fulfillment layer and typed constraints (quantitative are machine-enforceable; qualitative are informational) | | **Delegation Chain** | Cryptographic chain linking Credential Provider -> User -> Agent via `cnf` claims at each layer | Implicit platform delegation (UCP) / Agent key delegation (AP2) | SPT `allowance` (loosely) | UCP delegates implicitly through platform identity; AP2 describes delegation conceptually. ACP's SPT scopes agent authority via amount/merchant/expiry, but constraints are set by the agent, not the buyer — a key semantic difference from VI's user-set constraints. VI implements delegation as a verifiable chain: L1 `cnf.jwk` = user key, L2 mandate `cnf.jwk` = agent key, L3 header `jwk` = agent key proof | | **Fallback to Immediate** | Merchant forces user confirmation when Intent Mandate is insufficient, converting to Immediate flow | `requires_escalation` status (UCP) / Merchant-forced confirmation (AP2) | `authentication_required` / `requires_escalation` state | Same concept across all three protocols. UCP uses the `requires_escalation` checkout status. ACP uses `authentication_required` or `requires_escalation` checkout states to signal that the seller forces buyer interaction. AP2 spec describes this as merchant requesting SA to bring user back into session | @@ -94,8 +98,11 @@ conveyed by the L2 `typ` header. Recurrence mapping note: recurrence is expressed in VI via the `mandate.payment.recurrence` and `mandate.payment.agent_recurrence` constraint types (not a top-level mandate field). VI uses ISO 20022 frequency codes -(e.g. `MNTH`, `WEEK`, `YEAR`), aligning with AP2's schema-level recurrence -typing. +(e.g. `MNTH`, `WEEK`, `YEAR`). Note this **differs** from AP2 V2, whose Open +Payment Mandate `agent_recurrence` uses a named enum +(`ON_DEMAND`, `DAILY`, `WEEKLY`, `BIWEEKLY`, `MONTHLY`, `QUARTERLY`, `ANNUALLY`); +only `ON_DEMAND` overlaps. Implementations bridging VI and AP2 V2 must map +between the two vocabularies. --- diff --git a/protocol-landscape/protocols.md b/protocol-landscape/protocols.md index a95e874..ed749de 100644 --- a/protocol-landscape/protocols.md +++ b/protocol-landscape/protocols.md @@ -3,10 +3,13 @@ How Verifiable Intent (VI) sits alongside existing agentic commerce protocols. > **Protocol versions**: Based on the Universal Commerce Protocol (UCP) specification -> as of 23 January 2026, the Agent Payments Protocol (AP2) specification as of -> 16 September 2025, and the Agentic Commerce Protocol (ACP) specification as of +> as of 23 January 2026, the Agent Payments Protocol (AP2) version 0.2.0 ("V2", +> 28 April 2026), and the Agentic Commerce Protocol (ACP) specification as of > 30 January 2026. Protocol details may change; statements about what each protocol -> "does not define" or "leaves open" are bounded by these versions. +> "does not define" or "leaves open" are bounded by these versions. Note: AP2 V2 +> (0.2.0) added an SD-JWT-based Human-Not-Present model with Open Checkout/Open +> Payment Mandates and `cnf` agent key binding, narrowing several gaps earlier +> attributed to AP2 0.1.0. --- diff --git a/python/README.md b/python/README.md new file mode 100644 index 0000000..60d8914 --- /dev/null +++ b/python/README.md @@ -0,0 +1,36 @@ +# Verifiable Intent — Python + +Reference implementation of the [Verifiable Intent](../README.md) credential format: a +layered SD-JWT delegation chain (Issuer → User → Agent) that produces cryptographic proof +an AI agent's commercial actions stayed within the scope a human explicitly delegated. + +This is the **Python** package of the Verifiable Intent monorepo. A byte-compatible +TypeScript port lives in [`../typescript`](../typescript). + +## Install + +```bash +pip install -e ".[dev]" +``` + +## Quick start + +```python +from verifiable_intent import ( + IssuerCredential, create_layer1, + UserMandate, MandateMode, create_layer2_immediate, + verify_chain, +) +``` + +See [`examples/`](examples) for full Immediate and Autonomous flows, and +[`../spec`](../spec) for the normative specification. + +## Develop + +```bash +ruff check src/ tests/ examples/ +pytest tests/ -v +``` + +The public API surface is locked by `tests/test_package_exports.py`. License: Apache-2.0. diff --git a/conftest.py b/python/conftest.py similarity index 100% rename from conftest.py rename to python/conftest.py diff --git a/examples/autonomous_flow.py b/python/examples/autonomous_flow.py similarity index 100% rename from examples/autonomous_flow.py rename to python/examples/autonomous_flow.py diff --git a/examples/constraint_checking.py b/python/examples/constraint_checking.py similarity index 100% rename from examples/constraint_checking.py rename to python/examples/constraint_checking.py diff --git a/examples/helpers.py b/python/examples/helpers.py similarity index 100% rename from examples/helpers.py rename to python/examples/helpers.py diff --git a/examples/immediate_flow.py b/python/examples/immediate_flow.py similarity index 100% rename from examples/immediate_flow.py rename to python/examples/immediate_flow.py diff --git a/examples/network_validation.py b/python/examples/network_validation.py similarity index 100% rename from examples/network_validation.py rename to python/examples/network_validation.py diff --git a/examples/selective_disclosure.py b/python/examples/selective_disclosure.py similarity index 100% rename from examples/selective_disclosure.py rename to python/examples/selective_disclosure.py diff --git a/pyproject.toml b/python/pyproject.toml similarity index 100% rename from pyproject.toml rename to python/pyproject.toml diff --git a/python/scripts/generate_vectors.py b/python/scripts/generate_vectors.py new file mode 100644 index 0000000..5a3e050 --- /dev/null +++ b/python/scripts/generate_vectors.py @@ -0,0 +1,705 @@ +"""Generate golden conformance vectors from the Python reference implementation. + +Snapshots one Immediate and one Autonomous flow with deterministic demo keys +and fixed timestamps/nonces. ECDSA P-256 signatures are randomized, so full +credentials are NOT reproducible by re-signing; instead we record every +*deterministic* artifact (disclosure encodings, hashes, payload JSON, +delegate_payload, _sd arrays, sd_hash inputs/outputs) plus the exact salts +consumed, so the TypeScript port can replay them byte-for-byte and the suite +can compare without depending on signature bytes. + +Run from the python/ dir: + uv run --no-project --with "cryptography>=42" python scripts/generate_vectors.py + +Output: /test-vectors/vectors.json +""" + +from __future__ import annotations + +import hashlib +import json +import sys +from pathlib import Path + +PY_ROOT = Path(__file__).resolve().parent.parent # python/ +REPO_ROOT = PY_ROOT.parent # repo root +sys.path.insert(0, str(PY_ROOT / "src")) +sys.path.insert(0, str(PY_ROOT / "examples")) + +import helpers # noqa: E402 + +import verifiable_intent as vi # noqa: E402 +from verifiable_intent.crypto.disclosure import ( # noqa: E402 + _b64url_encode, + build_selective_presentation, + create_disclosure, + hash_bytes, + hash_disclosure, +) +from verifiable_intent.crypto.sd_jwt import decode_sd_jwt # noqa: E402 +from verifiable_intent.crypto.signing import _jwt_encode # noqa: E402 +from verifiable_intent.verification.chain import SplitL3 # noqa: E402 + +# --------------------------------------------------------------------------- +# Deterministic scenario constants +# --------------------------------------------------------------------------- + +ISSUER = helpers.get_issuer_keys() +USER = helpers.get_user_keys() +AGENT = helpers.get_agent_keys() +MERCHANT = helpers.get_merchant_keys() + +PI = helpers.PAYMENT_INSTRUMENT +M1 = {"id": "merchant-uuid-1", "name": "Tennis Warehouse", "website": "https://tennis-warehouse.com"} +M2 = {"id": "merchant-uuid-2", "name": "Babolat", "website": "https://babolat.com"} +ITEM1 = {"id": "BAB86345", "title": "Babolat Pure Aero Tennis Racket"} +ITEM2 = {"id": "HEA23102", "title": "Head Graphene 360 Speed"} + +L1_IAT = 1_750_000_000 +L1_EXP = L1_IAT + 31_536_000 # ~1 year +CHK_IAT = 1_750_000_050 +L2_IAT = 1_750_000_100 +L3_IAT = 1_750_000_200 + + +def sd_jwt_record(sj) -> dict: + """Capture every deterministic artifact of an SdJwt (signature excluded).""" + return { + "header": sj.header, + "payload": sj.payload, + "header_b64": sj._raw_header_b64, + "payload_b64": sj._raw_payload_b64, + "issuer_jwt": sj.issuer_jwt, + "serialized": sj.serialize(), + "disclosures": sj.disclosures, + "salts": [dv[0] for dv in sj.disclosure_values], + "values": [dv[-1] for dv in sj.disclosure_values], + "sd_hashes_of_disclosures": [hash_disclosure(d) for d in sj.disclosures], + } + + +def find_disclosure(sj, predicate) -> str: + for s, dv in zip(sj.disclosures, sj.disclosure_values): + if predicate(dv[-1]): + return s + raise ValueError("disclosure not found") + + +# --------------------------------------------------------------------------- +# Layer 1 (shared by both flows) +# --------------------------------------------------------------------------- + +l1_cred = vi.IssuerCredential( + iss="https://issuer.mastercard.com", + sub="user-subject-12345", + iat=L1_IAT, + exp=L1_EXP, + cnf_jwk=USER.public_jwk, + pan_last_four="1234", + scheme="mastercard", + card_id=PI["id"], + email="user@example.com", +) +l1 = vi.create_layer1(l1_cred, ISSUER.private_key, kid=ISSUER.kid) +l1_serialized = l1.serialize() +l1_sd_hash = hash_bytes(l1_serialized.encode("ascii")) + +# --------------------------------------------------------------------------- +# Merchant checkout JWT (fixed timestamps; signature still random) +# --------------------------------------------------------------------------- + +checkout_payload = { + "iss": "https://tennis-warehouse.com", + "sub": "cart_checkout", + "iat": CHK_IAT, + "exp": CHK_IAT + 3600, + "cart": { + "items": [ + { + "sku": "BAB86345", + "name": "Babolat Pure Aero Tennis Racket", + "size": 3, + "size_label": "4 3/8", + "color": "white", + "quantity": 1, + "unitPrice": 279.99, + } + ], + "subTotal": {"amount": 279.99, "currencyCode": "USD"}, + }, +} +checkout_jwt = _jwt_encode( + {"alg": "ES256", "typ": "JWT", "kid": MERCHANT.kid}, + checkout_payload, + MERCHANT.private_key, +) +checkout_hash = _b64url_encode(hashlib.sha256(checkout_jwt.encode("ascii")).digest()) + +# --------------------------------------------------------------------------- +# Immediate flow: L2 with final values +# --------------------------------------------------------------------------- + +imm_mandate = vi.UserMandate( + nonce="immediate-nonce-0001", + aud="https://agent.example.com", + iat=L2_IAT, + mode=vi.MandateMode.IMMEDIATE, + iss="https://wallet.example.com", + exp=L2_IAT + 900, + sd_hash=l1_sd_hash, + checkout_mandate=vi.CheckoutMandate(vct="mandate.checkout.1", checkout_jwt=checkout_jwt), + payment_mandate=vi.PaymentMandate( + vct="mandate.payment.1", + payment_instrument=PI, + payee=M1, + currency="USD", + amount=27999, + ), +) +l2_imm = vi.create_layer2_immediate(imm_mandate, USER.private_key, kid=USER.kid).sd_jwt + +# --------------------------------------------------------------------------- +# Autonomous flow: L2 open mandates with constraints + nested disclosures +# --------------------------------------------------------------------------- + +auto_mandate = vi.UserMandate( + nonce="autonomous-nonce-0001", + aud="https://agent.example.com", + iat=L2_IAT, + mode=vi.MandateMode.AUTONOMOUS, + iss="https://wallet.example.com", + exp=L2_IAT + 86_400, + sd_hash=l1_sd_hash, + merchants=[M1, M2], + acceptable_items=[ITEM1, ITEM2], + checkout_mandate=vi.CheckoutMandate( + vct="mandate.checkout.open.1", + cnf_jwk=AGENT.public_jwk, + cnf_kid=AGENT.kid, + constraints=[ + vi.AllowedMerchantConstraint(allowed=[dict(M1), dict(M2)]), + vi.CheckoutLineItemsConstraint( + items=[{"id": "line-1", "acceptable_items": [dict(ITEM1), dict(ITEM2)], "quantity": 1}] + ), + ], + ), + payment_mandate=vi.PaymentMandate( + vct="mandate.payment.open.1", + cnf_jwk=AGENT.public_jwk, + cnf_kid=AGENT.kid, + constraints=[ + vi.AllowedPayeeConstraint(allowed=[dict(M1)]), + vi.PaymentAmountConstraint(currency="USD", min=10000, max=40000), + ], + payment_instrument=PI, + ), +) +l2_auto = vi.create_layer2_autonomous(auto_mandate, USER.private_key, kid=USER.kid) + +# Locate the L2 disclosures each L3 binds to. +payment_disc = find_disclosure(l2_auto, lambda v: isinstance(v, dict) and v.get("vct") == "mandate.payment.open.1") +checkout_disc = find_disclosure(l2_auto, lambda v: isinstance(v, dict) and v.get("vct") == "mandate.checkout.open.1") +merchant_disc = find_disclosure(l2_auto, lambda v: isinstance(v, dict) and v.get("id") == "merchant-uuid-1" and "website" in v) +item_disc = find_disclosure(l2_auto, lambda v: isinstance(v, dict) and v.get("id") == "BAB86345" and "title" in v) + +# --------------------------------------------------------------------------- +# Split-agent attack fixture: an autonomous L2 whose payment mandate is bound to +# a DIFFERENT agent key than its checkout mandate. The chain MUST be rejected +# (cnf.jwk must be identical across the mandate pair). Mirrors the Python +# reference test (tests/test_verification_hardening.py +# TestDualMandateCnfCrossCheck) so the TypeScript port inherits the same +# fail-closed coverage via the autonomous_split_agent chain scenario below. +split_agent_mandate = vi.UserMandate( + nonce="autonomous-split-agent-0001", + aud="https://agent.example.com", + iat=L2_IAT, + mode=vi.MandateMode.AUTONOMOUS, + iss="https://wallet.example.com", + exp=L2_IAT + 86_400, + sd_hash=l1_sd_hash, + merchants=[M1, M2], + acceptable_items=[ITEM1, ITEM2], + checkout_mandate=vi.CheckoutMandate( + vct="mandate.checkout.open.1", + cnf_jwk=AGENT.public_jwk, + cnf_kid=AGENT.kid, + constraints=[ + vi.AllowedMerchantConstraint(allowed=[dict(M1), dict(M2)]), + vi.CheckoutLineItemsConstraint( + items=[{"id": "line-1", "acceptable_items": [dict(ITEM1), dict(ITEM2)], "quantity": 1}] + ), + ], + ), + payment_mandate=vi.PaymentMandate( + vct="mandate.payment.open.1", + cnf_jwk=MERCHANT.public_jwk, # different key than the checkout mandate's cnf + cnf_kid=AGENT.kid, + constraints=[ + vi.AllowedPayeeConstraint(allowed=[dict(M1)]), + vi.PaymentAmountConstraint(currency="USD", min=10000, max=40000), + ], + payment_instrument=PI, + ), +) +l2_split_agent = vi.create_layer2_autonomous(split_agent_mandate, USER.private_key, kid=USER.kid) + +# --------------------------------------------------------------------------- +# Autonomous flow: L3a (payment -> network) and L3b (checkout -> merchant) +# --------------------------------------------------------------------------- + +l3a_mandate = vi.PaymentL3Mandate( + nonce="l3a-nonce-0001", + aud="https://network.mastercard.com", + iat=L3_IAT, + iss="https://agent.example.com", + exp=L3_IAT + 300, + final_payment=vi.FinalPaymentMandate( + transaction_id=checkout_hash, + payee=dict(M1), + payment_amount={"currency": "USD", "amount": 27999}, + payment_instrument=PI, + ), + final_merchant=dict(M1), +) +l3a = vi.create_layer3_payment(l3a_mandate, AGENT.private_key, l2_auto.issuer_jwt, payment_disc, merchant_disc, kid=AGENT.kid) + +l3b_mandate = vi.CheckoutL3Mandate( + nonce="l3b-nonce-0001", + aud="https://tennis-warehouse.com", + iat=L3_IAT, + iss="https://agent.example.com", + exp=L3_IAT + 300, + final_checkout=vi.FinalCheckoutMandate(checkout_jwt=checkout_jwt, checkout_hash=checkout_hash), +) +l3b = vi.create_layer3_checkout(l3b_mandate, AGENT.private_key, l2_auto.issuer_jwt, checkout_disc, item_disc, kid=AGENT.kid) + +l3a_presentation = build_selective_presentation(l2_auto.issuer_jwt, [payment_disc, merchant_disc]) +l3b_presentation = build_selective_presentation(l2_auto.issuer_jwt, [checkout_disc, item_disc]) + +# --------------------------------------------------------------------------- +# Self-verify (sanity; ensures emitted vectors are a known-good chain) +# +# Timestamps are pinned to a fixed instant so the deterministic snapshot is +# stable. verify_chain reads the wall clock internally, so we pin time.time() +# to a moment inside every layer's validity window for the duration of the +# self-check only (the reference implementation itself is left untouched). +# --------------------------------------------------------------------------- + +import time as _time # noqa: E402 + +VERIFY_NOW = L3_IAT + 60 # valid for L1, both L2 variants, and L3 (exp-iat<=3600) +_orig_time = _time.time +_time.time = lambda: VERIFY_NOW + +verification = {"verify_now": VERIFY_NOW} + +imm_result = vi.verify_chain( + l1, + l2_imm, + issuer_public_key=ISSUER.public_key, + l1_serialized=l1_serialized, + l2_serialized=l2_imm.serialize(), + expected_l2_aud="https://agent.example.com", + expected_l2_nonce="immediate-nonce-0001", +) +verification["immediate_valid"] = imm_result.valid +verification["immediate_errors"] = imm_result.errors + +try: + auto_result = vi.verify_chain( + l1, + l2_auto, + issuer_public_key=ISSUER.public_key, + l1_serialized=l1_serialized, + l2_serialized=l2_auto.serialize(), + split_l3s=[ + SplitL3( + l3_payment=l3a, + l3_checkout=l3b, + l2_payment_serialized=l3a_presentation, + l2_checkout_serialized=l3b_presentation, + ) + ], + expected_l2_aud="https://agent.example.com", + expected_l2_nonce="autonomous-nonce-0001", + expected_l3_payment_aud="https://network.mastercard.com", + expected_l3_payment_nonce="l3a-nonce-0001", + expected_l3_checkout_aud="https://tennis-warehouse.com", + expected_l3_checkout_nonce="l3b-nonce-0001", + ) + verification["autonomous_valid"] = auto_result.valid + verification["autonomous_errors"] = auto_result.errors +except Exception as e: # pragma: no cover - diagnostic only + verification["autonomous_valid"] = False + verification["autonomous_errors"] = [f"{type(e).__name__}: {e}"] +finally: + _time.time = _orig_time + +# --------------------------------------------------------------------------- +# Verification conformance scenarios (chain / constraints / integrity) +# Each scenario records the exact result of the Python verifier so the +# TypeScript port can be asserted against it (clock pinned to current_time). +# --------------------------------------------------------------------------- + + +def tamper_signature(serialized: str) -> str: + """Flip one character of the issuer JWT signature (breaks the signature).""" + jwt, sep, rest = serialized.partition("~") + h, p, sig = jwt.split(".") + flipped = ("B" if sig[0] != "B" else "C") + sig[1:] + return f"{h}.{p}.{flipped}{sep}{rest}" + + +L2_IMM_SER = l2_imm.serialize() +L2_AUTO_SER = l2_auto.serialize() +L2_SPLIT_AGENT_SER = l2_split_agent.serialize() +L3A_SER = l3a.serialize() +L3B_SER = l3b.serialize() + +_chain_scenarios = [] + + +def chain_scenario(name, *, l1s=l1_serialized, l2s, l3p=None, l3c=None, l2p=None, l2c=None, + issuer_pub=ISSUER.public_jwk, skip_issuer=False, current=VERIFY_NOW, **params): + _time.time = lambda: current + try: + kwargs = dict( + issuer_public_key=(ISSUER.public_key if issuer_pub is not None else None), + skip_issuer_verification=skip_issuer, + l1_serialized=l1s, + l2_serialized=l2s, + ) + kwargs.update(params) + if l3p is not None or l3c is not None: + kwargs["split_l3s"] = [ + SplitL3( + l3_payment=decode_sd_jwt(l3p) if l3p else None, + l3_checkout=decode_sd_jwt(l3c) if l3c else None, + l2_payment_serialized=l2p, + l2_checkout_serialized=l2c, + ) + ] + res = vi.verify_chain(decode_sd_jwt(l1s), decode_sd_jwt(l2s), **kwargs) + finally: + _time.time = _orig_time + _chain_scenarios.append({ + "name": name, + "current_time": current, + "issuer_public": issuer_pub, + "skip_issuer_verification": skip_issuer, + "l1": l1s, + "l2": l2s, + "l3_payment": l3p, + "l3_checkout": l3c, + "l2_payment_serialized": l2p, + "l2_checkout_serialized": l2c, + "expected_l2_aud": params.get("expected_l2_aud"), + "expected_l2_nonce": params.get("expected_l2_nonce"), + "expected_l3_payment_aud": params.get("expected_l3_payment_aud"), + "expected_l3_payment_nonce": params.get("expected_l3_payment_nonce"), + "expected_l3_checkout_aud": params.get("expected_l3_checkout_aud"), + "expected_l3_checkout_nonce": params.get("expected_l3_checkout_nonce"), + "expected_valid": res.valid, + "expected_errors": res.errors, + }) + + +# Immediate +chain_scenario("immediate_valid", l2s=L2_IMM_SER, expected_l2_aud="https://agent.example.com", expected_l2_nonce="immediate-nonce-0001") +chain_scenario("immediate_wrong_aud", l2s=L2_IMM_SER, expected_l2_aud="https://attacker.example.com") +chain_scenario("immediate_wrong_nonce", l2s=L2_IMM_SER, expected_l2_nonce="wrong-nonce") +chain_scenario("immediate_expired", l2s=L2_IMM_SER, current=VERIFY_NOW + 1_000_000) +chain_scenario("immediate_iat_future", l2s=L2_IMM_SER, current=L1_IAT - 1_000_000) +chain_scenario("immediate_tampered_l1", l1s=tamper_signature(l1_serialized), l2s=L2_IMM_SER) +chain_scenario("immediate_missing_issuer_key", l2s=L2_IMM_SER, issuer_pub=None) + +# RFC 9901 section 7.1: an L1 whose _sd carries a duplicated digest must be rejected. +_dup_l1_payload = dict(l1.payload) +_l1_sd = _dup_l1_payload.get("_sd") +if isinstance(_l1_sd, list) and _l1_sd: + _dup_l1_payload["_sd"] = list(_l1_sd) + [_l1_sd[0]] +_dup_l1_jwt = _jwt_encode(l1.header, _dup_l1_payload, ISSUER.private_key) +_dup_l1_ser = "~".join([_dup_l1_jwt] + list(l1.disclosures)) + "~" +chain_scenario("immediate_duplicate_l1_sd", l1s=_dup_l1_ser, l2s=L2_IMM_SER) + +# Autonomous +chain_scenario( + "autonomous_valid", l2s=L2_AUTO_SER, l3p=L3A_SER, l3c=L3B_SER, l2p=l3a_presentation, l2c=l3b_presentation, + expected_l2_aud="https://agent.example.com", expected_l2_nonce="autonomous-nonce-0001", + expected_l3_payment_aud="https://network.mastercard.com", expected_l3_payment_nonce="l3a-nonce-0001", + expected_l3_checkout_aud="https://tennis-warehouse.com", expected_l3_checkout_nonce="l3b-nonce-0001", +) +chain_scenario("autonomous_swapped_l3", l2s=L2_AUTO_SER, l3p=L3A_SER, l3c=L3B_SER, l2p=l3b_presentation, l2c=l3a_presentation) +chain_scenario( + "autonomous_wrong_l3_aud", l2s=L2_AUTO_SER, l3p=L3A_SER, l3c=L3B_SER, l2p=l3a_presentation, l2c=l3b_presentation, + expected_l3_payment_aud="https://attacker.example.com", +) +chain_scenario("autonomous_l3_expired", l2s=L2_AUTO_SER, l3p=L3A_SER, l3c=L3B_SER, l2p=l3a_presentation, l2c=l3b_presentation, current=VERIFY_NOW + 1_000_000) +chain_scenario("autonomous_skip_issuer_ok", l2s=L2_AUTO_SER, l3p=L3A_SER, l3c=L3B_SER, l2p=l3a_presentation, l2c=l3b_presentation, issuer_pub=None, skip_issuer=True) +# Split-agent attack: payment and checkout mandates bound to different agent keys → reject. +chain_scenario("autonomous_split_agent", l2s=L2_SPLIT_AGENT_SER) + +_constraint_cases = [] + + +def cc(name, constraints, fulfillment, mode, is_open=False): + r = vi.check_constraints(constraints, fulfillment, mode=mode, is_open_mandate=is_open) + _constraint_cases.append({ + "name": name, + "constraints": constraints, + "fulfillment": fulfillment, + "mode": mode.value, + "is_open_mandate": is_open, + "expected": { + "satisfied": r.satisfied, + "violations": len(r.violations), + "checked": len(r.checked), + "skipped": len(r.skipped), + }, + }) + + +_AMT = vi.PaymentAmountConstraint(currency="USD", min=10000, max=40000).to_dict() +cc("amount_ok", [_AMT], {"payment_amount": {"currency": "USD", "amount": 27999}}, vi.StrictnessMode.PERMISSIVE) +cc("amount_over_max", [_AMT], {"payment_amount": {"currency": "USD", "amount": 50000}}, vi.StrictnessMode.PERMISSIVE) +cc("amount_under_min", [_AMT], {"payment_amount": {"currency": "USD", "amount": 5000}}, vi.StrictnessMode.PERMISSIVE) +cc("amount_wrong_currency", [_AMT], {"payment_amount": {"currency": "EUR", "amount": 27999}}, vi.StrictnessMode.PERMISSIVE) + +_PAYEE = vi.AllowedPayeeConstraint(allowed=[dict(M1)]).to_dict() +cc("payee_ok", [_PAYEE], {"payee": dict(M1)}, vi.StrictnessMode.PERMISSIVE) +cc("payee_not_allowed", [_PAYEE], {"payee": dict(M2)}, vi.StrictnessMode.PERMISSIVE) + +_LI = vi.CheckoutLineItemsConstraint(items=[{"id": "line-1", "acceptable_items": [dict(ITEM1)], "quantity": 2}]).to_dict() +cc("line_items_ok", [_LI], {"line_items": [{"id": "BAB86345", "quantity": 1}]}, vi.StrictnessMode.PERMISSIVE) +cc("line_items_over_qty", [_LI], {"line_items": [{"id": "BAB86345", "quantity": 5}]}, vi.StrictnessMode.PERMISSIVE) +cc("line_items_wrong_id", [_LI], {"line_items": [{"id": "ZZZ99999", "quantity": 1}]}, vi.StrictnessMode.PERMISSIVE) + +_UNK = [{"type": "mandate.custom.foo", "bar": 1}] +cc("unknown_permissive", _UNK, {}, vi.StrictnessMode.PERMISSIVE) +cc("unknown_strict", _UNK, {}, vi.StrictnessMode.STRICT) +cc("unknown_open_rejected", _UNK, {}, vi.StrictnessMode.PERMISSIVE, is_open=True) + +_integrity_cases = [] +_chk = {"vct": "mandate.checkout.1", "checkout_jwt": checkout_jwt, "checkout_hash": checkout_hash} +_pay = {"vct": "mandate.payment.1", "transaction_id": checkout_hash} +_integrity_cases.append({"name": "checkout_hash_ok", "kind": "checkout_hash", "checkout_mandate": _chk, "payment_mandate": _pay, "expected_valid": vi.verify_checkout_hash_binding(_chk, _pay)[0]}) +_bad_pay = {"vct": "mandate.payment.1", "transaction_id": "WRONGHASH"} +_integrity_cases.append({"name": "checkout_hash_mismatch", "kind": "checkout_hash", "checkout_mandate": _chk, "payment_mandate": _bad_pay, "expected_valid": vi.verify_checkout_hash_binding(_chk, _bad_pay)[0]}) +_l3a_c = {"delegate_payload": [{"vct": "mandate.payment.1", "transaction_id": checkout_hash}]} +_l3b_c = {"delegate_payload": [{"vct": "mandate.checkout.1", "checkout_hash": checkout_hash}]} +_integrity_cases.append({"name": "l3_xref_ok", "kind": "l3_xref", "l3_payment_claims": _l3a_c, "l3_checkout_claims": _l3b_c, "expected_valid": vi.verify_l3_cross_reference(_l3a_c, _l3b_c)[0]}) +_l3b_bad = {"delegate_payload": [{"vct": "mandate.checkout.1", "checkout_hash": "DIFFERENT"}]} +_integrity_cases.append({"name": "l3_xref_mismatch", "kind": "l3_xref", "l3_payment_claims": _l3a_c, "l3_checkout_claims": _l3b_bad, "expected_valid": vi.verify_l3_cross_reference(_l3a_c, _l3b_bad)[0]}) +# L2 reference binding: conditional_transaction_id == hash of the checkout disclosure. +_l2ref_pay = {"vct": "mandate.payment.open.1", "constraints": [{"type": "mandate.payment.reference", "conditional_transaction_id": hash_disclosure(checkout_disc)}]} +_integrity_cases.append({"name": "l2_reference_ok", "kind": "l2_ref", "checkout_disclosure": checkout_disc, "payment_mandate": _l2ref_pay, "expected_valid": vi.verify_l2_reference_binding({}, _l2ref_pay, checkout_disc)[0]}) +_l2ref_bad = {"vct": "mandate.payment.open.1", "constraints": [{"type": "mandate.payment.reference", "conditional_transaction_id": "WRONGREF"}]} +_integrity_cases.append({"name": "l2_reference_mismatch", "kind": "l2_ref", "checkout_disclosure": checkout_disc, "payment_mandate": _l2ref_bad, "expected_valid": vi.verify_l2_reference_binding({}, _l2ref_bad, checkout_disc)[0]}) + +verification_conformance = { + "chain_scenarios": _chain_scenarios, + "constraint_cases": _constraint_cases, + "integrity_cases": _integrity_cases, +} + +# --------------------------------------------------------------------------- +# Primitive vectors (independent of any flow) +# --------------------------------------------------------------------------- + +prim_disclosures = [] +for claim_name, value, salt in [ + ("email", "user@example.com", "AAAAAAAAAAAAAAAAAAAAAA"), + (None, dict(M1), "BBBBBBBBBBBBBBBBBBBBBB"), + (None, dict(ITEM1), "CCCCCCCCCCCCCCCCCCCCCC"), + (None, {"vct": "mandate.payment.1", "payment_amount": {"currency": "USD", "amount": 27999}}, "DDDDDDDDDDDDDDDDDDDDDD"), +]: + d = create_disclosure(claim_name, value, salt=salt) + prim_disclosures.append( + {"claim_name": claim_name, "value": value, "salt": salt, "disclosure": d, "hash": hash_disclosure(d)} + ) + +prim_hashes = [ + {"input_ascii": s, "hash": hash_bytes(s.encode("ascii"))} + for s in ["", "hello", "abc~def~", checkout_jwt, l1_serialized] +] + +prim_b64url = [ + {"bytes_hex": bytes(b).hex(), "b64url": _b64url_encode(bytes(b))} + for b in [b"", b"\x00", b"\x00\x01\x02\x03", b"hello world", bytes(range(20))] +] + +# --------------------------------------------------------------------------- +# Model vectors (to_dict / to_payload JSON, byte-exact) +# --------------------------------------------------------------------------- + + +def jdump(obj) -> str: + return json.dumps(obj, separators=(",", ":")) + + +constraint_models = [ + ("AllowedMerchantConstraint", vi.AllowedMerchantConstraint(allowed=[dict(M1), dict(M2)]).to_dict()), + ( + "CheckoutLineItemsConstraint", + vi.CheckoutLineItemsConstraint( + items=[{"id": "line-1", "acceptable_items": [dict(ITEM1)], "quantity": 2}], match_mode="exact" + ).to_dict(), + ), + ("AllowedPayeeConstraint", vi.AllowedPayeeConstraint(allowed=[dict(M1)]).to_dict()), + ("PaymentAmountConstraint", vi.PaymentAmountConstraint(currency="USD", min=10000, max=40000).to_dict()), + ("PaymentAmountConstraint_max_only", vi.PaymentAmountConstraint(currency="EUR", max=5000).to_dict()), + ("ReferenceConstraint", vi.ReferenceConstraint(conditional_transaction_id="deadbeef").to_dict()), + ("PaymentBudgetConstraint", vi.PaymentBudgetConstraint(currency="USD", max=100000, min=500).to_dict()), + ( + "PaymentRecurrenceConstraint", + vi.PaymentRecurrenceConstraint(frequency="MNTH", start_date="2026-01-01", end_date="2026-12-31", number=12).to_dict(), + ), + ( + "AgentRecurrenceConstraint", + vi.AgentRecurrenceConstraint(frequency="WEEK", start_date="2026-01-01", end_date="2026-06-30", max_occurrences=26).to_dict(), + ), +] +model_constraints = [{"name": n, "dict": d, "json": jdump(d)} for n, d in constraint_models] + +model_misc = { + "issuer_credential_to_payload": {"dict": l1_cred.to_payload(), "json": jdump(l1_cred.to_payload())}, + "final_payment_mandate": { + "dict": vi.FinalPaymentMandate( + transaction_id=checkout_hash, payee=dict(M1), payment_amount={"currency": "USD", "amount": 27999}, payment_instrument=PI + ).to_dict() + }, + "final_checkout_mandate": { + "dict": vi.FinalCheckoutMandate(checkout_jwt=checkout_jwt, checkout_hash=checkout_hash).to_dict() + }, +} + +# --------------------------------------------------------------------------- +# Assemble & write +# --------------------------------------------------------------------------- + +vectors = { + "_meta": { + "description": "Golden conformance vectors from the Python verifiable-intent reference implementation.", + "note": "ECDSA signatures are randomized; compare deterministic artifacts (disclosures, hashes, payload JSON, delegate_payload, _sd, sd_hash) — never signature bytes.", + "vi_version": vi.__version__, + "timestamps": {"l1_iat": L1_IAT, "l1_exp": L1_EXP, "checkout_iat": CHK_IAT, "l2_iat": L2_IAT, "l3_iat": L3_IAT}, + }, + "keys": { + "issuer": {"kid": ISSUER.kid, "public": ISSUER.public_jwk, "private": ISSUER.private_jwk}, + "user": {"kid": USER.kid, "public": USER.public_jwk, "private": USER.private_jwk}, + "agent": {"kid": AGENT.kid, "public": AGENT.public_jwk, "private": AGENT.private_jwk}, + "merchant": {"kid": MERCHANT.kid, "public": MERCHANT.public_jwk, "private": MERCHANT.private_jwk}, + }, + "primitives": {"b64url": prim_b64url, "disclosures": prim_disclosures, "hash_bytes": prim_hashes}, + "models": {"constraints": model_constraints, "misc": model_misc}, + "shared": { + "checkout_jwt": checkout_jwt, + "checkout_hash": checkout_hash, + "l1_serialized": l1_serialized, + "l1_sd_hash": l1_sd_hash, + "l1": sd_jwt_record(l1), + "l1_credential": { + "iss": l1_cred.iss, + "sub": l1_cred.sub, + "iat": l1_cred.iat, + "exp": l1_cred.exp, + "vct": l1_cred.vct, + "cnf_jwk": l1_cred.cnf_jwk, + "pan_last_four": l1_cred.pan_last_four, + "scheme": l1_cred.scheme, + "card_id": l1_cred.card_id, + "email": l1_cred.email, + }, + }, + "immediate": { + "inputs": { + "nonce": "immediate-nonce-0001", + "aud": "https://agent.example.com", + "iat": L2_IAT, + "iss": "https://wallet.example.com", + "exp": L2_IAT + 900, + "sd_hash": l1_sd_hash, + "kid": USER.kid, + "checkout_mandate": {"vct": "mandate.checkout.1", "checkout_jwt": checkout_jwt}, + "payment_mandate": { + "vct": "mandate.payment.1", + "payment_instrument": PI, + "payee": M1, + "currency": "USD", + "amount": 27999, + }, + }, + "checkout_mandate_dict": imm_mandate.checkout_mandate.to_dict(), + "payment_mandate_dict": imm_mandate.payment_mandate.to_dict(), + "l2": sd_jwt_record(l2_imm), + "delegate_payload": l2_imm.payload["delegate_payload"], + }, + "autonomous": { + "inputs": { + "nonce": "autonomous-nonce-0001", + "aud": "https://agent.example.com", + "iat": L2_IAT, + "iss": "https://wallet.example.com", + "exp": L2_IAT + 86_400, + "sd_hash": l1_sd_hash, + "kid": USER.kid, + "agent_cnf_jwk": AGENT.public_jwk, + "agent_cnf_kid": AGENT.kid, + "merchants": [M1, M2], + "acceptable_items": [ITEM1, ITEM2], + }, + "l2": sd_jwt_record(l2_auto), + "disclosure_refs": { + "payment_disc": payment_disc, + "checkout_disc": checkout_disc, + "merchant_disc": merchant_disc, + "item_disc": item_disc, + }, + "l3a": { + "inputs": { + "nonce": "l3a-nonce-0001", + "aud": "https://network.mastercard.com", + "iat": L3_IAT, + "iss": "https://agent.example.com", + "exp": L3_IAT + 300, + "kid": AGENT.kid, + "l2_base_jwt": l2_auto.issuer_jwt, + "payment_disclosure": payment_disc, + "merchant_disclosure": merchant_disc, + "final_payment": l3a_mandate.final_payment.to_dict(), + "final_merchant": dict(M1), + }, + "selective_presentation": l3a_presentation, + "sd_hash": l3a.payload["sd_hash"], + "credential": sd_jwt_record(l3a), + }, + "l3b": { + "inputs": { + "nonce": "l3b-nonce-0001", + "aud": "https://tennis-warehouse.com", + "iat": L3_IAT, + "iss": "https://agent.example.com", + "exp": L3_IAT + 300, + "kid": AGENT.kid, + "l2_base_jwt": l2_auto.issuer_jwt, + "checkout_disclosure": checkout_disc, + "item_disclosure": item_disc, + "final_checkout": l3b_mandate.final_checkout.to_dict(), + }, + "selective_presentation": l3b_presentation, + "sd_hash": l3b.payload["sd_hash"], + "credential": sd_jwt_record(l3b), + }, + }, + "verification": verification, + "verification_conformance": verification_conformance, +} + +out_dir = REPO_ROOT / "test-vectors" +out_dir.mkdir(exist_ok=True) +out_path = out_dir / "vectors.json" +out_path.write_text(json.dumps(vectors, indent=2, ensure_ascii=False) + "\n", encoding="utf-8") + +print(f"Wrote {out_path.relative_to(REPO_ROOT)} ({out_path.stat().st_size} bytes)") +print(f" immediate verify : valid={verification.get('immediate_valid')} errors={verification.get('immediate_errors')}") +print(f" autonomous verify: valid={verification.get('autonomous_valid')} errors={verification.get('autonomous_errors')}") diff --git a/src/verifiable_intent/__init__.py b/python/src/verifiable_intent/__init__.py similarity index 100% rename from src/verifiable_intent/__init__.py rename to python/src/verifiable_intent/__init__.py diff --git a/src/verifiable_intent/crypto/__init__.py b/python/src/verifiable_intent/crypto/__init__.py similarity index 100% rename from src/verifiable_intent/crypto/__init__.py rename to python/src/verifiable_intent/crypto/__init__.py diff --git a/src/verifiable_intent/crypto/disclosure.py b/python/src/verifiable_intent/crypto/disclosure.py similarity index 100% rename from src/verifiable_intent/crypto/disclosure.py rename to python/src/verifiable_intent/crypto/disclosure.py diff --git a/src/verifiable_intent/crypto/jcs.py b/python/src/verifiable_intent/crypto/jcs.py similarity index 100% rename from src/verifiable_intent/crypto/jcs.py rename to python/src/verifiable_intent/crypto/jcs.py diff --git a/src/verifiable_intent/crypto/kb_sd_jwt.py b/python/src/verifiable_intent/crypto/kb_sd_jwt.py similarity index 100% rename from src/verifiable_intent/crypto/kb_sd_jwt.py rename to python/src/verifiable_intent/crypto/kb_sd_jwt.py diff --git a/src/verifiable_intent/crypto/sd_jwt.py b/python/src/verifiable_intent/crypto/sd_jwt.py similarity index 100% rename from src/verifiable_intent/crypto/sd_jwt.py rename to python/src/verifiable_intent/crypto/sd_jwt.py diff --git a/src/verifiable_intent/crypto/signing.py b/python/src/verifiable_intent/crypto/signing.py similarity index 100% rename from src/verifiable_intent/crypto/signing.py rename to python/src/verifiable_intent/crypto/signing.py diff --git a/src/verifiable_intent/issuance/__init__.py b/python/src/verifiable_intent/issuance/__init__.py similarity index 100% rename from src/verifiable_intent/issuance/__init__.py rename to python/src/verifiable_intent/issuance/__init__.py diff --git a/src/verifiable_intent/issuance/agent.py b/python/src/verifiable_intent/issuance/agent.py similarity index 100% rename from src/verifiable_intent/issuance/agent.py rename to python/src/verifiable_intent/issuance/agent.py diff --git a/src/verifiable_intent/issuance/issuer.py b/python/src/verifiable_intent/issuance/issuer.py similarity index 100% rename from src/verifiable_intent/issuance/issuer.py rename to python/src/verifiable_intent/issuance/issuer.py diff --git a/src/verifiable_intent/issuance/user.py b/python/src/verifiable_intent/issuance/user.py similarity index 100% rename from src/verifiable_intent/issuance/user.py rename to python/src/verifiable_intent/issuance/user.py diff --git a/src/verifiable_intent/models/__init__.py b/python/src/verifiable_intent/models/__init__.py similarity index 100% rename from src/verifiable_intent/models/__init__.py rename to python/src/verifiable_intent/models/__init__.py diff --git a/src/verifiable_intent/models/agent_mandate.py b/python/src/verifiable_intent/models/agent_mandate.py similarity index 100% rename from src/verifiable_intent/models/agent_mandate.py rename to python/src/verifiable_intent/models/agent_mandate.py diff --git a/src/verifiable_intent/models/cart.py b/python/src/verifiable_intent/models/cart.py similarity index 100% rename from src/verifiable_intent/models/cart.py rename to python/src/verifiable_intent/models/cart.py diff --git a/src/verifiable_intent/models/constraints.py b/python/src/verifiable_intent/models/constraints.py similarity index 100% rename from src/verifiable_intent/models/constraints.py rename to python/src/verifiable_intent/models/constraints.py diff --git a/src/verifiable_intent/models/issuer_credential.py b/python/src/verifiable_intent/models/issuer_credential.py similarity index 100% rename from src/verifiable_intent/models/issuer_credential.py rename to python/src/verifiable_intent/models/issuer_credential.py diff --git a/src/verifiable_intent/models/user_mandate.py b/python/src/verifiable_intent/models/user_mandate.py similarity index 100% rename from src/verifiable_intent/models/user_mandate.py rename to python/src/verifiable_intent/models/user_mandate.py diff --git a/src/verifiable_intent/verification/__init__.py b/python/src/verifiable_intent/verification/__init__.py similarity index 100% rename from src/verifiable_intent/verification/__init__.py rename to python/src/verifiable_intent/verification/__init__.py diff --git a/src/verifiable_intent/verification/chain.py b/python/src/verifiable_intent/verification/chain.py similarity index 97% rename from src/verifiable_intent/verification/chain.py rename to python/src/verifiable_intent/verification/chain.py index 160bdbe..f9af160 100644 --- a/src/verifiable_intent/verification/chain.py +++ b/python/src/verifiable_intent/verification/chain.py @@ -42,6 +42,24 @@ def _is_expired(exp_value, now: int, skew: int) -> bool | None: return now > exp_value + skew +def _duplicate_sd_digest(payload: dict) -> str | None: + """RFC 9901 section 7.1: a digest MUST NOT appear more than once in `_sd`. + + Returns the first duplicate digest found, or None. A repeated digest is an + anti-ambiguity / anti-smuggling violation; valid credentials never carry one. + """ + sd = payload.get("_sd") + if not isinstance(sd, list): + return None + seen: set[str] = set() + for h in sd: + if isinstance(h, str): + if h in seen: + return h + seen.add(h) + return None + + def _is_future_dated(iat_value, now: int, skew: int) -> bool | None: """Check if an iat claim is in the future. Returns None if iat is absent.""" if iat_value is None: @@ -217,6 +235,12 @@ def verify_chain( result.errors.append(f"L1 _sd_alg must be 'sha-256', got '{l1_sd_alg}'") return result + # 1b-bis. RFC 9901 section 7.1: reject duplicate disclosure digests in _sd + _l1_dup = _duplicate_sd_digest(l1.payload) + if _l1_dup is not None: + result.errors.append(f"L1 _sd contains a duplicate disclosure digest (RFC 9901 section 7.1): {_l1_dup}") + return result + # 2. Check L1 expiration l1_exp = l1.payload.get("exp") if _is_expired(l1_exp, now, clock_skew_seconds): @@ -269,6 +293,12 @@ def verify_chain( result.errors.append(f"L2 _sd_alg must be 'sha-256', got '{l2_sd_alg}'") return result + # 4a2-bis. RFC 9901 section 7.1: reject duplicate disclosure digests in _sd + _l2_dup = _duplicate_sd_digest(l2.payload) + if _l2_dup is not None: + result.errors.append(f"L2 _sd contains a duplicate disclosure digest (RFC 9901 section 7.1): {_l2_dup}") + return result + # 4a3. Check L2 top-level iat not in the future l2_iat = l2.payload.get("iat") if _is_future_dated(l2_iat, now, clock_skew_seconds): @@ -565,6 +595,13 @@ def verify_chain( result.errors.append(f"{l3_label} _sd_alg must be 'sha-256', got '{l3_sd_alg}'") return result + _l3_dup = _duplicate_sd_digest(l3.payload) + if _l3_dup is not None: + result.errors.append( + f"{l3_label} _sd contains a duplicate disclosure digest (RFC 9901 section 7.1): {_l3_dup}" + ) + return result + l3_iat = l3.payload.get("iat") if _is_future_dated(l3_iat, now, clock_skew_seconds): result.errors.append(f"{l3_label} iat is in the future: {l3_iat}") diff --git a/src/verifiable_intent/verification/constraint_checker.py b/python/src/verifiable_intent/verification/constraint_checker.py similarity index 100% rename from src/verifiable_intent/verification/constraint_checker.py rename to python/src/verifiable_intent/verification/constraint_checker.py diff --git a/src/verifiable_intent/verification/integrity.py b/python/src/verifiable_intent/verification/integrity.py similarity index 100% rename from src/verifiable_intent/verification/integrity.py rename to python/src/verifiable_intent/verification/integrity.py diff --git a/tests/__init__.py b/python/tests/__init__.py similarity index 100% rename from tests/__init__.py rename to python/tests/__init__.py diff --git a/tests/conftest.py b/python/tests/conftest.py similarity index 100% rename from tests/conftest.py rename to python/tests/conftest.py diff --git a/tests/test_aud_nonce.py b/python/tests/test_aud_nonce.py similarity index 100% rename from tests/test_aud_nonce.py rename to python/tests/test_aud_nonce.py diff --git a/tests/test_authorization_scope.py b/python/tests/test_authorization_scope.py similarity index 100% rename from tests/test_authorization_scope.py rename to python/tests/test_authorization_scope.py diff --git a/tests/test_autonomous_flow.py b/python/tests/test_autonomous_flow.py similarity index 100% rename from tests/test_autonomous_flow.py rename to python/tests/test_autonomous_flow.py diff --git a/tests/test_card_id.py b/python/tests/test_card_id.py similarity index 100% rename from tests/test_card_id.py rename to python/tests/test_card_id.py diff --git a/tests/test_chain_binding.py b/python/tests/test_chain_binding.py similarity index 100% rename from tests/test_chain_binding.py rename to python/tests/test_chain_binding.py diff --git a/tests/test_checkout_jwt_type.py b/python/tests/test_checkout_jwt_type.py similarity index 100% rename from tests/test_checkout_jwt_type.py rename to python/tests/test_checkout_jwt_type.py diff --git a/tests/test_configurable_vct.py b/python/tests/test_configurable_vct.py similarity index 100% rename from tests/test_configurable_vct.py rename to python/tests/test_configurable_vct.py diff --git a/tests/test_constraint_checking.py b/python/tests/test_constraint_checking.py similarity index 100% rename from tests/test_constraint_checking.py rename to python/tests/test_constraint_checking.py diff --git a/tests/test_constraints_none.py b/python/tests/test_constraints_none.py similarity index 100% rename from tests/test_constraints_none.py rename to python/tests/test_constraints_none.py diff --git a/tests/test_examples.py b/python/tests/test_examples.py similarity index 100% rename from tests/test_examples.py rename to python/tests/test_examples.py diff --git a/tests/test_immediate_flow.py b/python/tests/test_immediate_flow.py similarity index 100% rename from tests/test_immediate_flow.py rename to python/tests/test_immediate_flow.py diff --git a/tests/test_merchant_schema.py b/python/tests/test_merchant_schema.py similarity index 100% rename from tests/test_merchant_schema.py rename to python/tests/test_merchant_schema.py diff --git a/tests/test_mode_enforcement.py b/python/tests/test_mode_enforcement.py similarity index 100% rename from tests/test_mode_enforcement.py rename to python/tests/test_mode_enforcement.py diff --git a/tests/test_mode_inference.py b/python/tests/test_mode_inference.py similarity index 100% rename from tests/test_mode_inference.py rename to python/tests/test_mode_inference.py diff --git a/tests/test_multi_pair.py b/python/tests/test_multi_pair.py similarity index 100% rename from tests/test_multi_pair.py rename to python/tests/test_multi_pair.py diff --git a/tests/test_network_validation.py b/python/tests/test_network_validation.py similarity index 100% rename from tests/test_network_validation.py rename to python/tests/test_network_validation.py diff --git a/tests/test_new_constraints.py b/python/tests/test_new_constraints.py similarity index 100% rename from tests/test_new_constraints.py rename to python/tests/test_new_constraints.py diff --git a/tests/test_package_exports.py b/python/tests/test_package_exports.py similarity index 100% rename from tests/test_package_exports.py rename to python/tests/test_package_exports.py diff --git a/tests/test_reference_binding.py b/python/tests/test_reference_binding.py similarity index 100% rename from tests/test_reference_binding.py rename to python/tests/test_reference_binding.py diff --git a/tests/test_review_fixes.py b/python/tests/test_review_fixes.py similarity index 100% rename from tests/test_review_fixes.py rename to python/tests/test_review_fixes.py diff --git a/tests/test_review_round4_fixes.py b/python/tests/test_review_round4_fixes.py similarity index 100% rename from tests/test_review_round4_fixes.py rename to python/tests/test_review_round4_fixes.py diff --git a/tests/test_schema_validation.py b/python/tests/test_schema_validation.py similarity index 100% rename from tests/test_schema_validation.py rename to python/tests/test_schema_validation.py diff --git a/tests/test_selective_disclosure.py b/python/tests/test_selective_disclosure.py similarity index 100% rename from tests/test_selective_disclosure.py rename to python/tests/test_selective_disclosure.py diff --git a/tests/test_selective_routing.py b/python/tests/test_selective_routing.py similarity index 100% rename from tests/test_selective_routing.py rename to python/tests/test_selective_routing.py diff --git a/tests/test_verification_bypass.py b/python/tests/test_verification_bypass.py similarity index 100% rename from tests/test_verification_bypass.py rename to python/tests/test_verification_bypass.py diff --git a/tests/test_verification_hardening.py b/python/tests/test_verification_hardening.py similarity index 100% rename from tests/test_verification_hardening.py rename to python/tests/test_verification_hardening.py diff --git a/test-vectors/vectors.json b/test-vectors/vectors.json new file mode 100644 index 0000000..a1fe7d6 --- /dev/null +++ b/test-vectors/vectors.json @@ -0,0 +1,1705 @@ +{ + "_meta": { + "description": "Golden conformance vectors from the Python verifiable-intent reference implementation.", + "note": "ECDSA signatures are randomized; compare deterministic artifacts (disclosures, hashes, payload JSON, delegate_payload, _sd, sd_hash) — never signature bytes.", + "vi_version": "0.1.0", + "timestamps": { + "l1_iat": 1750000000, + "l1_exp": 1781536000, + "checkout_iat": 1750000050, + "l2_iat": 1750000100, + "l3_iat": 1750000200 + } + }, + "keys": { + "issuer": { + "kid": "mastercard-issuer-key-1", + "public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "private": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI", + "d": "AaKzxNXm9wgZKjtMXW5_gBEiM0RVZneImaq7zN3u_wE" + } + }, + "user": { + "kid": "user-device-key-1", + "public": { + "kty": "EC", + "crv": "P-256", + "x": "oSIYER4ca7X_m1DdylF9yF4ww6S16BEFNWucX8qk0YE", + "y": "RsocODIpBWgX5cuTHLDyO9k07q5iYCbdk1hTJQ8NL9I" + }, + "private": { + "kty": "EC", + "crv": "P-256", + "x": "oSIYER4ca7X_m1DdylF9yF4ww6S16BEFNWucX8qk0YE", + "y": "RsocODIpBWgX5cuTHLDyO9k07q5iYCbdk1hTJQ8NL9I", + "d": "ArPE1eb3CBkqO0xdbn-AESIzRFVmd4iZqrvM3e7_AQI" + } + }, + "agent": { + "kid": "agent-key-1", + "public": { + "kty": "EC", + "crv": "P-256", + "x": "0arZwPb0BGw_lgaU0QrylN8tv56fuKPJx6tZDSAlS90", + "y": "m9sm5wFdCfwCy6Ucf621n_hdRt_fd9ZssP-TVtP8Y2k" + }, + "private": { + "kty": "EC", + "crv": "P-256", + "x": "0arZwPb0BGw_lgaU0QrylN8tv56fuKPJx6tZDSAlS90", + "y": "m9sm5wFdCfwCy6Ucf621n_hdRt_fd9ZssP-TVtP8Y2k", + "d": "A8TV5vcIGSo7TF1uf4ARIjNEVWZ3iJmqu8zd7v8BAgM" + } + }, + "merchant": { + "kid": "merchant-key-1", + "public": { + "kty": "EC", + "crv": "P-256", + "x": "5HB8MJLNRpAQaLTpaWkDNxhHwQpAzO7rWuamrm-RPI0", + "y": "W5X17H1gV-bP46QJKLw_cklNdmbzk7LbaHhNP5YLyUE" + }, + "private": { + "kty": "EC", + "crv": "P-256", + "x": "5HB8MJLNRpAQaLTpaWkDNxhHwQpAzO7rWuamrm-RPI0", + "y": "W5X17H1gV-bP46QJKLw_cklNdmbzk7LbaHhNP5YLyUE", + "d": "BNXm9wgZKjtMXW5_gBEiM0RVZneImaq7zN3u_wECAwQ" + } + } + }, + "primitives": { + "b64url": [ + { + "bytes_hex": "", + "b64url": "" + }, + { + "bytes_hex": "00", + "b64url": "AA" + }, + { + "bytes_hex": "00010203", + "b64url": "AAECAw" + }, + { + "bytes_hex": "68656c6c6f20776f726c64", + "b64url": "aGVsbG8gd29ybGQ" + }, + { + "bytes_hex": "000102030405060708090a0b0c0d0e0f10111213", + "b64url": "AAECAwQFBgcICQoLDA0ODxAREhM" + } + ], + "disclosures": [ + { + "claim_name": "email", + "value": "user@example.com", + "salt": "AAAAAAAAAAAAAAAAAAAAAA", + "disclosure": "WyJBQUFBQUFBQUFBQUFBQUFBQUFBQUFBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0", + "hash": "_RK8GnwTBbwIzmGQxO4nVSiBgLvPL3huNEUGL3-DFF0" + }, + { + "claim_name": null, + "value": { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + }, + "salt": "BBBBBBBBBBBBBBBBBBBBBB", + "disclosure": "WyJCQkJCQkJCQkJCQkJCQkJCQkJCQkJCIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ", + "hash": "sfNXKih7MCixjVXTti30ctyBOyhU8-c2PNb7n0jsWYQ" + }, + { + "claim_name": null, + "value": { + "id": "BAB86345", + "title": "Babolat Pure Aero Tennis Racket" + }, + "salt": "CCCCCCCCCCCCCCCCCCCCCC", + "disclosure": "WyJDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDQ0NDIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0", + "hash": "dmjlriYqVvX94Z5YBIuBcv9xqiuSc8T8Vgcwf4SXeXw" + }, + { + "claim_name": null, + "value": { + "vct": "mandate.payment.1", + "payment_amount": { + "currency": "USD", + "amount": 27999 + } + }, + "salt": "DDDDDDDDDDDDDDDDDDDDDD", + "disclosure": "WyJEREREREREREREREREREREREREREREIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwicGF5bWVudF9hbW91bnQiOnsiY3VycmVuY3kiOiJVU0QiLCJhbW91bnQiOjI3OTk5fX1d", + "hash": "4I02VLDioklN5xSpkETSog0yZo_YSQfnEd30-UEUhs0" + } + ], + "hash_bytes": [ + { + "input_ascii": "", + "hash": "47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU" + }, + { + "input_ascii": "hello", + "hash": "LPJNul-wow4m6DsqxbninhsWHlwfp0JecwQzYpOLmCQ" + }, + { + "input_ascii": "abc~def~", + "hash": "W7TLozjiSeBgTbGn4nPjqR_ziW77CzcmzSLLnK5oeCg" + }, + { + "input_ascii": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1lcmNoYW50LWtleS0xIn0.eyJpc3MiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIiwic3ViIjoiY2FydF9jaGVja291dCIsImlhdCI6MTc1MDAwMDA1MCwiZXhwIjoxNzUwMDAzNjUwLCJjYXJ0Ijp7Iml0ZW1zIjpbeyJza3UiOiJCQUI4NjM0NSIsIm5hbWUiOiJCYWJvbGF0IFB1cmUgQWVybyBUZW5uaXMgUmFja2V0Iiwic2l6ZSI6Mywic2l6ZV9sYWJlbCI6IjQgMy84IiwiY29sb3IiOiJ3aGl0ZSIsInF1YW50aXR5IjoxLCJ1bml0UHJpY2UiOjI3OS45OX1dLCJzdWJUb3RhbCI6eyJhbW91bnQiOjI3OS45OSwiY3VycmVuY3lDb2RlIjoiVVNEIn19fQ.p20eGtZjSuB4kMcaEnabna0kDSrdrpOxAwgXCHBLcLBRxqKPAIMhx--U3H3mG7JUFAQ3ARJYwjvrDMoGe1mFng", + "hash": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + }, + { + "input_ascii": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "hash": "jhpBtnZS6GhlblNmWj_0O4mMgnTp7cL9uAYx6itGytk" + } + ] + }, + "models": { + "constraints": [ + { + "name": "AllowedMerchantConstraint", + "dict": { + "type": "mandate.checkout.allowed_merchants", + "allowed": [ + { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + }, + { + "id": "merchant-uuid-2", + "name": "Babolat", + "website": "https://babolat.com" + } + ] + }, + "json": "{\"type\":\"mandate.checkout.allowed_merchants\",\"allowed\":[{\"id\":\"merchant-uuid-1\",\"name\":\"Tennis Warehouse\",\"website\":\"https://tennis-warehouse.com\"},{\"id\":\"merchant-uuid-2\",\"name\":\"Babolat\",\"website\":\"https://babolat.com\"}]}" + }, + { + "name": "CheckoutLineItemsConstraint", + "dict": { + "type": "mandate.checkout.line_items", + "items": [ + { + "id": "line-1", + "acceptable_items": [ + { + "id": "BAB86345", + "title": "Babolat Pure Aero Tennis Racket" + } + ], + "quantity": 2 + } + ], + "match_mode": "exact" + }, + "json": "{\"type\":\"mandate.checkout.line_items\",\"items\":[{\"id\":\"line-1\",\"acceptable_items\":[{\"id\":\"BAB86345\",\"title\":\"Babolat Pure Aero Tennis Racket\"}],\"quantity\":2}],\"match_mode\":\"exact\"}" + }, + { + "name": "AllowedPayeeConstraint", + "dict": { + "type": "mandate.payment.allowed_payees", + "allowed": [ + { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + } + ] + }, + "json": "{\"type\":\"mandate.payment.allowed_payees\",\"allowed\":[{\"id\":\"merchant-uuid-1\",\"name\":\"Tennis Warehouse\",\"website\":\"https://tennis-warehouse.com\"}]}" + }, + { + "name": "PaymentAmountConstraint", + "dict": { + "type": "mandate.payment.amount_range", + "currency": "USD", + "min": 10000, + "max": 40000 + }, + "json": "{\"type\":\"mandate.payment.amount_range\",\"currency\":\"USD\",\"min\":10000,\"max\":40000}" + }, + { + "name": "PaymentAmountConstraint_max_only", + "dict": { + "type": "mandate.payment.amount_range", + "currency": "EUR", + "max": 5000 + }, + "json": "{\"type\":\"mandate.payment.amount_range\",\"currency\":\"EUR\",\"max\":5000}" + }, + { + "name": "ReferenceConstraint", + "dict": { + "type": "mandate.payment.reference", + "conditional_transaction_id": "deadbeef" + }, + "json": "{\"type\":\"mandate.payment.reference\",\"conditional_transaction_id\":\"deadbeef\"}" + }, + { + "name": "PaymentBudgetConstraint", + "dict": { + "type": "mandate.payment.budget", + "currency": "USD", + "max": 100000, + "min": 500 + }, + "json": "{\"type\":\"mandate.payment.budget\",\"currency\":\"USD\",\"max\":100000,\"min\":500}" + }, + { + "name": "PaymentRecurrenceConstraint", + "dict": { + "type": "mandate.payment.recurrence", + "frequency": "MNTH", + "start_date": "2026-01-01", + "end_date": "2026-12-31", + "number": 12 + }, + "json": "{\"type\":\"mandate.payment.recurrence\",\"frequency\":\"MNTH\",\"start_date\":\"2026-01-01\",\"end_date\":\"2026-12-31\",\"number\":12}" + }, + { + "name": "AgentRecurrenceConstraint", + "dict": { + "type": "mandate.payment.agent_recurrence", + "frequency": "WEEK", + "start_date": "2026-01-01", + "end_date": "2026-06-30", + "max_occurrences": 26 + }, + "json": "{\"type\":\"mandate.payment.agent_recurrence\",\"frequency\":\"WEEK\",\"start_date\":\"2026-01-01\",\"end_date\":\"2026-06-30\",\"max_occurrences\":26}" + } + ], + "misc": { + "issuer_credential_to_payload": { + "dict": { + "iss": "https://issuer.mastercard.com", + "sub": "user-subject-12345", + "iat": 1750000000, + "exp": 1781536000, + "vct": "https://credentials.mastercard.com/card", + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "oSIYER4ca7X_m1DdylF9yF4ww6S16BEFNWucX8qk0YE", + "y": "RsocODIpBWgX5cuTHLDyO9k07q5iYCbdk1hTJQ8NL9I" + } + }, + "pan_last_four": "1234", + "scheme": "mastercard", + "card_id": "f199c3dd-7106-478b-9b5f-7af9ca725170" + }, + "json": "{\"iss\":\"https://issuer.mastercard.com\",\"sub\":\"user-subject-12345\",\"iat\":1750000000,\"exp\":1781536000,\"vct\":\"https://credentials.mastercard.com/card\",\"cnf\":{\"jwk\":{\"kty\":\"EC\",\"crv\":\"P-256\",\"x\":\"oSIYER4ca7X_m1DdylF9yF4ww6S16BEFNWucX8qk0YE\",\"y\":\"RsocODIpBWgX5cuTHLDyO9k07q5iYCbdk1hTJQ8NL9I\"}},\"pan_last_four\":\"1234\",\"scheme\":\"mastercard\",\"card_id\":\"f199c3dd-7106-478b-9b5f-7af9ca725170\"}" + }, + "final_payment_mandate": { + "dict": { + "vct": "mandate.payment.1", + "transaction_id": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI", + "payee": { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + }, + "payment_amount": { + "currency": "USD", + "amount": 27999 + }, + "payment_instrument": { + "type": "mastercard.srcDigitalCard", + "id": "f199c3dd-7106-478b-9b5f-7af9ca725170", + "description": "Mastercard **** 1234" + } + } + }, + "final_checkout_mandate": { + "dict": { + "vct": "mandate.checkout.1", + "checkout_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1lcmNoYW50LWtleS0xIn0.eyJpc3MiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIiwic3ViIjoiY2FydF9jaGVja291dCIsImlhdCI6MTc1MDAwMDA1MCwiZXhwIjoxNzUwMDAzNjUwLCJjYXJ0Ijp7Iml0ZW1zIjpbeyJza3UiOiJCQUI4NjM0NSIsIm5hbWUiOiJCYWJvbGF0IFB1cmUgQWVybyBUZW5uaXMgUmFja2V0Iiwic2l6ZSI6Mywic2l6ZV9sYWJlbCI6IjQgMy84IiwiY29sb3IiOiJ3aGl0ZSIsInF1YW50aXR5IjoxLCJ1bml0UHJpY2UiOjI3OS45OX1dLCJzdWJUb3RhbCI6eyJhbW91bnQiOjI3OS45OSwiY3VycmVuY3lDb2RlIjoiVVNEIn19fQ.p20eGtZjSuB4kMcaEnabna0kDSrdrpOxAwgXCHBLcLBRxqKPAIMhx--U3H3mG7JUFAQ3ARJYwjvrDMoGe1mFng", + "checkout_hash": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + } + } + } + }, + "shared": { + "checkout_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1lcmNoYW50LWtleS0xIn0.eyJpc3MiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIiwic3ViIjoiY2FydF9jaGVja291dCIsImlhdCI6MTc1MDAwMDA1MCwiZXhwIjoxNzUwMDAzNjUwLCJjYXJ0Ijp7Iml0ZW1zIjpbeyJza3UiOiJCQUI4NjM0NSIsIm5hbWUiOiJCYWJvbGF0IFB1cmUgQWVybyBUZW5uaXMgUmFja2V0Iiwic2l6ZSI6Mywic2l6ZV9sYWJlbCI6IjQgMy84IiwiY29sb3IiOiJ3aGl0ZSIsInF1YW50aXR5IjoxLCJ1bml0UHJpY2UiOjI3OS45OX1dLCJzdWJUb3RhbCI6eyJhbW91bnQiOjI3OS45OSwiY3VycmVuY3lDb2RlIjoiVVNEIn19fQ.p20eGtZjSuB4kMcaEnabna0kDSrdrpOxAwgXCHBLcLBRxqKPAIMhx--U3H3mG7JUFAQ3ARJYwjvrDMoGe1mFng", + "checkout_hash": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI", + "l1_serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l1_sd_hash": "jhpBtnZS6GhlblNmWj_0O4mMgnTp7cL9uAYx6itGytk", + "l1": { + "header": { + "alg": "ES256", + "typ": "sd+jwt", + "kid": "mastercard-issuer-key-1" + }, + "payload": { + "iss": "https://issuer.mastercard.com", + "sub": "user-subject-12345", + "iat": 1750000000, + "exp": 1781536000, + "vct": "https://credentials.mastercard.com/card", + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "oSIYER4ca7X_m1DdylF9yF4ww6S16BEFNWucX8qk0YE", + "y": "RsocODIpBWgX5cuTHLDyO9k07q5iYCbdk1hTJQ8NL9I" + } + }, + "pan_last_four": "1234", + "scheme": "mastercard", + "card_id": "f199c3dd-7106-478b-9b5f-7af9ca725170", + "_sd": [ + "2kcLKYXCrkhv3v5TA45vDyeM5Fu4QYOfuLPJR-nu4os" + ], + "_sd_alg": "sha-256" + }, + "header_b64": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0", + "payload_b64": "eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ", + "issuer_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw", + "serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "disclosures": [ + "WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0" + ], + "salts": [ + "Mdr6tVLhWXEvZaiMRsnHwA" + ], + "values": [ + "user@example.com" + ], + "sd_hashes_of_disclosures": [ + "2kcLKYXCrkhv3v5TA45vDyeM5Fu4QYOfuLPJR-nu4os" + ] + }, + "l1_credential": { + "iss": "https://issuer.mastercard.com", + "sub": "user-subject-12345", + "iat": 1750000000, + "exp": 1781536000, + "vct": "https://credentials.mastercard.com/card", + "cnf_jwk": { + "kty": "EC", + "crv": "P-256", + "x": "oSIYER4ca7X_m1DdylF9yF4ww6S16BEFNWucX8qk0YE", + "y": "RsocODIpBWgX5cuTHLDyO9k07q5iYCbdk1hTJQ8NL9I" + }, + "pan_last_four": "1234", + "scheme": "mastercard", + "card_id": "f199c3dd-7106-478b-9b5f-7af9ca725170", + "email": "user@example.com" + } + }, + "immediate": { + "inputs": { + "nonce": "immediate-nonce-0001", + "aud": "https://agent.example.com", + "iat": 1750000100, + "iss": "https://wallet.example.com", + "exp": 1750001000, + "sd_hash": "jhpBtnZS6GhlblNmWj_0O4mMgnTp7cL9uAYx6itGytk", + "kid": "user-device-key-1", + "checkout_mandate": { + "vct": "mandate.checkout.1", + "checkout_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1lcmNoYW50LWtleS0xIn0.eyJpc3MiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIiwic3ViIjoiY2FydF9jaGVja291dCIsImlhdCI6MTc1MDAwMDA1MCwiZXhwIjoxNzUwMDAzNjUwLCJjYXJ0Ijp7Iml0ZW1zIjpbeyJza3UiOiJCQUI4NjM0NSIsIm5hbWUiOiJCYWJvbGF0IFB1cmUgQWVybyBUZW5uaXMgUmFja2V0Iiwic2l6ZSI6Mywic2l6ZV9sYWJlbCI6IjQgMy84IiwiY29sb3IiOiJ3aGl0ZSIsInF1YW50aXR5IjoxLCJ1bml0UHJpY2UiOjI3OS45OX1dLCJzdWJUb3RhbCI6eyJhbW91bnQiOjI3OS45OSwiY3VycmVuY3lDb2RlIjoiVVNEIn19fQ.p20eGtZjSuB4kMcaEnabna0kDSrdrpOxAwgXCHBLcLBRxqKPAIMhx--U3H3mG7JUFAQ3ARJYwjvrDMoGe1mFng" + }, + "payment_mandate": { + "vct": "mandate.payment.1", + "payment_instrument": { + "type": "mastercard.srcDigitalCard", + "id": "f199c3dd-7106-478b-9b5f-7af9ca725170", + "description": "Mastercard **** 1234" + }, + "payee": { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + }, + "currency": "USD", + "amount": 27999 + } + }, + "checkout_mandate_dict": { + "vct": "mandate.checkout.1", + "checkout_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1lcmNoYW50LWtleS0xIn0.eyJpc3MiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIiwic3ViIjoiY2FydF9jaGVja291dCIsImlhdCI6MTc1MDAwMDA1MCwiZXhwIjoxNzUwMDAzNjUwLCJjYXJ0Ijp7Iml0ZW1zIjpbeyJza3UiOiJCQUI4NjM0NSIsIm5hbWUiOiJCYWJvbGF0IFB1cmUgQWVybyBUZW5uaXMgUmFja2V0Iiwic2l6ZSI6Mywic2l6ZV9sYWJlbCI6IjQgMy84IiwiY29sb3IiOiJ3aGl0ZSIsInF1YW50aXR5IjoxLCJ1bml0UHJpY2UiOjI3OS45OX1dLCJzdWJUb3RhbCI6eyJhbW91bnQiOjI3OS45OSwiY3VycmVuY3lDb2RlIjoiVVNEIn19fQ.p20eGtZjSuB4kMcaEnabna0kDSrdrpOxAwgXCHBLcLBRxqKPAIMhx--U3H3mG7JUFAQ3ARJYwjvrDMoGe1mFng", + "checkout_hash": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + }, + "payment_mandate_dict": { + "vct": "mandate.payment.1", + "payment_instrument": { + "type": "mastercard.srcDigitalCard", + "id": "f199c3dd-7106-478b-9b5f-7af9ca725170", + "description": "Mastercard **** 1234" + }, + "payee": { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + }, + "payment_amount": { + "currency": "USD", + "amount": 27999 + }, + "transaction_id": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + }, + "l2": { + "header": { + "alg": "ES256", + "typ": "kb-sd-jwt", + "kid": "user-device-key-1" + }, + "payload": { + "nonce": "immediate-nonce-0001", + "aud": "https://agent.example.com", + "iat": 1750000100, + "sd_hash": "jhpBtnZS6GhlblNmWj_0O4mMgnTp7cL9uAYx6itGytk", + "delegate_payload": [ + { + "...": "rcAs1M-r2UWZZWFrBNPwlEYXbQekyD2qm1ETNUmU0O4" + }, + { + "...": "lbhDBk30b06Y0juxqxjyDH-5cd1ORzFO-S7baTyvZKE" + } + ], + "_sd_alg": "sha-256", + "iss": "https://wallet.example.com", + "exp": 1750001000, + "_sd": [ + "rcAs1M-r2UWZZWFrBNPwlEYXbQekyD2qm1ETNUmU0O4", + "lbhDBk30b06Y0juxqxjyDH-5cd1ORzFO-S7baTyvZKE" + ] + }, + "header_b64": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0", + "payload_b64": "eyJub25jZSI6ImltbWVkaWF0ZS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImlhdCI6MTc1MDAwMDEwMCwic2RfaGFzaCI6ImpocEJ0blpTNkdobGJsTm1Xal8wTzRtTWduVHA3Y0w5dUFZeDZpdEd5dGsiLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJyY0FzMU0tcjJVV1paV0ZyQk5Qd2xFWVhiUWVreUQycW0xRVROVW1VME80In0seyIuLi4iOiJsYmhEQmszMGIwNlkwanV4cXhqeURILTVjZDFPUnpGTy1TN2JhVHl2WktFIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDAxMDAwLCJfc2QiOlsicmNBczFNLXIyVVdaWldGckJOUHdsRVlYYlFla3lEMnFtMUVUTlVtVTBPNCIsImxiaERCazMwYjA2WTBqdXhxeGp5REgtNWNkMU9SekZPLVM3YmFUeXZaS0UiXX0", + "issuer_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImltbWVkaWF0ZS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImlhdCI6MTc1MDAwMDEwMCwic2RfaGFzaCI6ImpocEJ0blpTNkdobGJsTm1Xal8wTzRtTWduVHA3Y0w5dUFZeDZpdEd5dGsiLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJyY0FzMU0tcjJVV1paV0ZyQk5Qd2xFWVhiUWVreUQycW0xRVROVW1VME80In0seyIuLi4iOiJsYmhEQmszMGIwNlkwanV4cXhqeURILTVjZDFPUnpGTy1TN2JhVHl2WktFIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDAxMDAwLCJfc2QiOlsicmNBczFNLXIyVVdaWldGckJOUHdsRVlYYlFla3lEMnFtMUVUTlVtVTBPNCIsImxiaERCazMwYjA2WTBqdXhxeGp5REgtNWNkMU9SekZPLVM3YmFUeXZaS0UiXX0.QGdHm8bVsfwxuc8JvENWiRsz86H88pr_FRWFh8hsShgHnH1XSfQ5CePju5HxHDhU1ElaymwdrNfAh55qamAimA", + "serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImltbWVkaWF0ZS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImlhdCI6MTc1MDAwMDEwMCwic2RfaGFzaCI6ImpocEJ0blpTNkdobGJsTm1Xal8wTzRtTWduVHA3Y0w5dUFZeDZpdEd5dGsiLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJyY0FzMU0tcjJVV1paV0ZyQk5Qd2xFWVhiUWVreUQycW0xRVROVW1VME80In0seyIuLi4iOiJsYmhEQmszMGIwNlkwanV4cXhqeURILTVjZDFPUnpGTy1TN2JhVHl2WktFIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDAxMDAwLCJfc2QiOlsicmNBczFNLXIyVVdaWldGckJOUHdsRVlYYlFla3lEMnFtMUVUTlVtVTBPNCIsImxiaERCazMwYjA2WTBqdXhxeGp5REgtNWNkMU9SekZPLVM3YmFUeXZaS0UiXX0.QGdHm8bVsfwxuc8JvENWiRsz86H88pr_FRWFh8hsShgHnH1XSfQ5CePju5HxHDhU1ElaymwdrNfAh55qamAimA~WyIyci1Zdl95NzdjVTE2VWxMSlYtUUJnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~WyJSOXFvbnZXZVUwOWtlZmtmbWdFcWJnIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In0sInBheWVlIjp7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9LCJwYXltZW50X2Ftb3VudCI6eyJjdXJyZW5jeSI6IlVTRCIsImFtb3VudCI6Mjc5OTl9LCJ0cmFuc2FjdGlvbl9pZCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "disclosures": [ + "WyIyci1Zdl95NzdjVTE2VWxMSlYtUUJnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0", + "WyJSOXFvbnZXZVUwOWtlZmtmbWdFcWJnIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In0sInBheWVlIjp7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9LCJwYXltZW50X2Ftb3VudCI6eyJjdXJyZW5jeSI6IlVTRCIsImFtb3VudCI6Mjc5OTl9LCJ0cmFuc2FjdGlvbl9pZCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0" + ], + "salts": [ + "2r-Yv_y77cU16UlLJV-QBg", + "R9qonvWeU09kefkfmgEqbg" + ], + "values": [ + { + "vct": "mandate.checkout.1", + "checkout_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1lcmNoYW50LWtleS0xIn0.eyJpc3MiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIiwic3ViIjoiY2FydF9jaGVja291dCIsImlhdCI6MTc1MDAwMDA1MCwiZXhwIjoxNzUwMDAzNjUwLCJjYXJ0Ijp7Iml0ZW1zIjpbeyJza3UiOiJCQUI4NjM0NSIsIm5hbWUiOiJCYWJvbGF0IFB1cmUgQWVybyBUZW5uaXMgUmFja2V0Iiwic2l6ZSI6Mywic2l6ZV9sYWJlbCI6IjQgMy84IiwiY29sb3IiOiJ3aGl0ZSIsInF1YW50aXR5IjoxLCJ1bml0UHJpY2UiOjI3OS45OX1dLCJzdWJUb3RhbCI6eyJhbW91bnQiOjI3OS45OSwiY3VycmVuY3lDb2RlIjoiVVNEIn19fQ.p20eGtZjSuB4kMcaEnabna0kDSrdrpOxAwgXCHBLcLBRxqKPAIMhx--U3H3mG7JUFAQ3ARJYwjvrDMoGe1mFng", + "checkout_hash": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + }, + { + "vct": "mandate.payment.1", + "payment_instrument": { + "type": "mastercard.srcDigitalCard", + "id": "f199c3dd-7106-478b-9b5f-7af9ca725170", + "description": "Mastercard **** 1234" + }, + "payee": { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + }, + "payment_amount": { + "currency": "USD", + "amount": 27999 + }, + "transaction_id": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + } + ], + "sd_hashes_of_disclosures": [ + "rcAs1M-r2UWZZWFrBNPwlEYXbQekyD2qm1ETNUmU0O4", + "lbhDBk30b06Y0juxqxjyDH-5cd1ORzFO-S7baTyvZKE" + ] + }, + "delegate_payload": [ + { + "...": "rcAs1M-r2UWZZWFrBNPwlEYXbQekyD2qm1ETNUmU0O4" + }, + { + "...": "lbhDBk30b06Y0juxqxjyDH-5cd1ORzFO-S7baTyvZKE" + } + ] + }, + "autonomous": { + "inputs": { + "nonce": "autonomous-nonce-0001", + "aud": "https://agent.example.com", + "iat": 1750000100, + "iss": "https://wallet.example.com", + "exp": 1750086500, + "sd_hash": "jhpBtnZS6GhlblNmWj_0O4mMgnTp7cL9uAYx6itGytk", + "kid": "user-device-key-1", + "agent_cnf_jwk": { + "kty": "EC", + "crv": "P-256", + "x": "0arZwPb0BGw_lgaU0QrylN8tv56fuKPJx6tZDSAlS90", + "y": "m9sm5wFdCfwCy6Ucf621n_hdRt_fd9ZssP-TVtP8Y2k" + }, + "agent_cnf_kid": "agent-key-1", + "merchants": [ + { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + }, + { + "id": "merchant-uuid-2", + "name": "Babolat", + "website": "https://babolat.com" + } + ], + "acceptable_items": [ + { + "id": "BAB86345", + "title": "Babolat Pure Aero Tennis Racket" + }, + { + "id": "HEA23102", + "title": "Head Graphene 360 Speed" + } + ] + }, + "l2": { + "header": { + "alg": "ES256", + "typ": "kb-sd-jwt+kb", + "kid": "user-device-key-1" + }, + "payload": { + "nonce": "autonomous-nonce-0001", + "aud": "https://agent.example.com", + "iat": 1750000100, + "sd_hash": "jhpBtnZS6GhlblNmWj_0O4mMgnTp7cL9uAYx6itGytk", + "delegate_payload": [ + { + "...": "rhURWZMY8cMCjgWmHEFqtgdg1cqnaWoqCVxqEnkzcR4" + }, + { + "...": "e3ymh4aa6qxLZlEDk_3pERbqCODRQj7qfXEPb6GVknw" + } + ], + "_sd_alg": "sha-256", + "_sd": [ + "vMS0sCI_rxOkmy0Z3EZw_DWYzquSvsg5FpBnbbC6oSQ", + "mK70X4fRi1hAsjxOIFGUN-7mJBahfQUL7CN_hzes7NE", + "guzj77pfRec2WMFHY9k5H31dZJPIp9drDxLUljdeD_0", + "LVGZfVefQn97MNH-8lHaTXBcnys7152wDRstAguckH0", + "rhURWZMY8cMCjgWmHEFqtgdg1cqnaWoqCVxqEnkzcR4", + "e3ymh4aa6qxLZlEDk_3pERbqCODRQj7qfXEPb6GVknw" + ], + "iss": "https://wallet.example.com", + "exp": 1750086500 + }, + "header_b64": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0", + "payload_b64": "eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ", + "issuer_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q", + "serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJVeGRQY2lVTFp6SUNrTnU0MllBNk13Iix7ImlkIjoibWVyY2hhbnQtdXVpZC0yIiwibmFtZSI6IkJhYm9sYXQiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly9iYWJvbGF0LmNvbSJ9XQ~WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~WyJ1dXhkRW9wazBFNVdyVm1fM3pPVHl3Iix7ImlkIjoiSEVBMjMxMDIiLCJ0aXRsZSI6IkhlYWQgR3JhcGhlbmUgMzYwIFNwZWVkIn1d~WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~", + "disclosures": [ + "WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ", + "WyJVeGRQY2lVTFp6SUNrTnU0MllBNk13Iix7ImlkIjoibWVyY2hhbnQtdXVpZC0yIiwibmFtZSI6IkJhYm9sYXQiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly9iYWJvbGF0LmNvbSJ9XQ", + "WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0", + "WyJ1dXhkRW9wazBFNVdyVm1fM3pPVHl3Iix7ImlkIjoiSEVBMjMxMDIiLCJ0aXRsZSI6IkhlYWQgR3JhcGhlbmUgMzYwIFNwZWVkIn1d", + "WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d", + "WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ" + ], + "salts": [ + "ZuiUR9n5t0v3F6axn3TH-A", + "UxdPciULZzICkNu42YA6Mw", + "HjbabCr2_wVyzw224TEMpg", + "uuxdEopk0E5WrVm_3zOTyw", + "lnEe4FL32lvJDHR-_0aJqg", + "SrX6K86NlZ8WvWGdw7QR7w" + ], + "values": [ + { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + }, + { + "id": "merchant-uuid-2", + "name": "Babolat", + "website": "https://babolat.com" + }, + { + "id": "BAB86345", + "title": "Babolat Pure Aero Tennis Racket" + }, + { + "id": "HEA23102", + "title": "Head Graphene 360 Speed" + }, + { + "vct": "mandate.checkout.open.1", + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "0arZwPb0BGw_lgaU0QrylN8tv56fuKPJx6tZDSAlS90", + "y": "m9sm5wFdCfwCy6Ucf621n_hdRt_fd9ZssP-TVtP8Y2k", + "kid": "agent-key-1" + } + }, + "constraints": [ + { + "type": "mandate.checkout.allowed_merchants", + "allowed": [ + { + "...": "vMS0sCI_rxOkmy0Z3EZw_DWYzquSvsg5FpBnbbC6oSQ" + }, + { + "...": "mK70X4fRi1hAsjxOIFGUN-7mJBahfQUL7CN_hzes7NE" + } + ] + }, + { + "type": "mandate.checkout.line_items", + "items": [ + { + "id": "line-1", + "acceptable_items": [ + { + "...": "guzj77pfRec2WMFHY9k5H31dZJPIp9drDxLUljdeD_0" + }, + { + "...": "LVGZfVefQn97MNH-8lHaTXBcnys7152wDRstAguckH0" + } + ], + "quantity": 1 + } + ], + "match_mode": "minimum" + } + ] + }, + { + "vct": "mandate.payment.open.1", + "cnf": { + "jwk": { + "kty": "EC", + "crv": "P-256", + "x": "0arZwPb0BGw_lgaU0QrylN8tv56fuKPJx6tZDSAlS90", + "y": "m9sm5wFdCfwCy6Ucf621n_hdRt_fd9ZssP-TVtP8Y2k", + "kid": "agent-key-1" + } + }, + "constraints": [ + { + "type": "mandate.payment.allowed_payees", + "allowed": [ + { + "...": "vMS0sCI_rxOkmy0Z3EZw_DWYzquSvsg5FpBnbbC6oSQ" + } + ] + }, + { + "type": "mandate.payment.amount_range", + "currency": "USD", + "min": 10000, + "max": 40000 + }, + { + "type": "mandate.payment.reference", + "conditional_transaction_id": "rhURWZMY8cMCjgWmHEFqtgdg1cqnaWoqCVxqEnkzcR4" + } + ], + "payment_instrument": { + "type": "mastercard.srcDigitalCard", + "id": "f199c3dd-7106-478b-9b5f-7af9ca725170", + "description": "Mastercard **** 1234" + } + } + ], + "sd_hashes_of_disclosures": [ + "vMS0sCI_rxOkmy0Z3EZw_DWYzquSvsg5FpBnbbC6oSQ", + "mK70X4fRi1hAsjxOIFGUN-7mJBahfQUL7CN_hzes7NE", + "guzj77pfRec2WMFHY9k5H31dZJPIp9drDxLUljdeD_0", + "LVGZfVefQn97MNH-8lHaTXBcnys7152wDRstAguckH0", + "rhURWZMY8cMCjgWmHEFqtgdg1cqnaWoqCVxqEnkzcR4", + "e3ymh4aa6qxLZlEDk_3pERbqCODRQj7qfXEPb6GVknw" + ] + }, + "disclosure_refs": { + "payment_disc": "WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ", + "checkout_disc": "WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d", + "merchant_disc": "WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ", + "item_disc": "WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0" + }, + "l3a": { + "inputs": { + "nonce": "l3a-nonce-0001", + "aud": "https://network.mastercard.com", + "iat": 1750000200, + "iss": "https://agent.example.com", + "exp": 1750000500, + "kid": "agent-key-1", + "l2_base_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q", + "payment_disclosure": "WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ", + "merchant_disclosure": "WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ", + "final_payment": { + "vct": "mandate.payment.1", + "transaction_id": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI", + "payee": { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + }, + "payment_amount": { + "currency": "USD", + "amount": 27999 + }, + "payment_instrument": { + "type": "mastercard.srcDigitalCard", + "id": "f199c3dd-7106-478b-9b5f-7af9ca725170", + "description": "Mastercard **** 1234" + } + }, + "final_merchant": { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + } + }, + "selective_presentation": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~", + "sd_hash": "llsrHkmQpjk3h8Ykk2rjufjI8SwFQpEydlWYnHWB4bg", + "credential": { + "header": { + "alg": "ES256", + "typ": "kb-sd-jwt", + "kid": "agent-key-1" + }, + "payload": { + "nonce": "l3a-nonce-0001", + "aud": "https://network.mastercard.com", + "sd_hash": "llsrHkmQpjk3h8Ykk2rjufjI8SwFQpEydlWYnHWB4bg", + "iat": 1750000200, + "delegate_payload": [ + { + "...": "SNtn-pb8GPPrsEKyByCfVpmuZ8M9FD7O-Let-cs_vNY" + }, + { + "...": "TdWACE36GxRFBRvQGQBy4MA3iX06Dl0eurr1l4YZvfE" + } + ], + "_sd_alg": "sha-256", + "iss": "https://agent.example.com", + "exp": 1750000500 + }, + "header_b64": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0", + "payload_b64": "eyJub25jZSI6ImwzYS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9uZXR3b3JrLm1hc3RlcmNhcmQuY29tIiwic2RfaGFzaCI6Imxsc3JIa21RcGprM2g4WWtrMnJqdWZqSThTd0ZRcEV5ZGxXWW5IV0I0YmciLCJpYXQiOjE3NTAwMDAyMDAsImRlbGVnYXRlX3BheWxvYWQiOlt7Ii4uLiI6IlNOdG4tcGI4R1BQcnNFS3lCeUNmVnBtdVo4TTlGRDdPLUxldC1jc192TlkifSx7Ii4uLiI6IlRkV0FDRTM2R3hSRkJSdlFHUUJ5NE1BM2lYMDZEbDBldXJyMWw0WVp2ZkUifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImV4cCI6MTc1MDAwMDUwMH0", + "issuer_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9uZXR3b3JrLm1hc3RlcmNhcmQuY29tIiwic2RfaGFzaCI6Imxsc3JIa21RcGprM2g4WWtrMnJqdWZqSThTd0ZRcEV5ZGxXWW5IV0I0YmciLCJpYXQiOjE3NTAwMDAyMDAsImRlbGVnYXRlX3BheWxvYWQiOlt7Ii4uLiI6IlNOdG4tcGI4R1BQcnNFS3lCeUNmVnBtdVo4TTlGRDdPLUxldC1jc192TlkifSx7Ii4uLiI6IlRkV0FDRTM2R3hSRkJSdlFHUUJ5NE1BM2lYMDZEbDBldXJyMWw0WVp2ZkUifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImV4cCI6MTc1MDAwMDUwMH0.FKr84Cmnjj-SsUlXNJwretTGjtLYGBiT73t89FF-fK70jvvZ9KWn0AnG86jVjQzXhKFCVHQAbsfFTmenoWGZ5g", + "serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9uZXR3b3JrLm1hc3RlcmNhcmQuY29tIiwic2RfaGFzaCI6Imxsc3JIa21RcGprM2g4WWtrMnJqdWZqSThTd0ZRcEV5ZGxXWW5IV0I0YmciLCJpYXQiOjE3NTAwMDAyMDAsImRlbGVnYXRlX3BheWxvYWQiOlt7Ii4uLiI6IlNOdG4tcGI4R1BQcnNFS3lCeUNmVnBtdVo4TTlGRDdPLUxldC1jc192TlkifSx7Ii4uLiI6IlRkV0FDRTM2R3hSRkJSdlFHUUJ5NE1BM2lYMDZEbDBldXJyMWw0WVp2ZkUifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImV4cCI6MTc1MDAwMDUwMH0.FKr84Cmnjj-SsUlXNJwretTGjtLYGBiT73t89FF-fK70jvvZ9KWn0AnG86jVjQzXhKFCVHQAbsfFTmenoWGZ5g~WyJjQjdBN1Q5X1JNdWRaRmxfYWpHU2xRIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJwZXhPbW41WUEyU3dCSnI5X3RoRVRBIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwidHJhbnNhY3Rpb25faWQiOiI4NmplQjhmcVphNloxSE54bXhydmphRlZjaXVyVDRWTjJ1ZHJHNXN3NXpJIiwicGF5ZWUiOnsiaWQiOiJtZXJjaGFudC11dWlkLTEiLCJuYW1lIjoiVGVubmlzIFdhcmVob3VzZSIsIndlYnNpdGUiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIn0sInBheW1lbnRfYW1vdW50Ijp7ImN1cnJlbmN5IjoiVVNEIiwiYW1vdW50IjoyNzk5OX0sInBheW1lbnRfaW5zdHJ1bWVudCI6eyJ0eXBlIjoibWFzdGVyY2FyZC5zcmNEaWdpdGFsQ2FyZCIsImlkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiZGVzY3JpcHRpb24iOiJNYXN0ZXJjYXJkICoqKiogMTIzNCJ9fV0~", + "disclosures": [ + "WyJjQjdBN1Q5X1JNdWRaRmxfYWpHU2xRIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ", + "WyJwZXhPbW41WUEyU3dCSnI5X3RoRVRBIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwidHJhbnNhY3Rpb25faWQiOiI4NmplQjhmcVphNloxSE54bXhydmphRlZjaXVyVDRWTjJ1ZHJHNXN3NXpJIiwicGF5ZWUiOnsiaWQiOiJtZXJjaGFudC11dWlkLTEiLCJuYW1lIjoiVGVubmlzIFdhcmVob3VzZSIsIndlYnNpdGUiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIn0sInBheW1lbnRfYW1vdW50Ijp7ImN1cnJlbmN5IjoiVVNEIiwiYW1vdW50IjoyNzk5OX0sInBheW1lbnRfaW5zdHJ1bWVudCI6eyJ0eXBlIjoibWFzdGVyY2FyZC5zcmNEaWdpdGFsQ2FyZCIsImlkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiZGVzY3JpcHRpb24iOiJNYXN0ZXJjYXJkICoqKiogMTIzNCJ9fV0" + ], + "salts": [ + "cB7A7T9_RMudZFl_ajGSlQ", + "pexOmn5YA2SwBJr9_thETA" + ], + "values": [ + { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + }, + { + "vct": "mandate.payment.1", + "transaction_id": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI", + "payee": { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + }, + "payment_amount": { + "currency": "USD", + "amount": 27999 + }, + "payment_instrument": { + "type": "mastercard.srcDigitalCard", + "id": "f199c3dd-7106-478b-9b5f-7af9ca725170", + "description": "Mastercard **** 1234" + } + } + ], + "sd_hashes_of_disclosures": [ + "SNtn-pb8GPPrsEKyByCfVpmuZ8M9FD7O-Let-cs_vNY", + "TdWACE36GxRFBRvQGQBy4MA3iX06Dl0eurr1l4YZvfE" + ] + } + }, + "l3b": { + "inputs": { + "nonce": "l3b-nonce-0001", + "aud": "https://tennis-warehouse.com", + "iat": 1750000200, + "iss": "https://agent.example.com", + "exp": 1750000500, + "kid": "agent-key-1", + "l2_base_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q", + "checkout_disclosure": "WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d", + "item_disclosure": "WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0", + "final_checkout": { + "vct": "mandate.checkout.1", + "checkout_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1lcmNoYW50LWtleS0xIn0.eyJpc3MiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIiwic3ViIjoiY2FydF9jaGVja291dCIsImlhdCI6MTc1MDAwMDA1MCwiZXhwIjoxNzUwMDAzNjUwLCJjYXJ0Ijp7Iml0ZW1zIjpbeyJza3UiOiJCQUI4NjM0NSIsIm5hbWUiOiJCYWJvbGF0IFB1cmUgQWVybyBUZW5uaXMgUmFja2V0Iiwic2l6ZSI6Mywic2l6ZV9sYWJlbCI6IjQgMy84IiwiY29sb3IiOiJ3aGl0ZSIsInF1YW50aXR5IjoxLCJ1bml0UHJpY2UiOjI3OS45OX1dLCJzdWJUb3RhbCI6eyJhbW91bnQiOjI3OS45OSwiY3VycmVuY3lDb2RlIjoiVVNEIn19fQ.p20eGtZjSuB4kMcaEnabna0kDSrdrpOxAwgXCHBLcLBRxqKPAIMhx--U3H3mG7JUFAQ3ARJYwjvrDMoGe1mFng", + "checkout_hash": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + } + }, + "selective_presentation": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~", + "sd_hash": "DJ8l3f100eon6xfeHq6lK5aZ5d76SRGAiH-yb50oVP0", + "credential": { + "header": { + "alg": "ES256", + "typ": "kb-sd-jwt", + "kid": "agent-key-1" + }, + "payload": { + "nonce": "l3b-nonce-0001", + "aud": "https://tennis-warehouse.com", + "sd_hash": "DJ8l3f100eon6xfeHq6lK5aZ5d76SRGAiH-yb50oVP0", + "iat": 1750000200, + "delegate_payload": [ + { + "...": "h5z6lDoFbtUMpxyYgN6UpAG0ApZfGbu_61TwLlBeSIk" + } + ], + "_sd_alg": "sha-256", + "iss": "https://agent.example.com", + "exp": 1750000500 + }, + "header_b64": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0", + "payload_b64": "eyJub25jZSI6ImwzYi1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSIsInNkX2hhc2giOiJESjhsM2YxMDBlb242eGZlSHE2bEs1YVo1ZDc2U1JHQWlILXliNTBvVlAwIiwiaWF0IjoxNzUwMDAwMjAwLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJoNXo2bERvRmJ0VU1weHlZZ042VXBBRzBBcFpmR2J1XzYxVHdMbEJlU0lrIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJleHAiOjE3NTAwMDA1MDB9", + "issuer_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYi1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSIsInNkX2hhc2giOiJESjhsM2YxMDBlb242eGZlSHE2bEs1YVo1ZDc2U1JHQWlILXliNTBvVlAwIiwiaWF0IjoxNzUwMDAwMjAwLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJoNXo2bERvRmJ0VU1weHlZZ042VXBBRzBBcFpmR2J1XzYxVHdMbEJlU0lrIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJleHAiOjE3NTAwMDA1MDB9.SpZoGr9jaHf42Qu18IjWXrNqFmIzX2PUsmqQyTLJ16-pKGYdVCCl46mXbKw1JMoeb7fLUvL4SiiBTv0mXZBQrA", + "serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYi1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSIsInNkX2hhc2giOiJESjhsM2YxMDBlb242eGZlSHE2bEs1YVo1ZDc2U1JHQWlILXliNTBvVlAwIiwiaWF0IjoxNzUwMDAwMjAwLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJoNXo2bERvRmJ0VU1weHlZZ042VXBBRzBBcFpmR2J1XzYxVHdMbEJlU0lrIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJleHAiOjE3NTAwMDA1MDB9.SpZoGr9jaHf42Qu18IjWXrNqFmIzX2PUsmqQyTLJ16-pKGYdVCCl46mXbKw1JMoeb7fLUvL4SiiBTv0mXZBQrA~WyJsRFkzeVJFZElOOGZ6Q3pQU0hIRDdBIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "disclosures": [ + "WyJsRFkzeVJFZElOOGZ6Q3pQU0hIRDdBIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0" + ], + "salts": [ + "lDY3yREdIN8fzCzPSHHD7A" + ], + "values": [ + { + "vct": "mandate.checkout.1", + "checkout_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1lcmNoYW50LWtleS0xIn0.eyJpc3MiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIiwic3ViIjoiY2FydF9jaGVja291dCIsImlhdCI6MTc1MDAwMDA1MCwiZXhwIjoxNzUwMDAzNjUwLCJjYXJ0Ijp7Iml0ZW1zIjpbeyJza3UiOiJCQUI4NjM0NSIsIm5hbWUiOiJCYWJvbGF0IFB1cmUgQWVybyBUZW5uaXMgUmFja2V0Iiwic2l6ZSI6Mywic2l6ZV9sYWJlbCI6IjQgMy84IiwiY29sb3IiOiJ3aGl0ZSIsInF1YW50aXR5IjoxLCJ1bml0UHJpY2UiOjI3OS45OX1dLCJzdWJUb3RhbCI6eyJhbW91bnQiOjI3OS45OSwiY3VycmVuY3lDb2RlIjoiVVNEIn19fQ.p20eGtZjSuB4kMcaEnabna0kDSrdrpOxAwgXCHBLcLBRxqKPAIMhx--U3H3mG7JUFAQ3ARJYwjvrDMoGe1mFng", + "checkout_hash": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + } + ], + "sd_hashes_of_disclosures": [ + "h5z6lDoFbtUMpxyYgN6UpAG0ApZfGbu_61TwLlBeSIk" + ] + } + } + }, + "verification": { + "verify_now": 1750000260, + "immediate_valid": true, + "immediate_errors": [], + "autonomous_valid": true, + "autonomous_errors": [] + }, + "verification_conformance": { + "chain_scenarios": [ + { + "name": "immediate_valid", + "current_time": 1750000260, + "issuer_public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImltbWVkaWF0ZS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImlhdCI6MTc1MDAwMDEwMCwic2RfaGFzaCI6ImpocEJ0blpTNkdobGJsTm1Xal8wTzRtTWduVHA3Y0w5dUFZeDZpdEd5dGsiLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJyY0FzMU0tcjJVV1paV0ZyQk5Qd2xFWVhiUWVreUQycW0xRVROVW1VME80In0seyIuLi4iOiJsYmhEQmszMGIwNlkwanV4cXhqeURILTVjZDFPUnpGTy1TN2JhVHl2WktFIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDAxMDAwLCJfc2QiOlsicmNBczFNLXIyVVdaWldGckJOUHdsRVlYYlFla3lEMnFtMUVUTlVtVTBPNCIsImxiaERCazMwYjA2WTBqdXhxeGp5REgtNWNkMU9SekZPLVM3YmFUeXZaS0UiXX0.QGdHm8bVsfwxuc8JvENWiRsz86H88pr_FRWFh8hsShgHnH1XSfQ5CePju5HxHDhU1ElaymwdrNfAh55qamAimA~WyIyci1Zdl95NzdjVTE2VWxMSlYtUUJnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~WyJSOXFvbnZXZVUwOWtlZmtmbWdFcWJnIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In0sInBheWVlIjp7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9LCJwYXltZW50X2Ftb3VudCI6eyJjdXJyZW5jeSI6IlVTRCIsImFtb3VudCI6Mjc5OTl9LCJ0cmFuc2FjdGlvbl9pZCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l3_payment": null, + "l3_checkout": null, + "l2_payment_serialized": null, + "l2_checkout_serialized": null, + "expected_l2_aud": "https://agent.example.com", + "expected_l2_nonce": "immediate-nonce-0001", + "expected_l3_payment_aud": null, + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": true, + "expected_errors": [] + }, + { + "name": "immediate_wrong_aud", + "current_time": 1750000260, + "issuer_public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImltbWVkaWF0ZS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImlhdCI6MTc1MDAwMDEwMCwic2RfaGFzaCI6ImpocEJ0blpTNkdobGJsTm1Xal8wTzRtTWduVHA3Y0w5dUFZeDZpdEd5dGsiLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJyY0FzMU0tcjJVV1paV0ZyQk5Qd2xFWVhiUWVreUQycW0xRVROVW1VME80In0seyIuLi4iOiJsYmhEQmszMGIwNlkwanV4cXhqeURILTVjZDFPUnpGTy1TN2JhVHl2WktFIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDAxMDAwLCJfc2QiOlsicmNBczFNLXIyVVdaWldGckJOUHdsRVlYYlFla3lEMnFtMUVUTlVtVTBPNCIsImxiaERCazMwYjA2WTBqdXhxeGp5REgtNWNkMU9SekZPLVM3YmFUeXZaS0UiXX0.QGdHm8bVsfwxuc8JvENWiRsz86H88pr_FRWFh8hsShgHnH1XSfQ5CePju5HxHDhU1ElaymwdrNfAh55qamAimA~WyIyci1Zdl95NzdjVTE2VWxMSlYtUUJnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~WyJSOXFvbnZXZVUwOWtlZmtmbWdFcWJnIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In0sInBheWVlIjp7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9LCJwYXltZW50X2Ftb3VudCI6eyJjdXJyZW5jeSI6IlVTRCIsImFtb3VudCI6Mjc5OTl9LCJ0cmFuc2FjdGlvbl9pZCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l3_payment": null, + "l3_checkout": null, + "l2_payment_serialized": null, + "l2_checkout_serialized": null, + "expected_l2_aud": "https://attacker.example.com", + "expected_l2_nonce": null, + "expected_l3_payment_aud": null, + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": false, + "expected_errors": [ + "L2 aud mismatch: expected 'https://attacker.example.com', got 'https://agent.example.com'" + ] + }, + { + "name": "immediate_wrong_nonce", + "current_time": 1750000260, + "issuer_public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImltbWVkaWF0ZS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImlhdCI6MTc1MDAwMDEwMCwic2RfaGFzaCI6ImpocEJ0blpTNkdobGJsTm1Xal8wTzRtTWduVHA3Y0w5dUFZeDZpdEd5dGsiLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJyY0FzMU0tcjJVV1paV0ZyQk5Qd2xFWVhiUWVreUQycW0xRVROVW1VME80In0seyIuLi4iOiJsYmhEQmszMGIwNlkwanV4cXhqeURILTVjZDFPUnpGTy1TN2JhVHl2WktFIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDAxMDAwLCJfc2QiOlsicmNBczFNLXIyVVdaWldGckJOUHdsRVlYYlFla3lEMnFtMUVUTlVtVTBPNCIsImxiaERCazMwYjA2WTBqdXhxeGp5REgtNWNkMU9SekZPLVM3YmFUeXZaS0UiXX0.QGdHm8bVsfwxuc8JvENWiRsz86H88pr_FRWFh8hsShgHnH1XSfQ5CePju5HxHDhU1ElaymwdrNfAh55qamAimA~WyIyci1Zdl95NzdjVTE2VWxMSlYtUUJnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~WyJSOXFvbnZXZVUwOWtlZmtmbWdFcWJnIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In0sInBheWVlIjp7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9LCJwYXltZW50X2Ftb3VudCI6eyJjdXJyZW5jeSI6IlVTRCIsImFtb3VudCI6Mjc5OTl9LCJ0cmFuc2FjdGlvbl9pZCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l3_payment": null, + "l3_checkout": null, + "l2_payment_serialized": null, + "l2_checkout_serialized": null, + "expected_l2_aud": null, + "expected_l2_nonce": "wrong-nonce", + "expected_l3_payment_aud": null, + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": false, + "expected_errors": [ + "L2 nonce mismatch: expected 'wrong-nonce', got 'immediate-nonce-0001'" + ] + }, + { + "name": "immediate_expired", + "current_time": 1751000260, + "issuer_public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImltbWVkaWF0ZS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImlhdCI6MTc1MDAwMDEwMCwic2RfaGFzaCI6ImpocEJ0blpTNkdobGJsTm1Xal8wTzRtTWduVHA3Y0w5dUFZeDZpdEd5dGsiLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJyY0FzMU0tcjJVV1paV0ZyQk5Qd2xFWVhiUWVreUQycW0xRVROVW1VME80In0seyIuLi4iOiJsYmhEQmszMGIwNlkwanV4cXhqeURILTVjZDFPUnpGTy1TN2JhVHl2WktFIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDAxMDAwLCJfc2QiOlsicmNBczFNLXIyVVdaWldGckJOUHdsRVlYYlFla3lEMnFtMUVUTlVtVTBPNCIsImxiaERCazMwYjA2WTBqdXhxeGp5REgtNWNkMU9SekZPLVM3YmFUeXZaS0UiXX0.QGdHm8bVsfwxuc8JvENWiRsz86H88pr_FRWFh8hsShgHnH1XSfQ5CePju5HxHDhU1ElaymwdrNfAh55qamAimA~WyIyci1Zdl95NzdjVTE2VWxMSlYtUUJnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~WyJSOXFvbnZXZVUwOWtlZmtmbWdFcWJnIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In0sInBheWVlIjp7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9LCJwYXltZW50X2Ftb3VudCI6eyJjdXJyZW5jeSI6IlVTRCIsImFtb3VudCI6Mjc5OTl9LCJ0cmFuc2FjdGlvbl9pZCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l3_payment": null, + "l3_checkout": null, + "l2_payment_serialized": null, + "l2_checkout_serialized": null, + "expected_l2_aud": null, + "expected_l2_nonce": null, + "expected_l3_payment_aud": null, + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": false, + "expected_errors": [ + "L2 expired at 1750001000" + ] + }, + { + "name": "immediate_iat_future", + "current_time": 1749000000, + "issuer_public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImltbWVkaWF0ZS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImlhdCI6MTc1MDAwMDEwMCwic2RfaGFzaCI6ImpocEJ0blpTNkdobGJsTm1Xal8wTzRtTWduVHA3Y0w5dUFZeDZpdEd5dGsiLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJyY0FzMU0tcjJVV1paV0ZyQk5Qd2xFWVhiUWVreUQycW0xRVROVW1VME80In0seyIuLi4iOiJsYmhEQmszMGIwNlkwanV4cXhqeURILTVjZDFPUnpGTy1TN2JhVHl2WktFIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDAxMDAwLCJfc2QiOlsicmNBczFNLXIyVVdaWldGckJOUHdsRVlYYlFla3lEMnFtMUVUTlVtVTBPNCIsImxiaERCazMwYjA2WTBqdXhxeGp5REgtNWNkMU9SekZPLVM3YmFUeXZaS0UiXX0.QGdHm8bVsfwxuc8JvENWiRsz86H88pr_FRWFh8hsShgHnH1XSfQ5CePju5HxHDhU1ElaymwdrNfAh55qamAimA~WyIyci1Zdl95NzdjVTE2VWxMSlYtUUJnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~WyJSOXFvbnZXZVUwOWtlZmtmbWdFcWJnIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In0sInBheWVlIjp7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9LCJwYXltZW50X2Ftb3VudCI6eyJjdXJyZW5jeSI6IlVTRCIsImFtb3VudCI6Mjc5OTl9LCJ0cmFuc2FjdGlvbl9pZCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l3_payment": null, + "l3_checkout": null, + "l2_payment_serialized": null, + "l2_checkout_serialized": null, + "expected_l2_aud": null, + "expected_l2_nonce": null, + "expected_l3_payment_aud": null, + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": false, + "expected_errors": [ + "L1 credential iat is in the future: 1750000000" + ] + }, + { + "name": "immediate_tampered_l1", + "current_time": 1750000260, + "issuer_public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.BmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImltbWVkaWF0ZS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImlhdCI6MTc1MDAwMDEwMCwic2RfaGFzaCI6ImpocEJ0blpTNkdobGJsTm1Xal8wTzRtTWduVHA3Y0w5dUFZeDZpdEd5dGsiLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJyY0FzMU0tcjJVV1paV0ZyQk5Qd2xFWVhiUWVreUQycW0xRVROVW1VME80In0seyIuLi4iOiJsYmhEQmszMGIwNlkwanV4cXhqeURILTVjZDFPUnpGTy1TN2JhVHl2WktFIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDAxMDAwLCJfc2QiOlsicmNBczFNLXIyVVdaWldGckJOUHdsRVlYYlFla3lEMnFtMUVUTlVtVTBPNCIsImxiaERCazMwYjA2WTBqdXhxeGp5REgtNWNkMU9SekZPLVM3YmFUeXZaS0UiXX0.QGdHm8bVsfwxuc8JvENWiRsz86H88pr_FRWFh8hsShgHnH1XSfQ5CePju5HxHDhU1ElaymwdrNfAh55qamAimA~WyIyci1Zdl95NzdjVTE2VWxMSlYtUUJnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~WyJSOXFvbnZXZVUwOWtlZmtmbWdFcWJnIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In0sInBheWVlIjp7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9LCJwYXltZW50X2Ftb3VudCI6eyJjdXJyZW5jeSI6IlVTRCIsImFtb3VudCI6Mjc5OTl9LCJ0cmFuc2FjdGlvbl9pZCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l3_payment": null, + "l3_checkout": null, + "l2_payment_serialized": null, + "l2_checkout_serialized": null, + "expected_l2_aud": null, + "expected_l2_nonce": null, + "expected_l3_payment_aud": null, + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": false, + "expected_errors": [ + "L1 signature verification failed" + ] + }, + { + "name": "immediate_missing_issuer_key", + "current_time": 1750000260, + "issuer_public": null, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImltbWVkaWF0ZS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImlhdCI6MTc1MDAwMDEwMCwic2RfaGFzaCI6ImpocEJ0blpTNkdobGJsTm1Xal8wTzRtTWduVHA3Y0w5dUFZeDZpdEd5dGsiLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJyY0FzMU0tcjJVV1paV0ZyQk5Qd2xFWVhiUWVreUQycW0xRVROVW1VME80In0seyIuLi4iOiJsYmhEQmszMGIwNlkwanV4cXhqeURILTVjZDFPUnpGTy1TN2JhVHl2WktFIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDAxMDAwLCJfc2QiOlsicmNBczFNLXIyVVdaWldGckJOUHdsRVlYYlFla3lEMnFtMUVUTlVtVTBPNCIsImxiaERCazMwYjA2WTBqdXhxeGp5REgtNWNkMU9SekZPLVM3YmFUeXZaS0UiXX0.QGdHm8bVsfwxuc8JvENWiRsz86H88pr_FRWFh8hsShgHnH1XSfQ5CePju5HxHDhU1ElaymwdrNfAh55qamAimA~WyIyci1Zdl95NzdjVTE2VWxMSlYtUUJnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~WyJSOXFvbnZXZVUwOWtlZmtmbWdFcWJnIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In0sInBheWVlIjp7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9LCJwYXltZW50X2Ftb3VudCI6eyJjdXJyZW5jeSI6IlVTRCIsImFtb3VudCI6Mjc5OTl9LCJ0cmFuc2FjdGlvbl9pZCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l3_payment": null, + "l3_checkout": null, + "l2_payment_serialized": null, + "l2_checkout_serialized": null, + "expected_l2_aud": null, + "expected_l2_nonce": null, + "expected_l3_payment_aud": null, + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": false, + "expected_errors": [ + "issuer_public_key is required for chain verification (pass skip_issuer_verification=True to bypass in tests)" + ] + }, + { + "name": "immediate_duplicate_l1_sd", + "current_time": 1750000260, + "issuer_public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiLCIya2NMS1lYQ3JraHYzdjVUQTQ1dkR5ZU01RnU0UVlPZnVMUEpSLW51NG9zIl0sIl9zZF9hbGciOiJzaGEtMjU2In0.jOE3tRqRsRJItUx59lc3LVyOzddnm17BcG5OQKBF-uPBovdcjVs9Vs2G4mRAAgwIPGn8ulsS7cMykAxnaKSvqA~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImltbWVkaWF0ZS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImlhdCI6MTc1MDAwMDEwMCwic2RfaGFzaCI6ImpocEJ0blpTNkdobGJsTm1Xal8wTzRtTWduVHA3Y0w5dUFZeDZpdEd5dGsiLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJyY0FzMU0tcjJVV1paV0ZyQk5Qd2xFWVhiUWVreUQycW0xRVROVW1VME80In0seyIuLi4iOiJsYmhEQmszMGIwNlkwanV4cXhqeURILTVjZDFPUnpGTy1TN2JhVHl2WktFIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDAxMDAwLCJfc2QiOlsicmNBczFNLXIyVVdaWldGckJOUHdsRVlYYlFla3lEMnFtMUVUTlVtVTBPNCIsImxiaERCazMwYjA2WTBqdXhxeGp5REgtNWNkMU9SekZPLVM3YmFUeXZaS0UiXX0.QGdHm8bVsfwxuc8JvENWiRsz86H88pr_FRWFh8hsShgHnH1XSfQ5CePju5HxHDhU1ElaymwdrNfAh55qamAimA~WyIyci1Zdl95NzdjVTE2VWxMSlYtUUJnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~WyJSOXFvbnZXZVUwOWtlZmtmbWdFcWJnIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In0sInBheWVlIjp7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9LCJwYXltZW50X2Ftb3VudCI6eyJjdXJyZW5jeSI6IlVTRCIsImFtb3VudCI6Mjc5OTl9LCJ0cmFuc2FjdGlvbl9pZCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l3_payment": null, + "l3_checkout": null, + "l2_payment_serialized": null, + "l2_checkout_serialized": null, + "expected_l2_aud": null, + "expected_l2_nonce": null, + "expected_l3_payment_aud": null, + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": false, + "expected_errors": [ + "L1 _sd contains a duplicate disclosure digest (RFC 9901 section 7.1): 2kcLKYXCrkhv3v5TA45vDyeM5Fu4QYOfuLPJR-nu4os" + ] + }, + { + "name": "autonomous_valid", + "current_time": 1750000260, + "issuer_public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJVeGRQY2lVTFp6SUNrTnU0MllBNk13Iix7ImlkIjoibWVyY2hhbnQtdXVpZC0yIiwibmFtZSI6IkJhYm9sYXQiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly9iYWJvbGF0LmNvbSJ9XQ~WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~WyJ1dXhkRW9wazBFNVdyVm1fM3pPVHl3Iix7ImlkIjoiSEVBMjMxMDIiLCJ0aXRsZSI6IkhlYWQgR3JhcGhlbmUgMzYwIFNwZWVkIn1d~WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~", + "l3_payment": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9uZXR3b3JrLm1hc3RlcmNhcmQuY29tIiwic2RfaGFzaCI6Imxsc3JIa21RcGprM2g4WWtrMnJqdWZqSThTd0ZRcEV5ZGxXWW5IV0I0YmciLCJpYXQiOjE3NTAwMDAyMDAsImRlbGVnYXRlX3BheWxvYWQiOlt7Ii4uLiI6IlNOdG4tcGI4R1BQcnNFS3lCeUNmVnBtdVo4TTlGRDdPLUxldC1jc192TlkifSx7Ii4uLiI6IlRkV0FDRTM2R3hSRkJSdlFHUUJ5NE1BM2lYMDZEbDBldXJyMWw0WVp2ZkUifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImV4cCI6MTc1MDAwMDUwMH0.FKr84Cmnjj-SsUlXNJwretTGjtLYGBiT73t89FF-fK70jvvZ9KWn0AnG86jVjQzXhKFCVHQAbsfFTmenoWGZ5g~WyJjQjdBN1Q5X1JNdWRaRmxfYWpHU2xRIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJwZXhPbW41WUEyU3dCSnI5X3RoRVRBIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwidHJhbnNhY3Rpb25faWQiOiI4NmplQjhmcVphNloxSE54bXhydmphRlZjaXVyVDRWTjJ1ZHJHNXN3NXpJIiwicGF5ZWUiOnsiaWQiOiJtZXJjaGFudC11dWlkLTEiLCJuYW1lIjoiVGVubmlzIFdhcmVob3VzZSIsIndlYnNpdGUiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIn0sInBheW1lbnRfYW1vdW50Ijp7ImN1cnJlbmN5IjoiVVNEIiwiYW1vdW50IjoyNzk5OX0sInBheW1lbnRfaW5zdHJ1bWVudCI6eyJ0eXBlIjoibWFzdGVyY2FyZC5zcmNEaWdpdGFsQ2FyZCIsImlkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiZGVzY3JpcHRpb24iOiJNYXN0ZXJjYXJkICoqKiogMTIzNCJ9fV0~", + "l3_checkout": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYi1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSIsInNkX2hhc2giOiJESjhsM2YxMDBlb242eGZlSHE2bEs1YVo1ZDc2U1JHQWlILXliNTBvVlAwIiwiaWF0IjoxNzUwMDAwMjAwLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJoNXo2bERvRmJ0VU1weHlZZ042VXBBRzBBcFpmR2J1XzYxVHdMbEJlU0lrIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJleHAiOjE3NTAwMDA1MDB9.SpZoGr9jaHf42Qu18IjWXrNqFmIzX2PUsmqQyTLJ16-pKGYdVCCl46mXbKw1JMoeb7fLUvL4SiiBTv0mXZBQrA~WyJsRFkzeVJFZElOOGZ6Q3pQU0hIRDdBIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l2_payment_serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~", + "l2_checkout_serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~", + "expected_l2_aud": "https://agent.example.com", + "expected_l2_nonce": "autonomous-nonce-0001", + "expected_l3_payment_aud": "https://network.mastercard.com", + "expected_l3_payment_nonce": "l3a-nonce-0001", + "expected_l3_checkout_aud": "https://tennis-warehouse.com", + "expected_l3_checkout_nonce": "l3b-nonce-0001", + "expected_valid": true, + "expected_errors": [] + }, + { + "name": "autonomous_swapped_l3", + "current_time": 1750000260, + "issuer_public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJVeGRQY2lVTFp6SUNrTnU0MllBNk13Iix7ImlkIjoibWVyY2hhbnQtdXVpZC0yIiwibmFtZSI6IkJhYm9sYXQiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly9iYWJvbGF0LmNvbSJ9XQ~WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~WyJ1dXhkRW9wazBFNVdyVm1fM3pPVHl3Iix7ImlkIjoiSEVBMjMxMDIiLCJ0aXRsZSI6IkhlYWQgR3JhcGhlbmUgMzYwIFNwZWVkIn1d~WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~", + "l3_payment": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9uZXR3b3JrLm1hc3RlcmNhcmQuY29tIiwic2RfaGFzaCI6Imxsc3JIa21RcGprM2g4WWtrMnJqdWZqSThTd0ZRcEV5ZGxXWW5IV0I0YmciLCJpYXQiOjE3NTAwMDAyMDAsImRlbGVnYXRlX3BheWxvYWQiOlt7Ii4uLiI6IlNOdG4tcGI4R1BQcnNFS3lCeUNmVnBtdVo4TTlGRDdPLUxldC1jc192TlkifSx7Ii4uLiI6IlRkV0FDRTM2R3hSRkJSdlFHUUJ5NE1BM2lYMDZEbDBldXJyMWw0WVp2ZkUifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImV4cCI6MTc1MDAwMDUwMH0.FKr84Cmnjj-SsUlXNJwretTGjtLYGBiT73t89FF-fK70jvvZ9KWn0AnG86jVjQzXhKFCVHQAbsfFTmenoWGZ5g~WyJjQjdBN1Q5X1JNdWRaRmxfYWpHU2xRIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJwZXhPbW41WUEyU3dCSnI5X3RoRVRBIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwidHJhbnNhY3Rpb25faWQiOiI4NmplQjhmcVphNloxSE54bXhydmphRlZjaXVyVDRWTjJ1ZHJHNXN3NXpJIiwicGF5ZWUiOnsiaWQiOiJtZXJjaGFudC11dWlkLTEiLCJuYW1lIjoiVGVubmlzIFdhcmVob3VzZSIsIndlYnNpdGUiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIn0sInBheW1lbnRfYW1vdW50Ijp7ImN1cnJlbmN5IjoiVVNEIiwiYW1vdW50IjoyNzk5OX0sInBheW1lbnRfaW5zdHJ1bWVudCI6eyJ0eXBlIjoibWFzdGVyY2FyZC5zcmNEaWdpdGFsQ2FyZCIsImlkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiZGVzY3JpcHRpb24iOiJNYXN0ZXJjYXJkICoqKiogMTIzNCJ9fV0~", + "l3_checkout": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYi1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSIsInNkX2hhc2giOiJESjhsM2YxMDBlb242eGZlSHE2bEs1YVo1ZDc2U1JHQWlILXliNTBvVlAwIiwiaWF0IjoxNzUwMDAwMjAwLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJoNXo2bERvRmJ0VU1weHlZZ042VXBBRzBBcFpmR2J1XzYxVHdMbEJlU0lrIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJleHAiOjE3NTAwMDA1MDB9.SpZoGr9jaHf42Qu18IjWXrNqFmIzX2PUsmqQyTLJ16-pKGYdVCCl46mXbKw1JMoeb7fLUvL4SiiBTv0mXZBQrA~WyJsRFkzeVJFZElOOGZ6Q3pQU0hIRDdBIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l2_payment_serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~", + "l2_checkout_serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~", + "expected_l2_aud": null, + "expected_l2_nonce": null, + "expected_l3_payment_aud": null, + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": false, + "expected_errors": [ + "L3a (payment) sd_hash does not match L2 serialized form" + ] + }, + { + "name": "autonomous_wrong_l3_aud", + "current_time": 1750000260, + "issuer_public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJVeGRQY2lVTFp6SUNrTnU0MllBNk13Iix7ImlkIjoibWVyY2hhbnQtdXVpZC0yIiwibmFtZSI6IkJhYm9sYXQiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly9iYWJvbGF0LmNvbSJ9XQ~WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~WyJ1dXhkRW9wazBFNVdyVm1fM3pPVHl3Iix7ImlkIjoiSEVBMjMxMDIiLCJ0aXRsZSI6IkhlYWQgR3JhcGhlbmUgMzYwIFNwZWVkIn1d~WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~", + "l3_payment": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9uZXR3b3JrLm1hc3RlcmNhcmQuY29tIiwic2RfaGFzaCI6Imxsc3JIa21RcGprM2g4WWtrMnJqdWZqSThTd0ZRcEV5ZGxXWW5IV0I0YmciLCJpYXQiOjE3NTAwMDAyMDAsImRlbGVnYXRlX3BheWxvYWQiOlt7Ii4uLiI6IlNOdG4tcGI4R1BQcnNFS3lCeUNmVnBtdVo4TTlGRDdPLUxldC1jc192TlkifSx7Ii4uLiI6IlRkV0FDRTM2R3hSRkJSdlFHUUJ5NE1BM2lYMDZEbDBldXJyMWw0WVp2ZkUifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImV4cCI6MTc1MDAwMDUwMH0.FKr84Cmnjj-SsUlXNJwretTGjtLYGBiT73t89FF-fK70jvvZ9KWn0AnG86jVjQzXhKFCVHQAbsfFTmenoWGZ5g~WyJjQjdBN1Q5X1JNdWRaRmxfYWpHU2xRIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJwZXhPbW41WUEyU3dCSnI5X3RoRVRBIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwidHJhbnNhY3Rpb25faWQiOiI4NmplQjhmcVphNloxSE54bXhydmphRlZjaXVyVDRWTjJ1ZHJHNXN3NXpJIiwicGF5ZWUiOnsiaWQiOiJtZXJjaGFudC11dWlkLTEiLCJuYW1lIjoiVGVubmlzIFdhcmVob3VzZSIsIndlYnNpdGUiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIn0sInBheW1lbnRfYW1vdW50Ijp7ImN1cnJlbmN5IjoiVVNEIiwiYW1vdW50IjoyNzk5OX0sInBheW1lbnRfaW5zdHJ1bWVudCI6eyJ0eXBlIjoibWFzdGVyY2FyZC5zcmNEaWdpdGFsQ2FyZCIsImlkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiZGVzY3JpcHRpb24iOiJNYXN0ZXJjYXJkICoqKiogMTIzNCJ9fV0~", + "l3_checkout": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYi1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSIsInNkX2hhc2giOiJESjhsM2YxMDBlb242eGZlSHE2bEs1YVo1ZDc2U1JHQWlILXliNTBvVlAwIiwiaWF0IjoxNzUwMDAwMjAwLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJoNXo2bERvRmJ0VU1weHlZZ042VXBBRzBBcFpmR2J1XzYxVHdMbEJlU0lrIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJleHAiOjE3NTAwMDA1MDB9.SpZoGr9jaHf42Qu18IjWXrNqFmIzX2PUsmqQyTLJ16-pKGYdVCCl46mXbKw1JMoeb7fLUvL4SiiBTv0mXZBQrA~WyJsRFkzeVJFZElOOGZ6Q3pQU0hIRDdBIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l2_payment_serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~", + "l2_checkout_serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~", + "expected_l2_aud": null, + "expected_l2_nonce": null, + "expected_l3_payment_aud": "https://attacker.example.com", + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": false, + "expected_errors": [ + "L3a (payment) aud mismatch: expected 'https://attacker.example.com', got 'https://network.mastercard.com'" + ] + }, + { + "name": "autonomous_l3_expired", + "current_time": 1751000260, + "issuer_public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJVeGRQY2lVTFp6SUNrTnU0MllBNk13Iix7ImlkIjoibWVyY2hhbnQtdXVpZC0yIiwibmFtZSI6IkJhYm9sYXQiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly9iYWJvbGF0LmNvbSJ9XQ~WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~WyJ1dXhkRW9wazBFNVdyVm1fM3pPVHl3Iix7ImlkIjoiSEVBMjMxMDIiLCJ0aXRsZSI6IkhlYWQgR3JhcGhlbmUgMzYwIFNwZWVkIn1d~WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~", + "l3_payment": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9uZXR3b3JrLm1hc3RlcmNhcmQuY29tIiwic2RfaGFzaCI6Imxsc3JIa21RcGprM2g4WWtrMnJqdWZqSThTd0ZRcEV5ZGxXWW5IV0I0YmciLCJpYXQiOjE3NTAwMDAyMDAsImRlbGVnYXRlX3BheWxvYWQiOlt7Ii4uLiI6IlNOdG4tcGI4R1BQcnNFS3lCeUNmVnBtdVo4TTlGRDdPLUxldC1jc192TlkifSx7Ii4uLiI6IlRkV0FDRTM2R3hSRkJSdlFHUUJ5NE1BM2lYMDZEbDBldXJyMWw0WVp2ZkUifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImV4cCI6MTc1MDAwMDUwMH0.FKr84Cmnjj-SsUlXNJwretTGjtLYGBiT73t89FF-fK70jvvZ9KWn0AnG86jVjQzXhKFCVHQAbsfFTmenoWGZ5g~WyJjQjdBN1Q5X1JNdWRaRmxfYWpHU2xRIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJwZXhPbW41WUEyU3dCSnI5X3RoRVRBIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwidHJhbnNhY3Rpb25faWQiOiI4NmplQjhmcVphNloxSE54bXhydmphRlZjaXVyVDRWTjJ1ZHJHNXN3NXpJIiwicGF5ZWUiOnsiaWQiOiJtZXJjaGFudC11dWlkLTEiLCJuYW1lIjoiVGVubmlzIFdhcmVob3VzZSIsIndlYnNpdGUiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIn0sInBheW1lbnRfYW1vdW50Ijp7ImN1cnJlbmN5IjoiVVNEIiwiYW1vdW50IjoyNzk5OX0sInBheW1lbnRfaW5zdHJ1bWVudCI6eyJ0eXBlIjoibWFzdGVyY2FyZC5zcmNEaWdpdGFsQ2FyZCIsImlkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiZGVzY3JpcHRpb24iOiJNYXN0ZXJjYXJkICoqKiogMTIzNCJ9fV0~", + "l3_checkout": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYi1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSIsInNkX2hhc2giOiJESjhsM2YxMDBlb242eGZlSHE2bEs1YVo1ZDc2U1JHQWlILXliNTBvVlAwIiwiaWF0IjoxNzUwMDAwMjAwLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJoNXo2bERvRmJ0VU1weHlZZ042VXBBRzBBcFpmR2J1XzYxVHdMbEJlU0lrIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJleHAiOjE3NTAwMDA1MDB9.SpZoGr9jaHf42Qu18IjWXrNqFmIzX2PUsmqQyTLJ16-pKGYdVCCl46mXbKw1JMoeb7fLUvL4SiiBTv0mXZBQrA~WyJsRFkzeVJFZElOOGZ6Q3pQU0hIRDdBIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l2_payment_serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~", + "l2_checkout_serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~", + "expected_l2_aud": null, + "expected_l2_nonce": null, + "expected_l3_payment_aud": null, + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": false, + "expected_errors": [ + "L2 expired at 1750086500" + ] + }, + { + "name": "autonomous_skip_issuer_ok", + "current_time": 1750000260, + "issuer_public": null, + "skip_issuer_verification": true, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJVeGRQY2lVTFp6SUNrTnU0MllBNk13Iix7ImlkIjoibWVyY2hhbnQtdXVpZC0yIiwibmFtZSI6IkJhYm9sYXQiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly9iYWJvbGF0LmNvbSJ9XQ~WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~WyJ1dXhkRW9wazBFNVdyVm1fM3pPVHl3Iix7ImlkIjoiSEVBMjMxMDIiLCJ0aXRsZSI6IkhlYWQgR3JhcGhlbmUgMzYwIFNwZWVkIn1d~WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~", + "l3_payment": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYS1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly9uZXR3b3JrLm1hc3RlcmNhcmQuY29tIiwic2RfaGFzaCI6Imxsc3JIa21RcGprM2g4WWtrMnJqdWZqSThTd0ZRcEV5ZGxXWW5IV0I0YmciLCJpYXQiOjE3NTAwMDAyMDAsImRlbGVnYXRlX3BheWxvYWQiOlt7Ii4uLiI6IlNOdG4tcGI4R1BQcnNFS3lCeUNmVnBtdVo4TTlGRDdPLUxldC1jc192TlkifSx7Ii4uLiI6IlRkV0FDRTM2R3hSRkJSdlFHUUJ5NE1BM2lYMDZEbDBldXJyMWw0WVp2ZkUifV0sIl9zZF9hbGciOiJzaGEtMjU2IiwiaXNzIjoiaHR0cHM6Ly9hZ2VudC5leGFtcGxlLmNvbSIsImV4cCI6MTc1MDAwMDUwMH0.FKr84Cmnjj-SsUlXNJwretTGjtLYGBiT73t89FF-fK70jvvZ9KWn0AnG86jVjQzXhKFCVHQAbsfFTmenoWGZ5g~WyJjQjdBN1Q5X1JNdWRaRmxfYWpHU2xRIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJwZXhPbW41WUEyU3dCSnI5X3RoRVRBIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC4xIiwidHJhbnNhY3Rpb25faWQiOiI4NmplQjhmcVphNloxSE54bXhydmphRlZjaXVyVDRWTjJ1ZHJHNXN3NXpJIiwicGF5ZWUiOnsiaWQiOiJtZXJjaGFudC11dWlkLTEiLCJuYW1lIjoiVGVubmlzIFdhcmVob3VzZSIsIndlYnNpdGUiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIn0sInBheW1lbnRfYW1vdW50Ijp7ImN1cnJlbmN5IjoiVVNEIiwiYW1vdW50IjoyNzk5OX0sInBheW1lbnRfaW5zdHJ1bWVudCI6eyJ0eXBlIjoibWFzdGVyY2FyZC5zcmNEaWdpdGFsQ2FyZCIsImlkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiZGVzY3JpcHRpb24iOiJNYXN0ZXJjYXJkICoqKiogMTIzNCJ9fV0~", + "l3_checkout": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCIsImtpZCI6ImFnZW50LWtleS0xIn0.eyJub25jZSI6ImwzYi1ub25jZS0wMDAxIiwiYXVkIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSIsInNkX2hhc2giOiJESjhsM2YxMDBlb242eGZlSHE2bEs1YVo1ZDc2U1JHQWlILXliNTBvVlAwIiwiaWF0IjoxNzUwMDAwMjAwLCJkZWxlZ2F0ZV9wYXlsb2FkIjpbeyIuLi4iOiJoNXo2bERvRmJ0VU1weHlZZ042VXBBRzBBcFpmR2J1XzYxVHdMbEJlU0lrIn1dLCJfc2RfYWxnIjoic2hhLTI1NiIsImlzcyI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJleHAiOjE3NTAwMDA1MDB9.SpZoGr9jaHf42Qu18IjWXrNqFmIzX2PUsmqQyTLJ16-pKGYdVCCl46mXbKw1JMoeb7fLUvL4SiiBTv0mXZBQrA~WyJsRFkzeVJFZElOOGZ6Q3pQU0hIRDdBIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQuMSIsImNoZWNrb3V0X2p3dCI6ImV5SmhiR2NpT2lKRlV6STFOaUlzSW5SNWNDSTZJa3BYVkNJc0ltdHBaQ0k2SW0xbGNtTm9ZVzUwTFd0bGVTMHhJbjAuZXlKcGMzTWlPaUpvZEhSd2N6b3ZMM1JsYm01cGN5MTNZWEpsYUc5MWMyVXVZMjl0SWl3aWMzVmlJam9pWTJGeWRGOWphR1ZqYTI5MWRDSXNJbWxoZENJNk1UYzFNREF3TURBMU1Dd2laWGh3SWpveE56VXdNREF6TmpVd0xDSmpZWEowSWpwN0ltbDBaVzF6SWpwYmV5SnphM1VpT2lKQ1FVSTROak0wTlNJc0ltNWhiV1VpT2lKQ1lXSnZiR0YwSUZCMWNtVWdRV1Z5YnlCVVpXNXVhWE1nVW1GamEyVjBJaXdpYzJsNlpTSTZNeXdpYzJsNlpWOXNZV0psYkNJNklqUWdNeTg0SWl3aVkyOXNiM0lpT2lKM2FHbDBaU0lzSW5GMVlXNTBhWFI1SWpveExDSjFibWwwVUhKcFkyVWlPakkzT1M0NU9YMWRMQ0p6ZFdKVWIzUmhiQ0k2ZXlKaGJXOTFiblFpT2pJM09TNDVPU3dpWTNWeWNtVnVZM2xEYjJSbElqb2lWVk5FSW4xOWZRLnAyMGVHdFpqU3VCNGtNY2FFbmFibmEwa0RTcmRycE94QXdnWENIQkxjTEJSeHFLUEFJTWh4LS1VM0gzbUc3SlVGQVEzQVJKWXdqdnJETW9HZTFtRm5nIiwiY2hlY2tvdXRfaGFzaCI6Ijg2amVCOGZxWmE2WjFITnhteHJ2amFGVmNpdXJUNFZOMnVkckc1c3c1ekkifV0~", + "l2_payment_serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJTclg2Szg2TmxaOFd2V0dkdzdRUjd3Iix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiMGFyWndQYjBCR3dfbGdhVTBRcnlsTjh0djU2ZnVLUEp4NnRaRFNBbFM5MCIsInkiOiJtOXNtNXdGZENmd0N5NlVjZjYyMW5faGRSdF9mZDlac3NQLVRWdFA4WTJrIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~WyJadWlVUjluNXQwdjNGNmF4bjNUSC1BIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~", + "l2_checkout_serialized": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtbm9uY2UtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoicmhVUldaTVk4Y01DamdXbUhFRnF0Z2RnMWNxbmFXb3FDVnhxRW5remNSNCJ9LHsiLi4uIjoiZTN5bWg0YWE2cXhMWmxFRGtfM3BFUmJxQ09EUlFqN3FmWEVQYjZHVmtudyJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsidk1TMHNDSV9yeE9rbXkwWjNFWndfRFdZenF1U3ZzZzVGcEJuYmJDNm9TUSIsIm1LNzBYNGZSaTFoQXNqeE9JRkdVTi03bUpCYWhmUVVMN0NOX2h6ZXM3TkUiLCJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIiwiTFZHWmZWZWZRbjk3TU5ILThsSGFUWEJjbnlzNzE1MndEUnN0QWd1Y2tIMCIsInJoVVJXWk1ZOGNNQ2pnV21IRUZxdGdkZzFjcW5hV29xQ1Z4cUVua3pjUjQiLCJlM3ltaDRhYTZxeExabEVEa18zcEVSYnFDT0RSUWo3cWZYRVBiNkdWa253Il0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ._8N51mlG64O6hb9QsFHHVj6e4EuLQ4aMu5nb5J7t9guf0FhUHRllByGE71Bvm5o9PAJhjHnrOWbqZnlaLWyy_Q~WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJIamJhYkNyMl93Vnl6dzIyNFRFTXBnIix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~", + "expected_l2_aud": null, + "expected_l2_nonce": null, + "expected_l3_payment_aud": null, + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": true, + "expected_errors": [] + }, + { + "name": "autonomous_split_agent", + "current_time": 1750000260, + "issuer_public": { + "kty": "EC", + "crv": "P-256", + "x": "p_QdCPhvJWMo2e47d25qTkJYQi4VZ1Iw7Dn5E_aiM_o", + "y": "lrbLQN-m2MGs8BMLB1bHqh2MlCty6Ue_YVRzItSwxZI" + }, + "skip_issuer_verification": false, + "l1": "eyJhbGciOiJFUzI1NiIsInR5cCI6InNkK2p3dCIsImtpZCI6Im1hc3RlcmNhcmQtaXNzdWVyLWtleS0xIn0.eyJpc3MiOiJodHRwczovL2lzc3Vlci5tYXN0ZXJjYXJkLmNvbSIsInN1YiI6InVzZXItc3ViamVjdC0xMjM0NSIsImlhdCI6MTc1MDAwMDAwMCwiZXhwIjoxNzgxNTM2MDAwLCJ2Y3QiOiJodHRwczovL2NyZWRlbnRpYWxzLm1hc3RlcmNhcmQuY29tL2NhcmQiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4Ijoib1NJWUVSNGNhN1hfbTFEZHlsRjl5RjR3dzZTMTZCRUZOV3VjWDhxazBZRSIsInkiOiJSc29jT0RJcEJXZ1g1Y3VUSExEeU85azA3cTVpWUNiZGsxaFRKUThOTDlJIn19LCJwYW5fbGFzdF9mb3VyIjoiMTIzNCIsInNjaGVtZSI6Im1hc3RlcmNhcmQiLCJjYXJkX2lkIjoiZjE5OWMzZGQtNzEwNi00NzhiLTliNWYtN2FmOWNhNzI1MTcwIiwiX3NkIjpbIjJrY0xLWVhDcmtodjN2NVRBNDV2RHllTTVGdTRRWU9mdUxQSlItbnU0b3MiXSwiX3NkX2FsZyI6InNoYS0yNTYifQ.LmV6prkmQncMvH9HDg3_B719_D36zun5teq5RsibOzRAdplUdSNTXZG8Ggass946zYnQwhSuSWwO0gQT4PMBbw~WyJNZHI2dFZMaFdYRXZaYWlNUnNuSHdBIiwiZW1haWwiLCJ1c2VyQGV4YW1wbGUuY29tIl0~", + "l2": "eyJhbGciOiJFUzI1NiIsInR5cCI6ImtiLXNkLWp3dCtrYiIsImtpZCI6InVzZXItZGV2aWNlLWtleS0xIn0.eyJub25jZSI6ImF1dG9ub21vdXMtc3BsaXQtYWdlbnQtMDAwMSIsImF1ZCI6Imh0dHBzOi8vYWdlbnQuZXhhbXBsZS5jb20iLCJpYXQiOjE3NTAwMDAxMDAsInNkX2hhc2giOiJqaHBCdG5aUzZHaGxibE5tV2pfME80bU1nblRwN2NMOXVBWXg2aXRHeXRrIiwiZGVsZWdhdGVfcGF5bG9hZCI6W3siLi4uIjoiajF0b1ZJcVJaVHU2NjZ2YVZqVkdjUEJLSVc3ejVwWnRESFQ5c2liVlVtYyJ9LHsiLi4uIjoibDFHMFk3VW5JaDRoMExpUk12MFhCNTVMVnFNV1B4ZGZHTGcxYm5oNjR5byJ9XSwiX3NkX2FsZyI6InNoYS0yNTYiLCJfc2QiOlsiQUhFejZPSjFJWEptVk5Cc2JlQVNYb3JMVlhFUmJsMV9Sa2ZXVHpHSXNoOCIsIkduZFlDOVd2ZnRUOW81YkJRcm44TWRVMW9XaW9lVHRxSTA1SkRnMmEyaFUiLCJWVG9sR0hCOUtHTDBsQjk5Q3p3clVpal9yaTNlUWZDWlg0alNXQWU5WDlzIiwiSi05Zlh2VklFNmk2bVVKTGM2cDNkYTlTZDRlNnpBMm9pQ1dvYTdDRU02TSIsImoxdG9WSXFSWlR1NjY2dmFWalZHY1BCS0lXN3o1cFp0REhUOXNpYlZVbWMiLCJsMUcwWTdVbkloNGgwTGlSTXYwWEI1NUxWcU1XUHhkZkdMZzFibmg2NHlvIl0sImlzcyI6Imh0dHBzOi8vd2FsbGV0LmV4YW1wbGUuY29tIiwiZXhwIjoxNzUwMDg2NTAwfQ.TIVD18FIxhB6n6HZX-2HkTJmoHeZ3VnIqm9L-pN6O8fSNCRq4KFRykoLpi9Zvy1SsrXJbeLZCyjyem_ztG9JBQ~WyI0cXA3MXByclFOaUVhZWxKX1dFaWJRIix7ImlkIjoibWVyY2hhbnQtdXVpZC0xIiwibmFtZSI6IlRlbm5pcyBXYXJlaG91c2UiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly90ZW5uaXMtd2FyZWhvdXNlLmNvbSJ9XQ~WyJ2TVZfVldyMVNSR0pCTkFud1ZjbE1nIix7ImlkIjoibWVyY2hhbnQtdXVpZC0yIiwibmFtZSI6IkJhYm9sYXQiLCJ3ZWJzaXRlIjoiaHR0cHM6Ly9iYWJvbGF0LmNvbSJ9XQ~WyJsdUJhUDVIV3pqNTZQaUhTbkFfQjV3Iix7ImlkIjoiQkFCODYzNDUiLCJ0aXRsZSI6IkJhYm9sYXQgUHVyZSBBZXJvIFRlbm5pcyBSYWNrZXQifV0~WyJvWGVJRVNwYk8zWjVSVzZpdW1XR2JBIix7ImlkIjoiSEVBMjMxMDIiLCJ0aXRsZSI6IkhlYWQgR3JhcGhlbmUgMzYwIFNwZWVkIn1d~WyJYXzl1STZqRThUeHRRdEhkMXFCbnVnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJBSEV6Nk9KMUlYSm1WTkJzYmVBU1hvckxWWEVSYmwxX1JrZldUekdJc2g4In0seyIuLi4iOiJHbmRZQzlXdmZ0VDlvNWJCUXJuOE1kVTFvV2lvZVR0cUkwNUpEZzJhMmhVIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJWVG9sR0hCOUtHTDBsQjk5Q3p3clVpal9yaTNlUWZDWlg0alNXQWU5WDlzIn0seyIuLi4iOiJKLTlmWHZWSUU2aTZtVUpMYzZwM2RhOVNkNGU2ekEyb2lDV29hN0NFTTZNIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d~WyJKZnVoYzk1QVhucVdxLTNYYVhMSThRIix7InZjdCI6Im1hbmRhdGUucGF5bWVudC5vcGVuLjEiLCJjbmYiOnsiandrIjp7Imt0eSI6IkVDIiwiY3J2IjoiUC0yNTYiLCJ4IjoiNUhCOE1KTE5ScEFRYUxUcGFXa0ROeGhId1FwQXpPN3JXdWFtcm0tUlBJMCIsInkiOiJXNVgxN0gxZ1YtYlA0NlFKS0x3X2NrbE5kbWJ6azdMYmFIaE5QNVlMeVVFIiwia2lkIjoiYWdlbnQta2V5LTEifX0sImNvbnN0cmFpbnRzIjpbeyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFsbG93ZWRfcGF5ZWVzIiwiYWxsb3dlZCI6W3siLi4uIjoiQUhFejZPSjFJWEptVk5Cc2JlQVNYb3JMVlhFUmJsMV9Sa2ZXVHpHSXNoOCJ9XX0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LmFtb3VudF9yYW5nZSIsImN1cnJlbmN5IjoiVVNEIiwibWluIjoxMDAwMCwibWF4Ijo0MDAwMH0seyJ0eXBlIjoibWFuZGF0ZS5wYXltZW50LnJlZmVyZW5jZSIsImNvbmRpdGlvbmFsX3RyYW5zYWN0aW9uX2lkIjoiajF0b1ZJcVJaVHU2NjZ2YVZqVkdjUEJLSVc3ejVwWnRESFQ5c2liVlVtYyJ9XSwicGF5bWVudF9pbnN0cnVtZW50Ijp7InR5cGUiOiJtYXN0ZXJjYXJkLnNyY0RpZ2l0YWxDYXJkIiwiaWQiOiJmMTk5YzNkZC03MTA2LTQ3OGItOWI1Zi03YWY5Y2E3MjUxNzAiLCJkZXNjcmlwdGlvbiI6Ik1hc3RlcmNhcmQgKioqKiAxMjM0In19XQ~", + "l3_payment": null, + "l3_checkout": null, + "l2_payment_serialized": null, + "l2_checkout_serialized": null, + "expected_l2_aud": null, + "expected_l2_nonce": null, + "expected_l3_payment_aud": null, + "expected_l3_payment_nonce": null, + "expected_l3_checkout_aud": null, + "expected_l3_checkout_nonce": null, + "expected_valid": false, + "expected_errors": [ + "L2 mandate cnf.jwk values must be identical across all pairs but differ" + ] + } + ], + "constraint_cases": [ + { + "name": "amount_ok", + "constraints": [ + { + "type": "mandate.payment.amount_range", + "currency": "USD", + "min": 10000, + "max": 40000 + } + ], + "fulfillment": { + "payment_amount": { + "currency": "USD", + "amount": 27999 + } + }, + "mode": "permissive", + "is_open_mandate": false, + "expected": { + "satisfied": true, + "violations": 0, + "checked": 1, + "skipped": 0 + } + }, + { + "name": "amount_over_max", + "constraints": [ + { + "type": "mandate.payment.amount_range", + "currency": "USD", + "min": 10000, + "max": 40000 + } + ], + "fulfillment": { + "payment_amount": { + "currency": "USD", + "amount": 50000 + } + }, + "mode": "permissive", + "is_open_mandate": false, + "expected": { + "satisfied": false, + "violations": 1, + "checked": 1, + "skipped": 0 + } + }, + { + "name": "amount_under_min", + "constraints": [ + { + "type": "mandate.payment.amount_range", + "currency": "USD", + "min": 10000, + "max": 40000 + } + ], + "fulfillment": { + "payment_amount": { + "currency": "USD", + "amount": 5000 + } + }, + "mode": "permissive", + "is_open_mandate": false, + "expected": { + "satisfied": false, + "violations": 1, + "checked": 1, + "skipped": 0 + } + }, + { + "name": "amount_wrong_currency", + "constraints": [ + { + "type": "mandate.payment.amount_range", + "currency": "USD", + "min": 10000, + "max": 40000 + } + ], + "fulfillment": { + "payment_amount": { + "currency": "EUR", + "amount": 27999 + } + }, + "mode": "permissive", + "is_open_mandate": false, + "expected": { + "satisfied": false, + "violations": 1, + "checked": 1, + "skipped": 0 + } + }, + { + "name": "payee_ok", + "constraints": [ + { + "type": "mandate.payment.allowed_payees", + "allowed": [ + { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + } + ] + } + ], + "fulfillment": { + "payee": { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + } + }, + "mode": "permissive", + "is_open_mandate": false, + "expected": { + "satisfied": true, + "violations": 0, + "checked": 1, + "skipped": 0 + } + }, + { + "name": "payee_not_allowed", + "constraints": [ + { + "type": "mandate.payment.allowed_payees", + "allowed": [ + { + "id": "merchant-uuid-1", + "name": "Tennis Warehouse", + "website": "https://tennis-warehouse.com" + } + ] + } + ], + "fulfillment": { + "payee": { + "id": "merchant-uuid-2", + "name": "Babolat", + "website": "https://babolat.com" + } + }, + "mode": "permissive", + "is_open_mandate": false, + "expected": { + "satisfied": false, + "violations": 1, + "checked": 1, + "skipped": 0 + } + }, + { + "name": "line_items_ok", + "constraints": [ + { + "type": "mandate.checkout.line_items", + "items": [ + { + "id": "line-1", + "acceptable_items": [ + { + "id": "BAB86345", + "title": "Babolat Pure Aero Tennis Racket" + } + ], + "quantity": 2 + } + ], + "match_mode": "minimum" + } + ], + "fulfillment": { + "line_items": [ + { + "id": "BAB86345", + "quantity": 1 + } + ] + }, + "mode": "permissive", + "is_open_mandate": false, + "expected": { + "satisfied": true, + "violations": 0, + "checked": 1, + "skipped": 0 + } + }, + { + "name": "line_items_over_qty", + "constraints": [ + { + "type": "mandate.checkout.line_items", + "items": [ + { + "id": "line-1", + "acceptable_items": [ + { + "id": "BAB86345", + "title": "Babolat Pure Aero Tennis Racket" + } + ], + "quantity": 2 + } + ], + "match_mode": "minimum" + } + ], + "fulfillment": { + "line_items": [ + { + "id": "BAB86345", + "quantity": 5 + } + ] + }, + "mode": "permissive", + "is_open_mandate": false, + "expected": { + "satisfied": false, + "violations": 2, + "checked": 1, + "skipped": 0 + } + }, + { + "name": "line_items_wrong_id", + "constraints": [ + { + "type": "mandate.checkout.line_items", + "items": [ + { + "id": "line-1", + "acceptable_items": [ + { + "id": "BAB86345", + "title": "Babolat Pure Aero Tennis Racket" + } + ], + "quantity": 2 + } + ], + "match_mode": "minimum" + } + ], + "fulfillment": { + "line_items": [ + { + "id": "ZZZ99999", + "quantity": 1 + } + ] + }, + "mode": "permissive", + "is_open_mandate": false, + "expected": { + "satisfied": false, + "violations": 1, + "checked": 1, + "skipped": 0 + } + }, + { + "name": "unknown_permissive", + "constraints": [ + { + "type": "mandate.custom.foo", + "bar": 1 + } + ], + "fulfillment": {}, + "mode": "permissive", + "is_open_mandate": false, + "expected": { + "satisfied": true, + "violations": 0, + "checked": 0, + "skipped": 1 + } + }, + { + "name": "unknown_strict", + "constraints": [ + { + "type": "mandate.custom.foo", + "bar": 1 + } + ], + "fulfillment": {}, + "mode": "strict", + "is_open_mandate": false, + "expected": { + "satisfied": false, + "violations": 1, + "checked": 0, + "skipped": 0 + } + }, + { + "name": "unknown_open_rejected", + "constraints": [ + { + "type": "mandate.custom.foo", + "bar": 1 + } + ], + "fulfillment": {}, + "mode": "permissive", + "is_open_mandate": true, + "expected": { + "satisfied": false, + "violations": 1, + "checked": 0, + "skipped": 0 + } + } + ], + "integrity_cases": [ + { + "name": "checkout_hash_ok", + "kind": "checkout_hash", + "checkout_mandate": { + "vct": "mandate.checkout.1", + "checkout_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1lcmNoYW50LWtleS0xIn0.eyJpc3MiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIiwic3ViIjoiY2FydF9jaGVja291dCIsImlhdCI6MTc1MDAwMDA1MCwiZXhwIjoxNzUwMDAzNjUwLCJjYXJ0Ijp7Iml0ZW1zIjpbeyJza3UiOiJCQUI4NjM0NSIsIm5hbWUiOiJCYWJvbGF0IFB1cmUgQWVybyBUZW5uaXMgUmFja2V0Iiwic2l6ZSI6Mywic2l6ZV9sYWJlbCI6IjQgMy84IiwiY29sb3IiOiJ3aGl0ZSIsInF1YW50aXR5IjoxLCJ1bml0UHJpY2UiOjI3OS45OX1dLCJzdWJUb3RhbCI6eyJhbW91bnQiOjI3OS45OSwiY3VycmVuY3lDb2RlIjoiVVNEIn19fQ.p20eGtZjSuB4kMcaEnabna0kDSrdrpOxAwgXCHBLcLBRxqKPAIMhx--U3H3mG7JUFAQ3ARJYwjvrDMoGe1mFng", + "checkout_hash": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + }, + "payment_mandate": { + "vct": "mandate.payment.1", + "transaction_id": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + }, + "expected_valid": true + }, + { + "name": "checkout_hash_mismatch", + "kind": "checkout_hash", + "checkout_mandate": { + "vct": "mandate.checkout.1", + "checkout_jwt": "eyJhbGciOiJFUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6Im1lcmNoYW50LWtleS0xIn0.eyJpc3MiOiJodHRwczovL3Rlbm5pcy13YXJlaG91c2UuY29tIiwic3ViIjoiY2FydF9jaGVja291dCIsImlhdCI6MTc1MDAwMDA1MCwiZXhwIjoxNzUwMDAzNjUwLCJjYXJ0Ijp7Iml0ZW1zIjpbeyJza3UiOiJCQUI4NjM0NSIsIm5hbWUiOiJCYWJvbGF0IFB1cmUgQWVybyBUZW5uaXMgUmFja2V0Iiwic2l6ZSI6Mywic2l6ZV9sYWJlbCI6IjQgMy84IiwiY29sb3IiOiJ3aGl0ZSIsInF1YW50aXR5IjoxLCJ1bml0UHJpY2UiOjI3OS45OX1dLCJzdWJUb3RhbCI6eyJhbW91bnQiOjI3OS45OSwiY3VycmVuY3lDb2RlIjoiVVNEIn19fQ.p20eGtZjSuB4kMcaEnabna0kDSrdrpOxAwgXCHBLcLBRxqKPAIMhx--U3H3mG7JUFAQ3ARJYwjvrDMoGe1mFng", + "checkout_hash": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + }, + "payment_mandate": { + "vct": "mandate.payment.1", + "transaction_id": "WRONGHASH" + }, + "expected_valid": false + }, + { + "name": "l3_xref_ok", + "kind": "l3_xref", + "l3_payment_claims": { + "delegate_payload": [ + { + "vct": "mandate.payment.1", + "transaction_id": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + } + ] + }, + "l3_checkout_claims": { + "delegate_payload": [ + { + "vct": "mandate.checkout.1", + "checkout_hash": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + } + ] + }, + "expected_valid": true + }, + { + "name": "l3_xref_mismatch", + "kind": "l3_xref", + "l3_payment_claims": { + "delegate_payload": [ + { + "vct": "mandate.payment.1", + "transaction_id": "86jeB8fqZa6Z1HNxmxrvjaFVciurT4VN2udrG5sw5zI" + } + ] + }, + "l3_checkout_claims": { + "delegate_payload": [ + { + "vct": "mandate.checkout.1", + "checkout_hash": "DIFFERENT" + } + ] + }, + "expected_valid": false + }, + { + "name": "l2_reference_ok", + "kind": "l2_ref", + "checkout_disclosure": "WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d", + "payment_mandate": { + "vct": "mandate.payment.open.1", + "constraints": [ + { + "type": "mandate.payment.reference", + "conditional_transaction_id": "rhURWZMY8cMCjgWmHEFqtgdg1cqnaWoqCVxqEnkzcR4" + } + ] + }, + "expected_valid": true + }, + { + "name": "l2_reference_mismatch", + "kind": "l2_ref", + "checkout_disclosure": "WyJsbkVlNEZMMzJsdkpESFItXzBhSnFnIix7InZjdCI6Im1hbmRhdGUuY2hlY2tvdXQub3Blbi4xIiwiY25mIjp7Imp3ayI6eyJrdHkiOiJFQyIsImNydiI6IlAtMjU2IiwieCI6IjBhclp3UGIwQkd3X2xnYVUwUXJ5bE44dHY1NmZ1S1BKeDZ0WkRTQWxTOTAiLCJ5IjoibTlzbTV3RmRDZndDeTZVY2Y2MjFuX2hkUnRfZmQ5WnNzUC1UVnRQOFkyayIsImtpZCI6ImFnZW50LWtleS0xIn19LCJjb25zdHJhaW50cyI6W3sidHlwZSI6Im1hbmRhdGUuY2hlY2tvdXQuYWxsb3dlZF9tZXJjaGFudHMiLCJhbGxvd2VkIjpbeyIuLi4iOiJ2TVMwc0NJX3J4T2tteTBaM0Vad19EV1l6cXVTdnNnNUZwQm5iYkM2b1NRIn0seyIuLi4iOiJtSzcwWDRmUmkxaEFzanhPSUZHVU4tN21KQmFoZlFVTDdDTl9oemVzN05FIn1dfSx7InR5cGUiOiJtYW5kYXRlLmNoZWNrb3V0LmxpbmVfaXRlbXMiLCJpdGVtcyI6W3siaWQiOiJsaW5lLTEiLCJhY2NlcHRhYmxlX2l0ZW1zIjpbeyIuLi4iOiJndXpqNzdwZlJlYzJXTUZIWTlrNUgzMWRaSlBJcDlkckR4TFVsamRlRF8wIn0seyIuLi4iOiJMVkdaZlZlZlFuOTdNTkgtOGxIYVRYQmNueXM3MTUyd0RSc3RBZ3Vja0gwIn1dLCJxdWFudGl0eSI6MX1dLCJtYXRjaF9tb2RlIjoibWluaW11bSJ9XX1d", + "payment_mandate": { + "vct": "mandate.payment.open.1", + "constraints": [ + { + "type": "mandate.payment.reference", + "conditional_transaction_id": "WRONGREF" + } + ] + }, + "expected_valid": false + } + ] + } +} diff --git a/typescript/.gitignore b/typescript/.gitignore new file mode 100644 index 0000000..a79a473 --- /dev/null +++ b/typescript/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.tsbuildinfo +*.tgz diff --git a/typescript/LICENSE b/typescript/LICENSE new file mode 100644 index 0000000..f639871 --- /dev/null +++ b/typescript/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright 2026 Verifiable Intent Contributors + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/typescript/README.md b/typescript/README.md new file mode 100644 index 0000000..d8d18de --- /dev/null +++ b/typescript/README.md @@ -0,0 +1,224 @@ +# Verifiable Intent — TypeScript + +TypeScript implementation of the [Verifiable Intent](../README.md) credential format: +a layered SD-JWT delegation chain (Issuer → User → Agent) that produces cryptographic +proof an AI agent's commercial actions stayed within the scope a human explicitly +delegated. + +This is the **TypeScript** package of the Verifiable Intent monorepo, a port of the +[`../python`](../python) reference implementation. It has **zero runtime +dependencies** and uses only Web-standard APIs — WebCrypto (`crypto.subtle`) for +ES256 and SHA-256, `crypto.getRandomValues` for salts, `TextEncoder`/`TextDecoder` +and a hand-rolled base64url codec for bytes — so it runs on **Node.js >= 20 and +in modern browsers** unchanged. The Verifiable-Intent-specific layering, +`delegate_payload` mechanism, and split-L3 logic stay byte-compatible with the +Python implementation. + +> **Status:** `crypto`, `models`, `issuance`, and `verification` are implemented +> and validated byte-for-byte (issuance) and verdict-for-verdict (verification) +> against Python-generated conformance vectors +> ([`../test-vectors/vectors.json`](../test-vectors)). The vitest suite (183 +> tests) replays every shared golden vector — asserting Python's exact error +> strings — plus TS-specific parity, fail-closed hardening, an isomorphic +> (Buffer-free) runtime test, and end-to-end issue→verify round-trip tests. + +## Install & test + +```bash +npm install +npm test # vitest, validates against ../test-vectors +npm run build # tsdown -> dist/ (dual ESM + CJS + .d.ts) +npm run typecheck # tsc --noEmit +npm run lint # eslint +npm run test:isomorphic # builds, then runs dist/ with Node's Buffer global deleted +``` + +## Quick start (immediate mode) + +Immediate mode: the user authorizes one specific checkout — no agent delegation, +no L3. Every symbol below is exported from the package root. + +```ts +import { + CheckoutMandate, + createLayer1, + createLayer2Immediate, + generateEs256Key, + hashAscii, + IssuerCredential, + MandateMode, + PaymentMandate, + UserMandate, + verifyChain, +} from '@verifiable-intent/core'; + +const now = Math.floor(Date.now() / 1000); +const issuer = await generateEs256Key(); // the card network / issuer +const user = await generateEs256Key(); // the user's wallet key + +// L1: issuer credential binding the user's public key (cnf.jwk). +const l1 = await createLayer1( + new IssuerCredential({ + iss: 'https://www.mastercard.com', + sub: 'user-bob-001', + iat: now, + exp: now + 86400, + aud: 'https://wallet.example.com', + cnfJwk: user.publicKey, + }), + issuer.privateKey, +); + +// L2: the user's mandate for one specific, finalized checkout. +const nonce = crypto.randomUUID(); // Web-standard global (Node >= 20 / browsers) +const mandate = new UserMandate({ + nonce, + aud: 'https://agent.example.com', + iat: now, + iss: 'https://wallet.example.com', + exp: now + 900, + mode: MandateMode.IMMEDIATE, + // Hashing is async (WebCrypto): await hashAscii / hashDisclosure / hashBytes. + sdHash: await hashAscii(l1.serialize()), + promptSummary: 'Purchase Babolat Pure Aero racket', + // `checkoutJwt` is a merchant-signed checkout JWT; checkout_hash and + // transaction_id are auto-computed from it by createLayer2Immediate. + checkoutMandate: new CheckoutMandate({ vct: 'mandate.checkout.1', checkoutJwt }), + paymentMandate: new PaymentMandate({ + vct: 'mandate.payment.1', + paymentInstrument: { type: 'mastercard.srcDigitalCard', id: 'f199c3dd-7106-478b-9b5f-7af9ca725170' }, + payee: { id: 'merchant-uuid-1', name: 'Tennis Warehouse', website: 'https://tennis-warehouse.com' }, + currency: 'USD', + amount: 27999, // minor units (cents) + }), +}); +const { sdJwt: l2 } = await createLayer2Immediate(mandate, user.privateKey); + +// Verify the chain. Fails closed: `valid` is false with populated `errors` +// on any problem; verification never throws on malformed credentials. +const result = await verifyChain(l1, l2, { + issuerPublicJwk: issuer.publicKey, + l1Serialized: l1.serialize(), + expectedL2Aud: 'https://agent.example.com', + expectedL2Nonce: nonce, +}); +if (!result.valid) { + console.error(result.errors); +} +``` + +For autonomous mode (open mandates + agent delegation + split L3a/L3b), see +`test/roundtrip.test.ts` for a full end-to-end chain. + +## What `verifyChain` checks + +Derived from the implementation (`src/verification/chain.ts`); every failure is +fail-closed (`valid: false` + a populated `errors` list): + +- **Signatures** — ES256 at every layer, each verified against the *previous* + layer's `cnf` key: L1 against `issuerPublicJwk`, L2 against the L1 `cnf.jwk` + (user key), L3 against the agent key from the L2 mandates' `cnf.jwk` — never a + key named in the L3 header. +- **Headers** — `alg` pinned to ES256 and `typ` checked per layer and mode. +- **`_sd_alg`** — must be `sha-256` when present, at all layers. +- **Duplicate `_sd` digests** — rejected per RFC 9901 §7.1 at all layers. +- **Time claims** — `exp` / `iat` validated against a configurable + `clockSkewSeconds` (default 300s); malformed time claims fail closed. +- **L1↔L2 binding** — L2 `sd_hash` must match the presented L1 serialized form; + L1 `vct` is checked against the expected value. +- **L2 `aud` / `nonce`** — matched against caller-provided expected values. +- **Mandate pairing** — L2 `delegate_payload` disclosures are grouped into + checkout/payment pairs with orphan, duplicate, and smuggling (duplicate + disclosure reference) detection. +- **`card_id` cross-check** — L1 `card_id` must match each payment mandate's + `payment_instrument.id`. +- **Per-mode mandate rules** — immediate mode requires final values and no + `cnf`; autonomous (open) mandates must carry their pairing constraints. +- **Agent-key consistency** — the delegation `cnf.jwk` (and `kid`) must be + identical across all mandate pairs. +- **L3 binding** — L3 `sd_hash` must match the presented L2 serialized form, and + the L2 presentation must include the L3's own mandate-pair disclosure + (L3-to-mandate-pair identity binding). +- **L3 restrictions** — no `cnf` claim, lifetime ≤ 1 hour, header `kid` required + and matched when the L2 `cnf.jwk` names one. +- **L3a↔L3b cross-reference** — the payment and checkout fulfillments must + reference each other's transaction. +- **L3 structure** — required mandate fields (vct, transaction ids, payee, + amount, payment instrument) and L3↔L2 payment-instrument consistency. + +Per-transaction **constraint evaluation** (e.g. is this fulfillment within the +mandate's allowlists and amount range?) is a separate exported function, +`checkConstraints`, run by the party evaluating a specific transaction — see the +next section. + +## Constraints + +Autonomous-mode mandates carry constraints. The stateless ones are enforced by +the verifier (`checkConstraints`); the stateful ones are assigned by the spec to +the payment network and are only *surfaced* by `verifyChain` (see below). + +| Constraint | Key fields | Enforced by | +| --- | --- | --- | +| `mandate.checkout.allowed_merchants` | `allowed` | verifier (`checkConstraints`) | +| `mandate.checkout.line_items` | `items`, `match_mode` | verifier (`checkConstraints`) | +| `mandate.payment.allowed_payees` | `allowed` | verifier (`checkConstraints`) | +| `mandate.payment.amount_range` | `currency`, `min`, `max` | verifier (`checkConstraints`) | +| `mandate.payment.budget` | `currency`, `max`, `min` | **payment network** (stateful) | +| `mandate.payment.recurrence` | `frequency`, `start_date`, `end_date`, `number` | **payment network** (stateful) | +| `mandate.payment.agent_recurrence` | `frequency`, `start_date`, `end_date`, `max_occurrences` | **payment network** (stateful) | +| `mandate.payment.reference` | `conditional_transaction_id` | auto-injected by issuance; binding checked by the verifier | + +## Network-enforced constraints + +Budget and recurrence constraints are *stateful* — enforcing them requires +knowing how much has already been spent or how many installments have run — so +the spec assigns them to the payment network, and this stateless verifier parses +but never evaluates them. That means `valid: true` tells you every stateless +check passed; it does **not** mean a budget or recurrence limit is satisfied. + +`verifyChain` surfaces these constraints on the result so the caller (typically +the payment network) knows what it still must enforce: + +```ts +const result = await verifyChain(l1, l2, opts); +for (const ne of result.networkEnforced) { + // ne.pairIndex — which mandate pair it came from + // ne.type — e.g. 'mandate.payment.budget' + // ne.constraint — the raw constraint object from the payment mandate + enforceAtTheNetwork(ne); // your stateful enforcement here +} +``` + +`networkEnforced` is always present — an empty array when no payment mandate +carries budget/recurrence constraints. + +## VerifyChainOptions highlights + +- `issuerPublicJwk` — the issuer's public key; required unless + `skipIssuerVerification` is explicitly set (test-only bypass). +- `currentTime` — Unix seconds; inject to verify fixed-timestamp credentials + deterministically (defaults to the wall clock). +- `clockSkewSeconds` — tolerance for `exp`/`iat` checks (default 300). +- `expectedL2Aud` / `expectedL2Nonce` — bind the L2 to the intended audience and + the nonce you issued for this flow. +- `expectedL3{Payment,Checkout}{Aud,Nonce}` — the same for each L3. +- `splitL3s` — per-pair L3a/L3b credentials plus the role-specific L2 + presentations (`l2PaymentSerialized` / `l2CheckoutSerialized`) they bind to. + +## Layout + +``` +src/ + crypto/ ES256 signing, base64url, disclosures, SD-JWT, KB-SD-JWT + models/ issuer credential, user mandate, agent mandate, constraints + issuance/ createLayer1 / layer2 (immediate + autonomous) / layer3 (split) + verification/ verifyChain (L1→L2→split-L3), integrity bindings, constraint checker +``` + +The build (`tsdown`) emits dual **ESM + CJS** with bundled type declarations and +no externals — the package has zero runtime dependencies. `verifyChain` accepts +a `currentTime` option so fixed-timestamp credentials can be verified +deterministically. + +The conformance vectors are regenerated from the Python reference with +`python/scripts/generate_vectors.py`. License: Apache-2.0. diff --git a/typescript/eslint.config.js b/typescript/eslint.config.js new file mode 100644 index 0000000..678f62d --- /dev/null +++ b/typescript/eslint.config.js @@ -0,0 +1,21 @@ +import js from '@eslint/js'; +import tseslint from 'typescript-eslint'; + +export default tseslint.config( + { ignores: ['dist/**', 'node_modules/**'] }, + js.configs.recommended, + ...tseslint.configs.recommended, + { + rules: { + '@typescript-eslint/no-explicit-any': 'error', + '@typescript-eslint/no-unused-vars': ['error', { argsIgnorePattern: '^_', varsIgnorePattern: '^_' }], + }, + }, + { + // Test fixtures load the untyped conformance-vector JSON blob. + files: ['test/**/*.ts'], + rules: { + '@typescript-eslint/no-explicit-any': 'off', + }, + }, +); diff --git a/typescript/package-lock.json b/typescript/package-lock.json new file mode 100644 index 0000000..44f0a30 --- /dev/null +++ b/typescript/package-lock.json @@ -0,0 +1,3557 @@ +{ + "name": "@verifiable-intent/core", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@verifiable-intent/core", + "version": "0.1.0", + "license": "Apache-2.0", + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/node": "^22.7.0", + "eslint": "^9.13.0", + "publint": "^0.3.21", + "tsdown": "^0.22.1", + "typescript": "^5.6.0", + "typescript-eslint": "^8.10.0", + "vitest": "^4.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@babel/generator": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-8.0.0-rc.6.tgz", + "integrity": "sha512-6mIzgVK8DgEzvIapoQwhXTMnnkuE4STQmVv9H03i/tZ2ml8oev3TRvZJgTenK2Bsq0YWNtzOrFdTyNzCMFtjJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.0-rc.6", + "@babel/types": "^8.0.0-rc.6", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "@types/jsesc": "^2.5.0", + "jsesc": "^3.0.2" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-8.0.0-rc.6.tgz", + "integrity": "sha512-BCkFy+zN6kXQed3YOT7aJl93NfDSzQc3pBfsvTVPs9gU9X3V0aefEF5kwBT0E+mDWH9QgKaZstYUQN9VdQZT4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-8.0.0-rc.6.tgz", + "integrity": "sha512-nVJ+1JcCgntv8d78rRo++o2wuODT0Irknx2BF8Np4Ft2CRgjLqIs4qzSZ8b66yGbBdMWGmZBO9WEZv1hhNiSpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/parser": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-8.0.0-rc.6.tgz", + "integrity": "sha512-rOS8IpdO7mQELkTPlCsTgPejO0bFuZdEDCGQJouYbYf9e1FLTym7Fei2pEjq8q7MWbX0ravcd7QQYKs1TxOuog==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^8.0.0-rc.6" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@babel/types": { + "version": "8.0.0-rc.6", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-8.0.0-rc.6.tgz", + "integrity": "sha512-p7/ABylAYlexb31wtRdIfH9L9A0Z2T/9H6zAqzqndkY2PLkvNNc580wGhp/gGKN4Sp9sQvSkhc6Oga8/O+wTyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^8.0.0-rc.6", + "@babel/helper-validator-identifier": "^8.0.0-rc.6" + }, + "engines": { + "node": "^22.18.0 || >=24.11.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.10.0.tgz", + "integrity": "sha512-yq6OkJ4p82CAfPl0u9mQebQHKPJkY7WrIuk205cTYnYe+k2Z8YBh11FrbRG/H6ihirqcacOgl2BIO8oyMQLeXw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.10.0.tgz", + "integrity": "sha512-ewvYlk86xUoGI0zQRNq/mC+16R1QeDlKQy21Ki3oSYXNgLb45GV1P6A0M+/s6nyCuNDqe5VpaY84BzXGwVbwFA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@eslint-community/eslint-utils": { + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eslint-visitor-keys": "^3.4.3" + }, + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + }, + "peerDependencies": { + "eslint": "^6.0.0 || ^7.0.0 || >=8.0.0" + } + }, + "node_modules/@eslint-community/eslint-utils/node_modules/eslint-visitor-keys": { + "version": "3.4.3", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-3.4.3.tgz", + "integrity": "sha512-wpc+LXeiyiisxPlEkUzU6svyS1frIO3Mgxj1fdy7Pm8Ygzguax2N3Fa/D/ag1WqbOprdI+uY6wMUl8/a2G+iag==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^12.22.0 || ^14.17.0 || >=16.0.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint-community/regexpp": { + "version": "4.12.2", + "resolved": "https://registry.npmjs.org/@eslint-community/regexpp/-/regexpp-4.12.2.tgz", + "integrity": "sha512-EriSTlt5OC9/7SXkRSCAhfSxxoSUgBm33OH+IkwbdpgoqsSsUg7y3uh+IICI/Qg4BBWr3U2i39RpmycbxMq4ew==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.0.0 || ^14.0.0 || >=16.0.0" + } + }, + "node_modules/@eslint/config-array": { + "version": "0.21.2", + "resolved": "https://registry.npmjs.org/@eslint/config-array/-/config-array-0.21.2.tgz", + "integrity": "sha512-nJl2KGTlrf9GjLimgIru+V/mzgSK0ABCDQRvxw5BjURL7WfH5uoWmizbH7QB6MmnMBd8cIC9uceWnezL1VZWWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/object-schema": "^2.1.7", + "debug": "^4.3.1", + "minimatch": "^3.1.5" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/config-helpers": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.4.2.tgz", + "integrity": "sha512-gBrxN88gOIf3R7ja5K9slwNayVcZgK6SOUORm2uBzTeIEfeVaIhOpCtTox3P6R7o2jLFwLFTLnC7kU/RGcYEgw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/core": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.17.0.tgz", + "integrity": "sha512-yL/sLrpmtDaFEiUj1osRP4TI2MDz1AddJL+jZ7KSqvBuliN4xqYY54IfdN8qD8Toa6g1iloph1fxQNkjOxrrpQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@types/json-schema": "^7.0.15" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/eslintrc": { + "version": "3.3.5", + "resolved": "https://registry.npmjs.org/@eslint/eslintrc/-/eslintrc-3.3.5.tgz", + "integrity": "sha512-4IlJx0X0qftVsN5E+/vGujTRIFtwuLbNsVUe7TO6zYPDR1O6nFwvwhIKEKSrl6dZchmYBITazxKoUYOjdtjlRg==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^6.14.0", + "debug": "^4.3.2", + "espree": "^10.0.1", + "globals": "^14.0.0", + "ignore": "^5.2.0", + "import-fresh": "^3.2.1", + "js-yaml": "^4.1.1", + "minimatch": "^3.1.5", + "strip-json-comments": "^3.1.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@eslint/js": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.39.4.tgz", + "integrity": "sha512-nE7DEIchvtiFTwBw4Lfbu59PG+kCofhjsKaCWzxTpt4lfRjRMqG6uMBzKXuEcyXhOHoUp9riAm7/aWYGhXZ9cw==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + } + }, + "node_modules/@eslint/object-schema": { + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/@eslint/object-schema/-/object-schema-2.1.7.tgz", + "integrity": "sha512-VtAOaymWVfZcmZbp6E2mympDIHvyjXs/12LqWYjVw6qjrfF+VK+fyG33kChz3nnK+SU5/NeHOqrTEHS8sXO3OA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@eslint/plugin-kit": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.4.1.tgz", + "integrity": "sha512-43/qtrDUokr7LJqoF2c3+RInu/t4zfrpYdoSDfYyhg52rwLV6TnOvdG4fXm7IkSB3wErkcmJS9iEhjVtOSEjjA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@eslint/core": "^0.17.0", + "levn": "^0.4.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } + }, + "node_modules/@humanfs/core": { + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/node": { + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", + "@humanwhocodes/retry": "^0.4.0" + }, + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, + "node_modules/@humanwhocodes/module-importer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", + "integrity": "sha512-bxveV4V8v5Yb4ncFTT3rPSgZBOpCkjfK0y4oVVVJwIuDVBRMDXrPyXRL988i5ap9m9bnyEEjWfm5WkBmtffLfA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.22" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@humanwhocodes/retry": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@humanwhocodes/retry/-/retry-0.4.3.tgz", + "integrity": "sha512-bV0Tgo9K4hfPCek+aMAn81RppFKv2ySDQeMoSZuvTASywNTnVJCArCZE2FWqpvIatKu7VMRLWlR1EazvVhDyhQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/nzakas" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.4.tgz", + "integrity": "sha512-3NQNNgA1YSlJb/kMH1ildASP9HW7/7kYnRI2szWJaofaS1hWmbGI4H+d3+22aGzXXN9IJ+n+GiFVcGipJP18ow==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.132.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.132.0.tgz", + "integrity": "sha512-FESMOxil5Se014ui/Eq8fT5uHJo6nIRwH0PfJrZJXs6Gek3ZVFOrpUv3YIZT20m+extU98Hg1Ym72U58rlsxUQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@publint/pack": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/@publint/pack/-/pack-0.1.4.tgz", + "integrity": "sha512-HDVTWq3H0uTXiU0eeSQntcVUTPP3GamzeXI41+x7uU9J65JgWQh3qWZHblR1i0npXfFtF+mxBiU2nJH8znxWnQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, + "node_modules/@quansync/fs": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@quansync/fs/-/fs-1.0.0.tgz", + "integrity": "sha512-4TJ3DFtlf1L5LDMaM6CanJ/0lckGNtJcMjQ1NAV6zDmA0tEHKZtxNKin8EgPaVX1YzljbxckyT2tJrpQKAtngQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.2.tgz", + "integrity": "sha512-ZS4D1JPGn/MYQN/SYDWftIE/nVsM8j/AFOYEzAoOE2O3NktQOZru+/vYXGbR/qtdLdIfGCP0lcoJiYVzsEz+iQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.2.tgz", + "integrity": "sha512-vdFA9+C/rekyGce7WqHs/xoT0ioZEWaOFyZLIV1mEeNFaFDUQrPIo8Vs2GvJ6eetb3rzDUtUBgzto3ExpXJB3w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.2.tgz", + "integrity": "sha512-BewSOwTHazv77DTYiAZXSqqKZ4KP/KonFisDMVU7PImxoWfB2aepnPhd2E4SWz3zDzYgDNbs6jBmTdgNnF02GA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.2.tgz", + "integrity": "sha512-m41o7M0YWtUdqk61Tb+jnKb2rN++iRdIASlExkUoKfIAH30DOHCB8fVLzSUpbWHHU8esmEioY62PxzexE8MBuA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.2.tgz", + "integrity": "sha512-jcojB9H7W/jS29pMKWAK1N+fU99vXodHDTatS3b3y/XSOCiHo0kkA74pL3jJmkoQtYpOCxDvaKs1fo2Ij/1X5w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.2.tgz", + "integrity": "sha512-1jn6qDU5iiOgFgygDzKUuKP0maTi0/f1+sBLgvij/76C77Nm3ts6ufz9Bjg5q5dduxiUIxtq86JIoBvo1xQ4Ig==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.2.tgz", + "integrity": "sha512-QVLO/czFMdoMFSqlX3bcswcJNm/23r+qoa/jgtmFc/qEp6/jXmIkDjF/XIo8dPfGaiwy1xfQn8o77L79GeXFgw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.2.tgz", + "integrity": "sha512-hgO5Abm0w5UL6FEa2iFnZqo2KlK7TQ5QhV5x09hujBf7t5KzHQ1VmfPuTpqRy/rNlSxua3eWH374xxiVrP+lcA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.2.tgz", + "integrity": "sha512-fy8rXxuYEu602abC8MUNaPjYLIFzReOaEIEMKMUa0rFEUxNpVXhs15KSSQ4qlqSaM7B6rcj9rDZgADh/IGDzLQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.2.tgz", + "integrity": "sha512-0+bOkiQ779+r1WpoHOWHqncvyySci0vKph+myNDYb+im6meJAzHQXay6oEgnkHuUGouM1LKTZwqKpBow6Kj7CQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.2.tgz", + "integrity": "sha512-mjSkrzZK5Qsl0a9d1JgILOiuZOSDTVdKENcSXBoqbzSrspLR/4/IRVDo5wd2GgZjNss/viBFJdeq+j7qH2nypw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.2.tgz", + "integrity": "sha512-1v5vHasdfQAZoEHakBV72LIFAC9JjnymsiKxp+GEr/ma3+NJCPSaYK+qavInOovJkgwFrs7GccX2d6IgDA3Z5w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.2.tgz", + "integrity": "sha512-mb1VobWn6NheziTk5/WEaR6AKVbrwT5sOi6C7zk3gy/pD1qtJfU1j4PgTo2NJnOtbL9Dl3Aeei8w9jJ7qC2jZQ==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.2.tgz", + "integrity": "sha512-SqKonF56vA/L2yHwHYcEp2P34URpOZ7d1fS635cTkpDnUtEGdUbhI6NzsPdqeSWvAAeGDrxjWjNmibDIdFf9/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.2.tgz", + "integrity": "sha512-v7qRI7gXLRINcOGXt+7YmAZ6iFuyZVMIoXAxhd8oP+DR9dLfL9GfNIx7PLMxmhZdvq8waUJBQiWN9EKNy+TRBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.2", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.2.tgz", + "integrity": "sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/jsesc": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/@types/jsesc/-/jsesc-2.5.1.tgz", + "integrity": "sha512-9VN+6yxLOPLOav+7PwjZbxiID2bVaeq0ED4qSQmdQTdjnXJSaCVKTR58t15oqH1H5t8Ng2ZX1SabJVoN9Q34bw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.60.0.tgz", + "integrity": "sha512-QYb/sa74/s7OKMbACMjrYnGspj9Hs5YI5aaffSL65UfeBUzVzBJfVo3oWSpbzPurvm7yaCCo2Lk7lVj610HqKw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/type-utils": "8.60.0", + "@typescript-eslint/utils": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "ignore": "^7.0.5", + "natural-compare": "^1.4.0", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "@typescript-eslint/parser": "^8.60.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/eslint-plugin/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@typescript-eslint/parser": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.60.0.tgz", + "integrity": "sha512-fcqpj/MyK4sxDPcbe7STNPbpQL4RLZOPWuaTmwZYuc+hJKzRf58yRxfhqGpc6PIq9ZyfSBpfHgmUHmHs0KwHwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/project-service": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.60.0.tgz", + "integrity": "sha512-aZu74NNKJeUWqCjDddzdiKaS82dgYgV/vmf+Ui3ZdZejmgfXR/q+pRumgobnQ2cCJTgGTWp4ypiwsuofFubavg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/tsconfig-utils": "^8.60.0", + "@typescript-eslint/types": "^8.60.0", + "debug": "^4.4.3" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/scope-manager": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.60.0.tgz", + "integrity": "sha512-pFzqhllJMs+jghLQWzV00ds39xLzuyqPSev5pd8f4Ir0rtKR3ZLUB4/4dhjOFighWb9larvtfJvqL+4yKDI3Xw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/tsconfig-utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.60.0.tgz", + "integrity": "sha512-BZPR3RGYlAXnly6ymAxfkVn5rCbZzQNou0rxv3GfWZ8cTQp+hhVd73khbGLAd8k1TlAPLISH337M+tAgAnaJDQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/type-utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.60.0.tgz", + "integrity": "sha512-SX46wEUtitCpq7AN38HkUU/+zvUpdKf7ephtWAFgckH8O7PQIyL5gvrhQgBLuEYgLfuKWOVvWVskMbuFHAz5xg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/utils": "8.60.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/types": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.60.0.tgz", + "integrity": "sha512-AsE7x2XaAK+CVbeih0Fvbn+r1qHxtpLDJ3XUuFcIinT318T90yHMJC+Zgv+jUuDjQQd06HKwxnDu6sz1IcTilA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/typescript-estree": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.60.0.tgz", + "integrity": "sha512-3AcZNBGMClm6CXDyo8kYvVGT/sx29sS0oBsIb9oZI2gunA4Vm2M3YHzRLPvsUBBsl+yB5FPtltq7gGH0iTlp9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/project-service": "8.60.0", + "@typescript-eslint/tsconfig-utils": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/visitor-keys": "8.60.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", + "tinyglobby": "^0.2.15", + "ts-api-utils": "^2.5.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@typescript-eslint/typescript-estree/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@typescript-eslint/utils": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.60.0.tgz", + "integrity": "sha512-HtXuPfrHTyBDkameWpl+vJb1Uevu2tznAyahM1Oc4AENidCLTPiZDWIo4GfcxNdC/RcfGcadzzkqbRG87dUrQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.60.0", + "@typescript-eslint/types": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/@typescript-eslint/visitor-keys": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.60.0.tgz", + "integrity": "sha512-9WI52t8ZGLVGrPMBet25yAftqY/n95+zmoUUtJBBQTKDSKUu7OsPTroT2op7U9JatkoRccL0YkWDNMFfC4Sjxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "8.60.0", + "eslint-visitor-keys": "^5.0.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^20.19.0 || ^22.13.0 || >=24" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.7.tgz", + "integrity": "sha512-1R+tw0ortHEbZDGMymm+pN7/AFQ/RkFFdtd7EN+VBpynKmLbP8A3rpEXdshBJ7+8hQ9zBJh/i1s0yKNtxAnU7w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.7.tgz", + "integrity": "sha512-vY7nuamKgfvpA1Koa3oYIw/k7D6kZnpGyNMZW8loow2bsBYla1TFdqTaXncWdRn4pgwNs+90RhnXhJScDwQeJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.7", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.7.tgz", + "integrity": "sha512-umgCarTOYQWIaDMvGDRZij+6b9oVeLIyJzfN+AS88e0ZOU3QTgNNSTtjQOpcvWr3np1N0j4WgZj+sb3oYBDscw==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.7.tgz", + "integrity": "sha512-BapjmAQ2aI78WdMEfeUWivnfVzB+VPGwWRQcJE0OUq7qEeEcBsCSf+0T5iREBNE5nBb4wA5Ya0W6IA+sghdEFw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.7", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.7.tgz", + "integrity": "sha512-ZacLzja+TmJeZ1h14xW2FB/WpeimUD3haBXQPyJqxvo8jQTmfeA8zv58mtjN2C7EHXZDYVcVYdYmAxjkWVvKCw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "@vitest/utils": "4.1.7", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.7.tgz", + "integrity": "sha512-kbkI5LMWakyuTIvs6fUJ5qdIVb1XVKsYJAT4OJ938cHMROYMSfmoQdZy0aaAnjbbc8F61vkoTqz/Az+/HiIu5Q==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.7.tgz", + "integrity": "sha512-T532WBu791cBxJlCl6SO+J14l81DQx6uQHm1bQbmCDY7nqlEIgkza/UFnSBNaUtSf41unldDFjdOBYEQC4b5Hw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.7", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-jsx": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", + "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" + } + }, + "node_modules/ajv": { + "version": "6.15.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.15.0.tgz", + "integrity": "sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.1", + "fast-json-stable-stringify": "^2.0.0", + "json-schema-traverse": "^0.4.1", + "uri-js": "^4.2.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/ansis": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/ansis/-/ansis-4.3.1.tgz", + "integrity": "sha512-BJ8/l4R5LRE7hW9WdSuGYrLSHi2ynxeFpDFbH0K/CgNeY/tyhk+vO6TYxXC5r5CpUhNVX310xzPsN/H9lCdfOA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=14" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true, + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/ast-kit": { + "version": "3.0.0-beta.1", + "resolved": "https://registry.npmjs.org/ast-kit/-/ast-kit-3.0.0-beta.1.tgz", + "integrity": "sha512-trmleAnZ2PxN/loHWVhhx1qeOHSRXq4TDsBBxq3GqeJitfk3+jTQ+v/C1km/KYq9M7wKqCewMh+/NAvVH7m+bw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^8.0.0-beta.4", + "estree-walker": "^3.0.3", + "pathe": "^2.0.3" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/birpc": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/birpc/-/birpc-4.0.0.tgz", + "integrity": "sha512-LShSxJP0KTmd101b6DRyGBj57LZxSDYWKitQNW/mi8GRMvZb078Uf9+pveax1DrVL89vm7mWe+TovdI/UDOuPw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.15", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.15.tgz", + "integrity": "sha512-EwOCDEex4quD37XhqM3omwtMoJjr//isUZz1JopUNWms+4Z2ViyM/k1YIRePpoVNnQhENnxtFjLaxNHrT7xIUg==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/cac": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/cac/-/cac-7.0.0.tgz", + "integrity": "sha512-tixWYgm5ZoOD+3g6UTea91eow5z6AAHaho3g0V9CNSNb45gM8SmflpAc+GRd1InC4AqN/07Unrgp56Y94N9hJQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.19.0" + } + }, + "node_modules/callsites": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", + "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/deep-is": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", + "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/dts-resolver": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/dts-resolver/-/dts-resolver-3.0.0.tgz", + "integrity": "sha512-1T1f+z+4tl9XD+m+0HBgWoL/nm0bOIffyWaUuUSBlFg/86IWvfx+wjNaO/ybU0AJzG9/Mi5hBUgGV6zCmWEN7Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "oxc-resolver": ">=11.0.0" + }, + "peerDependenciesMeta": { + "oxc-resolver": { + "optional": true + } + } + }, + "node_modules/empathic": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.1.tgz", + "integrity": "sha512-YGRs8knHhKHVShLkFET/rWAU8kmHbOV5LwN938RHI0pljAJ1Gf6SzXsSmRaEzcXTtOOmVqJ5+WtQPL5uigY50Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/es-module-lexer": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.1.0.tgz", + "integrity": "sha512-n27zTYMjYu1aj4MjCWzSP7G9r75utsaoc8m61weK+W8JMBGGQybd43GstCXZ3WNmSFtGT9wi59qQTW6mhTR5LQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/eslint": { + "version": "9.39.4", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-9.39.4.tgz", + "integrity": "sha512-XoMjdBOwe/esVgEvLmNsD3IRHkm7fbKIUGvrleloJXUZgDHig2IPWNniv+GwjyJXzuNqVjlr5+4yVUZjycJwfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@eslint-community/eslint-utils": "^4.8.0", + "@eslint-community/regexpp": "^4.12.1", + "@eslint/config-array": "^0.21.2", + "@eslint/config-helpers": "^0.4.2", + "@eslint/core": "^0.17.0", + "@eslint/eslintrc": "^3.3.5", + "@eslint/js": "9.39.4", + "@eslint/plugin-kit": "^0.4.1", + "@humanfs/node": "^0.16.6", + "@humanwhocodes/module-importer": "^1.0.1", + "@humanwhocodes/retry": "^0.4.2", + "@types/estree": "^1.0.6", + "ajv": "^6.14.0", + "chalk": "^4.0.0", + "cross-spawn": "^7.0.6", + "debug": "^4.3.2", + "escape-string-regexp": "^4.0.0", + "eslint-scope": "^8.4.0", + "eslint-visitor-keys": "^4.2.1", + "espree": "^10.4.0", + "esquery": "^1.5.0", + "esutils": "^2.0.2", + "fast-deep-equal": "^3.1.3", + "file-entry-cache": "^8.0.0", + "find-up": "^5.0.0", + "glob-parent": "^6.0.2", + "ignore": "^5.2.0", + "imurmurhash": "^0.1.4", + "is-glob": "^4.0.0", + "json-stable-stringify-without-jsonify": "^1.0.1", + "lodash.merge": "^4.6.2", + "minimatch": "^3.1.5", + "natural-compare": "^1.4.0", + "optionator": "^0.9.3" + }, + "bin": { + "eslint": "bin/eslint.js" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://eslint.org/donate" + }, + "peerDependencies": { + "jiti": "*" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + } + } + }, + "node_modules/eslint-scope": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-8.4.0.tgz", + "integrity": "sha512-sNXOfKCn74rt8RICKMvJS7XKV/Xk9kA7DyJr8mJik3S7Cwgy3qlkkmyS2uQB3jiJg6VNdZd/pDBJu0nvG2NlTg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^5.2.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/eslint-visitor-keys": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", + "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/espree": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/espree/-/espree-10.4.0.tgz", + "integrity": "sha512-j6PAQ2uUr79PZhBjP5C5fhl8e39FmRnOjsD5lGnWrFU8i2G776tBK7+nP8KuQUTTyAZUwfQqXAgrVH5MbH9CYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "acorn": "^8.15.0", + "acorn-jsx": "^5.3.2", + "eslint-visitor-keys": "^4.2.1" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "url": "https://opencollective.com/eslint" + } + }, + "node_modules/esquery": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", + "integrity": "sha512-Ap6G0WQwcU/LHsvLwON1fAQX9Zp0A2Y6Y/cJBl9r/JbW90Zyg4/zbG6zzKa2OTALELarYHmKu0GhpM5EO+7T0g==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "estraverse": "^5.1.0" + }, + "engines": { + "node": ">=0.10" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-json-stable-stringify": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", + "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-levenshtein": { + "version": "2.0.6", + "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", + "integrity": "sha512-DCXu6Ifhqcks7TZKY3Hxp3y6qphY5SJZmrWMDrKcERSOXWQdMhU9Ig/PYrzyw/ul9jOIyh0N4M0tbC5hodg8dw==", + "dev": true, + "license": "MIT" + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/file-entry-cache": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", + "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "flat-cache": "^4.0.0" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat-cache": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", + "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "dev": true, + "license": "MIT", + "dependencies": { + "flatted": "^3.2.9", + "keyv": "^4.5.4" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/flatted": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", + "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "dev": true, + "license": "ISC" + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.5", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.5.tgz", + "integrity": "sha512-/6gFNr0N04nob252sTQxyFLi3eKFRqIg1I87YcqAMT1i6SQrSF6KujUEQrtrjMV0H/eejTCltLdDSTEMzHbnsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/glob-parent": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", + "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", + "dev": true, + "license": "ISC", + "dependencies": { + "is-glob": "^4.0.3" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/globals": { + "version": "14.0.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", + "integrity": "sha512-oahGvuMGQlPw/ivIYBjVSrWAfWLBeku5tpPE2fOPLi+WHffIWbuh2tCjhyQhTBPMf5E9jDEH4FOmTYgYwbKwtQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hookable": { + "version": "6.1.1", + "resolved": "https://registry.npmjs.org/hookable/-/hookable-6.1.1.tgz", + "integrity": "sha512-U9LYDy1CwhMCnprUfeAZWZGByVbhd54hwepegYTK7Pi5NvqEj63ifz5z+xukznehT7i6NIZRu89Ay1AZmRsLEQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/import-fresh": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", + "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "parent-module": "^1.0.0", + "resolve-from": "^4.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/import-without-cache": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/import-without-cache/-/import-without-cache-0.4.0.tgz", + "integrity": "sha512-NkJQA7oZ4YHQhd2+H3BoRFKF3d/XNsiKpHZCQEMH9pDX27hQQLsTyOocyRgaIVtf8gHX3Nt3LPkR4e5EdtPAGQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^22.18.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, + "node_modules/imurmurhash": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", + "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.19" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/js-yaml": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.2.0.tgz", + "integrity": "sha512-ePWsvanv0DWuDRsW8dnt+R4jQ31SCRCQ7hhNcPXZPsoBZiemuZNYGf7adZdqX2D86j6rvKp3RpCxVTSb8WQlOw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-buffer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", + "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", + "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-stable-stringify-without-jsonify": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/json-stable-stringify-without-jsonify/-/json-stable-stringify-without-jsonify-1.0.1.tgz", + "integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==", + "dev": true, + "license": "MIT" + }, + "node_modules/keyv": { + "version": "4.5.4", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", + "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-buffer": "3.0.1" + } + }, + "node_modules/levn": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/levn/-/levn-0.4.1.tgz", + "integrity": "sha512-+bT2uH4E5LGE7h/n3evcS/sQlJXCpIp6ym8OWJ5eV6+67Dsql/LaaT7qJBAt2rzfoa/5QBGBhxDix1dMt2kQKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1", + "type-check": "~0.4.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash.merge": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", + "integrity": "sha512-0KpjqXRVvrYyCsX1swR/XTK0va6VQkQM6MNo7PqW77ByjAhoARA8EfrP1N4+KlKj8YS0ZUCtRT/YUuhyYDujIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/minimatch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/mri": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/mri/-/mri-1.2.0.tgz", + "integrity": "sha512-tzzskb3bG8LvYGFF/mDTpq3jpI6Q9wc3LEmBaghu+DdCssd1FakN7Bc0hVNmEyGq1bq3RgfkCb3cmQLpNPOroA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.12", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", + "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/natural-compare": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", + "integrity": "sha512-OWND8ei3VtNC9h7V60qff3SVobHr996CTwgxubgyQYEpg290h9J0buyECNNJexkFm5sOajh5G116RYA1c8ZMSw==", + "dev": true, + "license": "MIT" + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/optionator": { + "version": "0.9.4", + "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", + "integrity": "sha512-6IpQ7mKUxRcZNLIObR0hz7lxsapSSIYNZJwXPGeF0mTVqGKFIXj1DQcMoT22S3ROcLyY/rz0PWaWZ9ayWmad9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "deep-is": "^0.1.3", + "fast-levenshtein": "^2.0.6", + "levn": "^0.4.1", + "prelude-ls": "^1.2.1", + "type-check": "^0.4.0", + "word-wrap": "^1.2.5" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/package-manager-detector": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", + "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "dev": true, + "license": "MIT" + }, + "node_modules/parent-module": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", + "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", + "dev": true, + "license": "MIT", + "dependencies": { + "callsites": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/prelude-ls": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", + "integrity": "sha512-vkcDPrRZo1QZLbn5RLGPpg/WmIQ65qoWWhcGKf/b5eplkkarX0m9z8ppCat4mlOqUsWpyNuYgO3VRyrYHSzX5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/publint": { + "version": "0.3.21", + "resolved": "https://registry.npmjs.org/publint/-/publint-0.3.21.tgz", + "integrity": "sha512-OqejcnMV6E9zel2oCrUOJEiiFkGiAAni0A6ibfQNh1k9Gu5z4F+Yso8lllam7AzmV6Do0vp7u3UpZNRBwuXaHQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@publint/pack": "^0.1.4", + "package-manager-detector": "^1.6.0", + "picocolors": "^1.1.1", + "sade": "^1.8.1" + }, + "bin": { + "publint": "src/cli.js" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://bjornlu.com/sponsor" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/quansync": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/quansync/-/quansync-1.0.0.tgz", + "integrity": "sha512-5xZacEEufv3HSTPQuchrvV6soaiACMFnq1H8wkVioctoH3TRha9Sz66lOxRwPK/qZj7HPiSveih9yAyh98gvqA==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/antfu" + }, + { + "type": "individual", + "url": "https://github.com/sponsors/sxzz" + } + ], + "license": "MIT" + }, + "node_modules/resolve-from": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", + "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=4" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/rolldown": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.2.tgz", + "integrity": "sha512-oZx5zVDtVB44AW3eaifgDml1gWRDZGvjcfdxonE4swNPG98PrrXjaO/KrnUjzlMnztCCRVlUueA1kCXhARGk6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.132.0", + "@rolldown/pluginutils": "^1.0.0" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.2", + "@rolldown/binding-darwin-arm64": "1.0.2", + "@rolldown/binding-darwin-x64": "1.0.2", + "@rolldown/binding-freebsd-x64": "1.0.2", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.2", + "@rolldown/binding-linux-arm64-gnu": "1.0.2", + "@rolldown/binding-linux-arm64-musl": "1.0.2", + "@rolldown/binding-linux-ppc64-gnu": "1.0.2", + "@rolldown/binding-linux-s390x-gnu": "1.0.2", + "@rolldown/binding-linux-x64-gnu": "1.0.2", + "@rolldown/binding-linux-x64-musl": "1.0.2", + "@rolldown/binding-openharmony-arm64": "1.0.2", + "@rolldown/binding-wasm32-wasi": "1.0.2", + "@rolldown/binding-win32-arm64-msvc": "1.0.2", + "@rolldown/binding-win32-x64-msvc": "1.0.2" + } + }, + "node_modules/rolldown-plugin-dts": { + "version": "0.25.2", + "resolved": "https://registry.npmjs.org/rolldown-plugin-dts/-/rolldown-plugin-dts-0.25.2.tgz", + "integrity": "sha512-nMhN/R+vmR8GM45ZW1FWMSjRTSDDn/6w4GTf8RNrEFCBdl8B1kySWrU1ixPtbwzXoRlcO+R/S88VgXuJQwfdDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/generator": "8.0.0-rc.6", + "@babel/helper-validator-identifier": "8.0.0-rc.6", + "@babel/parser": "8.0.0-rc.6", + "ast-kit": "^3.0.0-beta.1", + "birpc": "^4.0.0", + "dts-resolver": "^3.0.0", + "get-tsconfig": "5.0.0-beta.5", + "obug": "^2.1.1" + }, + "engines": { + "node": "^22.18.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@ts-macro/tsc": "^0.3.6", + "@typescript/native-preview": ">=7.0.0-dev.20260325.1", + "rolldown": "^1.0.0", + "typescript": "^5.0.0 || ^6.0.0", + "vue-tsc": "~3.2.0" + }, + "peerDependenciesMeta": { + "@ts-macro/tsc": { + "optional": true + }, + "@typescript/native-preview": { + "optional": true + }, + "typescript": { + "optional": true + }, + "vue-tsc": { + "optional": true + } + } + }, + "node_modules/sade": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/sade/-/sade-1.8.1.tgz", + "integrity": "sha512-xal3CZX1Xlo/k4ApwCFrHVACi9fBqJ7V+mwhBsuf/1IOKbBy098Fex+Wa/5QMubw09pSZ/u8EY8PWgevJsXp1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mri": "^1.1.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/semver": { + "version": "7.8.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.1.tgz", + "integrity": "sha512-rkVq3IXh+4FDGch+KwzX3aV9W3kO54GyEgpvBzSyctDA6Xtd7RJQV1xmXbeQp5v7+VzLOfVqiutSE6GICgPFvg==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.1.0.tgz", + "integrity": "sha512-Rq7ybcX2RuC55r9oaPVEW7/xu3tj8u4GeBYHBWCychFtzMIr86A7e3PPEBPT37sHStKX3+TiX/Fr/ACmJLVlLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.2.4.tgz", + "integrity": "sha512-SHf/r48b7vOrjve9PxJo3MN5v5yuyjHvdUcrQffT3WXMUfnGmHDVbC4k3sHJaJTgZCwpUplIaAo5ANtMyp3YHg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-api-utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", + "integrity": "sha512-OJ/ibxhPlqrMM0UiNHJ/0CKQkoKF243/AEmplt3qpRgkW8VG7IfOS41h7V8TjITqdByHzrjcS/2si+y4lIh8NA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18.12" + }, + "peerDependencies": { + "typescript": ">=4.8.4" + } + }, + "node_modules/tsdown": { + "version": "0.22.1", + "resolved": "https://registry.npmjs.org/tsdown/-/tsdown-0.22.1.tgz", + "integrity": "sha512-Ldx1jLyDFEzsN/fMBi2TBVaZe4fuEJhIiHjQhX0pV7oa5uYz5Imdivs5mNzEXOrMEtFRR6C9BQ2YqLoroffB+Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansis": "^4.3.0", + "cac": "^7.0.0", + "defu": "^6.1.7", + "empathic": "^2.0.1", + "hookable": "^6.1.1", + "import-without-cache": "^0.4.0", + "obug": "^2.1.1", + "picomatch": "^4.0.4", + "rolldown": "^1.0.2", + "rolldown-plugin-dts": "^0.25.1", + "semver": "^7.8.0", + "tinyexec": "^1.1.2", + "tinyglobby": "^0.2.16", + "tree-kill": "^1.2.2", + "unconfig-core": "^7.5.0" + }, + "bin": { + "tsdown": "dist/run.mjs" + }, + "engines": { + "node": "^22.18.0 || >=24.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + }, + "peerDependencies": { + "@arethetypeswrong/core": "^0.18.1", + "@tsdown/css": "0.22.1", + "@tsdown/exe": "0.22.1", + "@vitejs/devtools": "*", + "publint": "^0.3.8", + "tsx": "*", + "typescript": "^5.0.0 || ^6.0.0", + "unplugin-unused": "^0.5.0", + "unrun": "*" + }, + "peerDependenciesMeta": { + "@arethetypeswrong/core": { + "optional": true + }, + "@tsdown/css": { + "optional": true + }, + "@tsdown/exe": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "publint": { + "optional": true + }, + "tsx": { + "optional": true + }, + "typescript": { + "optional": true + }, + "unplugin-unused": { + "optional": true + }, + "unrun": { + "optional": true + } + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/type-check": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", + "integrity": "sha512-XleUoc9uwGXqjWwXaUTZAmzMcFZ5858QA2vvx1Ur5xIcixXIP+8LnFDgRplU30us6teqdlskFfu+ae4K79Ooew==", + "dev": true, + "license": "MIT", + "dependencies": { + "prelude-ls": "^1.2.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/typescript-eslint": { + "version": "8.60.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.60.0.tgz", + "integrity": "sha512-9f65qWLZdAW9m1JaxBDUHcqRUfL8bkxxXL7XxEfI+F09q56PkBvIfCjLF3yInsDM/BBmwkqmCQdCZe/RYlIWEw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/eslint-plugin": "8.60.0", + "@typescript-eslint/parser": "8.60.0", + "@typescript-eslint/typescript-estree": "8.60.0", + "@typescript-eslint/utils": "8.60.0" + }, + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", + "typescript": ">=4.8.4 <6.1.0" + } + }, + "node_modules/unconfig-core": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/unconfig-core/-/unconfig-core-7.5.0.tgz", + "integrity": "sha512-Su3FauozOGP44ZmKdHy2oE6LPjk51M/TRRjHv2HNCWiDvfvCoxC2lno6jevMA91MYAdCdwP05QnWdWpSbncX/w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@quansync/fs": "^1.0.0", + "quansync": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/unrun": { + "version": "0.2.39", + "resolved": "https://registry.npmjs.org/unrun/-/unrun-0.2.39.tgz", + "integrity": "sha512-h9FxYVpztY/wwq+bauLOh6Y3CWu2IVeRLq5lxzneBiIU9Tn86OGp9xiQrGhnYspAmg5dzdY0Cc8+Y70kuTARCg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "rolldown": "1.0.0-rc.17" + }, + "bin": { + "unrun": "dist/cli.mjs" + }, + "engines": { + "node": ">=20.19.0" + }, + "funding": { + "url": "https://github.com/sponsors/Gugustinette" + }, + "peerDependencies": { + "synckit": "^0.11.11" + }, + "peerDependenciesMeta": { + "synckit": { + "optional": true + } + } + }, + "node_modules/unrun/node_modules/@oxc-project/types": { + "version": "0.127.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.127.0.tgz", + "integrity": "sha512-aIYXQBo4lCbO4z0R3FHeucQHpF46l2LbMdxRvqvuRuW2OxdnSkcng5B8+K12spgLDj93rtN3+J2Vac/TIO+ciQ==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-s70pVGhw4zqGeFnXWvAzJDlvxhlRollagdCCKRgOsgUOH3N1l0LIxf83AtGzmb5SiVM4Hjl5HyarMRfdfj3DaQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-4ksWc9n0mhlZpZ9PMZgTGjeOPRu8MB1Z3Tz0Mo02eWfWCHMW1zN82Qz/pL/rC+yQa+8ZnutMF0JjJe7PjwasYw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-SUSDOI6WwUVNcWxd02QEBjLdY1VPHvlEkw6T/8nYG322iYWCTxRb1vzk4E+mWWYehTp7ERibq54LSJGjmouOsw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.17.tgz", + "integrity": "sha512-hwnz3nw9dbJ05EDO/PvcjaaewqqDy7Y1rn1UO81l8iIK1GjenME75dl16ajbvSSMfv66WXSRCYKIqfgq2KCfxw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.17.tgz", + "integrity": "sha512-IS+W7epTcwANmFSQFrS1SivEXHtl1JtuQA9wlxrZTcNi6mx+FDOYrakGevvvTwgj2JvWiK8B29/qD9BELZPyXQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-e6usGaHKW5BMNZOymS1UcEYGowQMWcgZ71Z17Sl/h2+ZziNJ1a9n3Zvcz6LdRyIW5572wBCTH/Z+bKuZouGk9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-b/CgbwAJpmrRLp02RPfhbudf5tZnN9nsPWK82znefso832etkem8H7FSZwxrOI9djcdTP7U6YfNhbRnh7djErg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-4EII1iNGRUN5WwGbF/kOh/EIkoDN9HsupgLQoXfY+D1oyJm7/F4t5PYU5n8SWZgG0FEwakyM8pGgwcBYruGTlA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-AH8oq3XqQo4IibpVXvPeLDI5pzkpYn0WiZAfT05kFzoJ6tQNzwRdDYQ45M8I/gslbodRZwW8uxLhbSBbkv96rA==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.17.tgz", + "integrity": "sha512-cLnjV3xfo7KslbU41Z7z8BH/E1y5mzUYzAqih1d1MDaIGZRCMqTijqLv76/P7fyHuvUcfGsIpqCdddbxLLK9rA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.17.tgz", + "integrity": "sha512-0phclDw1spsL7dUB37sIARuis2tAgomCJXAHZlpt8PXZ4Ba0dRP1e+66lsRqrfhISeN9bEGNjQs+T/Fbd7oYGw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.17.tgz", + "integrity": "sha512-0ag/hEgXOwgw4t8QyQvUCxvEg+V0KBcA6YuOx9g0r02MprutRF5dyljgm3EmR02O292UX7UeS6HzWHAl6KgyhA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.17.tgz", + "integrity": "sha512-LEXei6vo0E5wTGwpkJ4KoT3OZJRnglwldt5ziLzOlc6qqb55z4tWNq2A+PFqCJuvWWdP53CVhG1Z9NtToDPJrA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/core": "1.10.0", + "@emnapi/runtime": "1.10.0", + "@napi-rs/wasm-runtime": "^1.1.4" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-gUmyzBl3SPMa6hrqFUth9sVfcLBlYsbMzBx5PlexMroZStgzGqlZ26pYG89rBb45Mnia+oil6YAIFeEWGWhoZA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.17.tgz", + "integrity": "sha512-3hkiolcUAvPB9FLb3UZdfjVVNWherN1f/skkGWJP/fgSQhYUZpSIRr0/I8ZK9TkF3F7kxvJAk0+IcKvPHk9qQg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/unrun/node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.17.tgz", + "integrity": "sha512-n8iosDOt6Ig1UhJ2AYqoIhHWh/isz0xpicHTzpKBeotdVsTEcxsSA/i3EVM7gQAj0rU27OLAxCjzlj15IWY7bg==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true + }, + "node_modules/unrun/node_modules/rolldown": { + "version": "1.0.0-rc.17", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.17.tgz", + "integrity": "sha512-ZrT53oAKrtA4+YtBWPQbtPOxIbVDbxT0orcYERKd63VJTF13zPcgXTvD4843L8pcsI7M6MErt8QtON6lrB9tyA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@oxc-project/types": "=0.127.0", + "@rolldown/pluginutils": "1.0.0-rc.17" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.17", + "@rolldown/binding-darwin-x64": "1.0.0-rc.17", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.17", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.17", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.17", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.17", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.17", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.17", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.17", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.17" + } + }, + "node_modules/uri-js": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", + "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "punycode": "^2.1.0" + } + }, + "node_modules/vite": { + "version": "8.0.14", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.14.tgz", + "integrity": "sha512-s4BJJ+5y1pYL6Otw51FHhVJQhPnuRinKig64g/1+EUNaJsd3gCKdD31IPFvswUgW9/60QT9oFHbZHbQK5imcxw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.15", + "rolldown": "1.0.2", + "tinyglobby": "^0.2.16" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.18", + "esbuild": "^0.27.0 || ^0.28.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.7.tgz", + "integrity": "sha512-flYyaFd2CgoCoU+0UKt3pxksgC+S02iTDN0n3LtqaMeXsI9SBcdNujc2k0DeFLzUn/0k538yNjOSdwgCqcrwJA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.7", + "@vitest/mocker": "4.1.7", + "@vitest/pretty-format": "4.1.7", + "@vitest/runner": "4.1.7", + "@vitest/snapshot": "4.1.7", + "@vitest/spy": "4.1.7", + "@vitest/utils": "4.1.7", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.7", + "@vitest/browser-preview": "4.1.7", + "@vitest/browser-webdriverio": "4.1.7", + "@vitest/coverage-istanbul": "4.1.7", + "@vitest/coverage-v8": "4.1.7", + "@vitest/ui": "4.1.7", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/coverage-istanbul": { + "optional": true + }, + "@vitest/coverage-v8": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/word-wrap": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.5.tgz", + "integrity": "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/typescript/package.json b/typescript/package.json new file mode 100644 index 0000000..c602c41 --- /dev/null +++ b/typescript/package.json @@ -0,0 +1,73 @@ +{ + "name": "@verifiable-intent/core", + "version": "0.1.0", + "description": "TypeScript implementation of the Verifiable Intent credential format \u2014 a layered SD-JWT delegation chain (Issuer \u2192 User \u2192 Agent) for cryptographic agent authorization in commerce.", + "license": "Apache-2.0", + "author": "Verifiable Intent Contributors", + "repository": { + "type": "git", + "url": "git+https://github.com/agent-intent/verifiable-intent.git", + "directory": "typescript" + }, + "homepage": "https://verifiableintent.dev", + "bugs": "https://github.com/agent-intent/verifiable-intent/issues", + "type": "module", + "engines": { + "node": ">=20" + }, + "main": "./dist/index.cjs", + "module": "./dist/index.mjs", + "types": "./dist/index.d.cts", + "sideEffects": false, + "exports": { + ".": { + "import": { + "types": "./dist/index.d.mts", + "default": "./dist/index.mjs" + }, + "require": { + "types": "./dist/index.d.cts", + "default": "./dist/index.cjs" + } + }, + "./package.json": "./package.json" + }, + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "publishConfig": { + "access": "public" + }, + "keywords": [ + "sd-jwt", + "agent", + "payments", + "authorization", + "commerce", + "credentials", + "delegation", + "verifiable-intent" + ], + "scripts": { + "build": "tsdown", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest", + "lint": "eslint src test", + "check:pkg": "publint", + "prepublishOnly": "npm run build", + "test:isomorphic": "npm run build && node scripts/isomorphic-check.mjs" + }, + "devDependencies": { + "@eslint/js": "^9.13.0", + "@types/node": "^22.7.0", + "eslint": "^9.13.0", + "publint": "^0.3.21", + "tsdown": "^0.22.1", + "typescript": "^5.6.0", + "typescript-eslint": "^8.10.0", + "vitest": "^4.0.0" + } +} diff --git a/typescript/scripts/isomorphic-check.mjs b/typescript/scripts/isomorphic-check.mjs new file mode 100644 index 0000000..a1f7f6b --- /dev/null +++ b/typescript/scripts/isomorphic-check.mjs @@ -0,0 +1,57 @@ +/** + * Browser-readiness proof for the BUILT artifact (dist/): delete Node's + * `Buffer` global, then load both entry points (ESM `dist/index.mjs` and CJS + * `dist/index.cjs`) and run a minimal issue → verify flow through each. Any + * accidental Node-builtin dependency in the bundle would throw here. + * + * Wired as `npm run test:isomorphic` (builds first, so dist is always fresh). + */ + +// Must run BEFORE the library is imported. +delete globalThis.Buffer; + +if (typeof globalThis.Buffer !== 'undefined') { + throw new Error('failed to delete the Buffer global'); +} + +const now = Math.floor(Date.now() / 1000); + +async function exercise(vi, label) { + const issuer = await vi.generateEs256Key(); + const user = await vi.generateEs256Key(); + const l1 = await vi.createLayer1( + new vi.IssuerCredential({ + iss: 'https://www.mastercard.com', + sub: 'user-123', + iat: now, + exp: now + 3600, + cnfJwk: user.publicKey, + email: 'user@example.com', + panLastFour: '1234', + scheme: 'Mastercard', + }), + issuer.privateKey, + ); + const parsed = vi.decodeSdJwt(l1.serialize()); + if (!(await vi.verifySdJwtSignature(parsed, issuer.publicKey))) { + throw new Error(`${label}: L1 signature did not verify`); + } + const resolved = await vi.resolveDisclosures(parsed); + if (resolved.email !== 'user@example.com') { + throw new Error(`${label}: disclosure resolution mismatch`); + } + const hash = await vi.hashAscii(l1.serialize()); + if (!/^[A-Za-z0-9_-]{43}$/.test(hash)) { + throw new Error(`${label}: hashAscii returned an unexpected value`); + } + console.log(`${label}: issue -> verify -> resolve -> hash OK (Buffer undefined)`); +} + +const esm = await import('../dist/index.mjs'); +await exercise(esm, 'dist/index.mjs (ESM)'); + +const { createRequire } = await import('node:module'); +const require = createRequire(import.meta.url); +await exercise(require('../dist/index.cjs'), 'dist/index.cjs (CJS)'); + +console.log('isomorphic check passed: the package runs with no Node Buffer global'); diff --git a/typescript/src/crypto/base64url.ts b/typescript/src/crypto/base64url.ts new file mode 100644 index 0000000..2b6dbfb --- /dev/null +++ b/typescript/src/crypto/base64url.ts @@ -0,0 +1,109 @@ +/** + * base64url (unpadded) helpers, matching the Python reference byte-for-byte. + * + * Pure-JS over Uint8Array — no Buffer, no btoa/atob — so the package runs in + * any Web-standard runtime (browsers and Node >= 20 alike). + */ + +const B64URL_ALPHABET = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_'; + +// Reverse lookup over ASCII: 6-bit value per code point, -1 for non-alphabet. +// Both base64 alphabets are accepted ('+/' map to the same values as '-_'), +// matching Node's base64url decoder (and Python's urlsafe b64decode, which +// only ever translates -_ back to +/). +const DECODE_TABLE = new Int8Array(128).fill(-1); +for (let i = 0; i < 64; i++) { + DECODE_TABLE[B64URL_ALPHABET.charCodeAt(i)] = i; +} +DECODE_TABLE[0x2b] = 62; // '+' +DECODE_TABLE[0x2f] = 63; // '/' + +const textEncoder = new TextEncoder(); +const textDecoder = new TextDecoder(); + +export function b64urlEncode(data: Uint8Array): string { + let out = ''; + let acc = 0; + let bits = 0; + for (const byte of data) { + acc = (acc << 8) | byte; + bits += 8; + while (bits >= 6) { + bits -= 6; + out += B64URL_ALPHABET[(acc >> bits) & 0x3f]; + } + } + if (bits > 0) { + out += B64URL_ALPHABET[(acc << (6 - bits)) & 0x3f]; + } + return out; +} + +export function b64urlDecode(s: string): Uint8Array { + // Match Python's `urlsafe_b64decode` (after its padding fixup), which raises + // on a length that is 1 more than a multiple of 4 (an impossible base64 + // length). Lenient decoders would otherwise silently return empty/garbage + // bytes for such input. + if (s.length % 4 === 1) { + throw new Error(`Invalid base64url string: length ${s.length} is not a valid base64 length`); + } + // Python's `urlsafe_b64decode(str)` first does `.encode('ascii')`, raising on + // any non-ASCII character. Lenient decoders would silently skip them as + // non-alphabet. + if (NON_ASCII_CHAR.test(s)) { + throw new Error('Invalid base64url string: contains non-ASCII characters'); + } + // Lenient core, byte-compatible with the previous Node-Buffer decoder (which + // the parity tests pin): non-alphabet ASCII characters are skipped, decoding + // stops at the first '=' padding character, and a trailing partial byte is + // dropped. + const out: number[] = []; + let acc = 0; + let bits = 0; + for (let i = 0; i < s.length; i++) { + const code = s.charCodeAt(i); + if (code === 0x3d) break; // '=': padding — stop decoding. + const value = DECODE_TABLE[code] ?? -1; + if (value < 0) continue; + acc = (acc << 6) | value; + bits += 6; + if (bits >= 8) { + bits -= 8; + out.push((acc >> bits) & 0xff); + } + } + return new Uint8Array(out); +} + +/** UTF-8 encode a string to bytes (compact JSON is ASCII, so this equals the ASCII bytes). */ +export function utf8(s: string): Uint8Array { + return textEncoder.encode(s); +} + +/** + * UTF-8 decode bytes to a string, replacing malformed sequences with U+FFFD — + * the same lossy behavior as Node's `Buffer.toString('utf8')`. + */ +export function utf8Decode(data: Uint8Array): string { + return textDecoder.decode(data); +} + +const NON_ASCII_CHAR = /[\u0080-\uffff]/; + +/** + * Encode an ASCII string to bytes (used for hashing disclosure strings and + * sd_hash / checkout_hash inputs). Throws on any code point above 0x7F, + * matching Python's `str.encode('ascii')` — Node's `'ascii'` encoding would + * instead silently mangle non-ASCII input (latin1-style), which made TS accept + * hash bindings over bytes Python refuses to produce. + */ +export function asciiBytes(s: string): Uint8Array { + if (NON_ASCII_CHAR.test(s)) { + throw new Error('asciiBytes: input contains non-ASCII characters'); + } + const out = new Uint8Array(s.length); + for (let i = 0; i < s.length; i++) { + out[i] = s.charCodeAt(i); + } + return out; +} diff --git a/typescript/src/crypto/disclosure.ts b/typescript/src/crypto/disclosure.ts new file mode 100644 index 0000000..351c51e --- /dev/null +++ b/typescript/src/crypto/disclosure.ts @@ -0,0 +1,85 @@ +/** SD-JWT selective-disclosure utilities, matching the Python reference byte-for-byte. */ + +import { asciiBytes, b64urlDecode, b64urlEncode, utf8, utf8Decode } from './base64url.js'; +import { compactJson } from './json.js'; + +/** A delegate-payload reference: `{"...": ""}`. */ +export interface DelegateRef { + '...': string; +} + +/** + * A decoded disclosure, per RFC 9901 §4.2: `[salt, claimName, claimValue]` + * (object property) or `[salt, claimValue]` (array element). Only the arity is + * checked at decode time (parity with the Python reference, which returns the + * raw list); consumers keep their per-element runtime guards for hostile input. + */ +export type DecodedDisclosure = [salt: string, claimName: string, claimValue: unknown] | [salt: string, claimValue: unknown]; + +/** Random 128-bit disclosure salt, base64url-encoded (16 bytes, as in the Python reference). */ +export async function generateSalt(): Promise { + return b64urlEncode(crypto.getRandomValues(new Uint8Array(16))); +} + +/** + * Create an SD-JWT disclosure. + * - object property: `[salt, claimName, claimValue]` + * - array element: `[salt, claimValue]` (pass `claimName = null`) + */ +export async function createDisclosure(claimName: string | null, claimValue: unknown, salt?: string): Promise { + const s = salt ?? (await generateSalt()); + const arr = claimName !== null ? [s, claimName, claimValue] : [s, claimValue]; + return b64urlEncode(utf8(compactJson(arr))); +} + +export function decodeDisclosure(disclosureB64: string): DecodedDisclosure { + // Route through b64urlDecode so impossible base64 lengths and non-ASCII input + // are rejected, matching Python's urlsafe_b64decode. + const parsed = JSON.parse(utf8Decode(b64urlDecode(disclosureB64))) as unknown; + // Per SD-JWT, a disclosure is [salt, value] (array element) or [salt, name, value] (object property). + if (!Array.isArray(parsed) || (parsed.length !== 2 && parsed.length !== 3)) { + throw new Error('Invalid disclosure: expected a 2- or 3-element array'); + } + return parsed as DecodedDisclosure; +} + +/** + * SHA-256 of the ASCII base64url disclosure *string* (not its decoded bytes), per SD-JWT. + * + * Not declared `async` on purpose: the non-ASCII guard must throw SYNCHRONOUSLY + * (parity with Python's `str.encode('ascii')` UnicodeEncodeError, pinned by the + * parity tests), which an `async` function would convert into a rejection. + */ +export function hashDisclosure(disclosureB64: string): Promise { + return hashBytes(asciiBytes(disclosureB64)); +} + +export async function createSdArray(disclosures: string[]): Promise { + return Promise.all(disclosures.map(hashDisclosure)); +} + +/** SHA-256 of raw bytes, base64url-encoded. */ +export async function hashBytes(data: Uint8Array): Promise { + const digest = await crypto.subtle.digest('SHA-256', data); + return b64urlEncode(new Uint8Array(digest)); +} + +/** + * SHA-256 of an ASCII string, base64url-encoded (used for sd_hash / checkout_hash). + * Throws synchronously on non-ASCII input — see `hashDisclosure`. + */ +export function hashAscii(s: string): Promise { + return hashBytes(asciiBytes(s)); +} + +export function createDelegateRef(disclosureHash: string): DelegateRef { + return { '...': disclosureHash }; +} + +/** + * Build a selective SD-JWT presentation string `~~~...~`. + * Used to compute the per-recipient L3 `sd_hash`. + */ +export function buildSelectivePresentation(baseJwt: string, disclosures: string[]): string { + return [baseJwt, ...disclosures].join('~') + '~'; +} diff --git a/typescript/src/crypto/index.ts b/typescript/src/crypto/index.ts new file mode 100644 index 0000000..a11a840 --- /dev/null +++ b/typescript/src/crypto/index.ts @@ -0,0 +1,6 @@ +export * from './base64url.js'; +export * from './json.js'; +export * from './signing.js'; +export * from './disclosure.js'; +export * from './sd-jwt.js'; +export * from './kb-sd-jwt.js'; diff --git a/typescript/src/crypto/json.ts b/typescript/src/crypto/json.ts new file mode 100644 index 0000000..292c50e --- /dev/null +++ b/typescript/src/crypto/json.ts @@ -0,0 +1,73 @@ +/** + * Compact JSON serialization matching Python's + * `json.dumps(obj, separators=(",", ":"))` (the default `ensure_ascii=True`). + * + * `JSON.stringify` matches Python for separators, control-character escaping, + * and `"`/`\` escaping. Two divergences are handled explicitly: + * + * 1. Non-ASCII: Python escapes every code point above U+007E as `\uXXXX`, + * whereas `JSON.stringify` emits them raw. We reproduce that escaping. + * + * 2. Numbers: `JSON.stringify` and Python `json.dumps` agree on integers within + * the safe range and on ordinary decimals (e.g. `279.99` → `"279.99"` in + * both), but diverge on a few edge classes: integers beyond ±(2^53 − 1) + * lose precision in JS, and floats that one side renders in scientific + * notation (`1e20` → `100000000000000000000` vs `1e+20`; `1e-7` → `1e-7` + * vs `1e-07`). For the agreeing cases we pass the value through untouched + * (byte-identical to Python). For the genuinely-divergent edge cases — which + * would silently break cross-impl `sd_hash`/signature verification — we + * reject, since the VI spec only ever calls for integers in minor units and + * integer Unix timestamps anyway. Ordinary non-integer numbers that + * serialize identically in both runtimes are allowed (a generic serializer + * cannot assume every number is a VI amount). + * + * Note on key order: both impls rely on insertion order for *string* keys. + * ECMAScript additionally hoists integer-index keys (decimal-string keys like + * `"10"`) ahead of string keys and sorts them numerically, which Python does + * not. VI's serialized structures use only fixed non-numeric schema field + * names, so this never arises — but callers MUST NOT serialize objects with + * purely-decimal-integer string keys through this function. + */ +export function compactJson(value: unknown): string { + return asciiEscape(JSON.stringify(value, numberGuard)); +} + +// Reject only numbers whose textual form actually diverges from Python's +// `json.dumps`; pass through every number that both runtimes serialize the same +// way (so a legitimate decimal like 279.99 is preserved). Divergent classes: +// - NaN / Infinity — not representable in JSON (JS emits `null`). +// - non-safe integers — |n| ≥ 2^53 loses precision / Python prints all digits. +// - floats in scientific form — |n| < 1e-4 or ≥ 1e16, or any value JS prints +// with an exponent, where JS and Python differ. +function numberGuard(_key: string, v: unknown): unknown { + if (typeof v !== 'number') return v; + if (!Number.isFinite(v)) { + throw new Error(`compactJson: ${v} is not representable in JSON`); + } + if (Number.isInteger(v)) { + if (!Number.isSafeInteger(v)) { + throw new Error( + `compactJson: integer ${v} exceeds the safe range (±2^53) and would serialize differently from the Python reference`, + ); + } + return v; + } + const abs = Math.abs(v); + if (abs < 1e-4 || abs >= 1e16 || String(v).includes('e')) { + throw new Error( + `compactJson: float ${v} would serialize in scientific notation differently from the Python reference; ` + + 'use integer minor units per the VI spec.', + ); + } + return v; +} + +// Every code point in U+007F..U+FFFF (built from escapes to avoid literal +// non-ASCII bytes in this source file). Astral characters are escaped via their +// surrogate halves, which also fall in this range — matching Python. +const NON_ASCII = new RegExp('[\\u007f-\\uffff]', 'g'); + +/** Escape non-ASCII as `\uXXXX`, matching Python's `ensure_ascii=True`. */ +export function asciiEscape(s: string): string { + return s.replace(NON_ASCII, (c) => '\\u' + c.charCodeAt(0).toString(16).padStart(4, '0')); +} diff --git a/typescript/src/crypto/kb-sd-jwt.ts b/typescript/src/crypto/kb-sd-jwt.ts new file mode 100644 index 0000000..dca6d7c --- /dev/null +++ b/typescript/src/crypto/kb-sd-jwt.ts @@ -0,0 +1,89 @@ +/** Key-Bound SD-JWT (KB-SD-JWT) operations. + * + * Exposed for completeness/interoperability. The Verifiable Intent L2/L3 flows + * embed the holder-binding claims directly in the SD-JWT payload (see issuance/) + * rather than appending a trailing KB-JWT, so these helpers are not used by the + * main pipeline. + */ + +import { b64urlEncode, utf8 } from './base64url.js'; +import { hashAscii } from './disclosure.js'; +import { compactJson } from './json.js'; +import { SdJwt } from './sd-jwt.js'; +import { type Es256Jwk, type IssuanceHeader, jwtDecodeParts, jwtEncode, makeSigner, makeVerifier } from './signing.js'; + +/** + * KB-JWT payload per RFC 9901: `iat`, `aud`, `nonce`, and `sd_hash` are + * REQUIRED in the signed payload; additional claims are permitted (open index + * signature). `sd_hash` may be omitted on input — `createKbSdJwt` computes it + * from the presentation. Applies to the construction path only; inbound + * KB-JWTs are untrusted and stay `unknown` until verification passes. + */ +export interface KbJwtPayload { + iat: number; + aud: string; + nonce: string; + sd_hash?: string; + [claim: string]: unknown; +} + +/** The holder-binding proof appended to an SD-JWT. */ +export class KbSdJwt { + constructor( + public header: Record, + public payload: Record, + public signature: Uint8Array, + ) {} + + get jwt(): string { + const h = b64urlEncode(utf8(compactJson(this.header))); + const p = b64urlEncode(utf8(compactJson(this.payload))); + return `${h}.${p}.${b64urlEncode(this.signature)}`; + } +} + +/** A complete SD-JWT presentation with a key-binding proof. */ +export class SdJwtWithKb { + constructor( + public sdJwt: SdJwt, + public kbJwt: KbSdJwt, + public disclosedIndices: number[] | null = null, + ) {} + + serialize(): string { + const sdPart = this.sdJwt.serialize(this.disclosedIndices ?? undefined); + return sdPart + this.kbJwt.jwt; + } +} + +export async function createKbSdJwt( + sdJwt: SdJwt, + holderHeader: IssuanceHeader, + holderPayload: KbJwtPayload, + holderPrivateJwk: Es256Jwk, + disclosedIndices: number[] | null = null, +): Promise { + const payload: Record = { ...holderPayload }; + if (!('sd_hash' in payload)) { + payload['sd_hash'] = await hashAscii(sdJwt.serialize(disclosedIndices ?? undefined)); + } + const signer = await makeSigner(holderPrivateJwk); + const token = await jwtEncode(holderHeader, payload, signer); + const decoded = jwtDecodeParts(token); + const kb = new KbSdJwt(decoded.header, decoded.payload, decoded.signature); + return new SdJwtWithKb(sdJwt, kb, disclosedIndices); +} + +/** Verify a key-binding JWT signature. Returns false (never rejects) on any invalid input. */ +export async function verifyKbJwt(kbJwt: KbSdJwt, publicJwk: Es256Jwk): Promise { + if (kbJwt.signature.length !== 64) return false; + try { + const h = b64urlEncode(utf8(compactJson(kbJwt.header))); + const p = b64urlEncode(utf8(compactJson(kbJwt.payload))); + const verifier = await makeVerifier(publicJwk); + return await verifier(`${h}.${p}`, b64urlEncode(kbJwt.signature)); + } catch { + // Malformed/non-P-256 public JWK (importKey reject): fail closed. + return false; + } +} diff --git a/typescript/src/crypto/py-json.ts b/typescript/src/crypto/py-json.ts new file mode 100644 index 0000000..2226ac0 --- /dev/null +++ b/typescript/src/crypto/py-json.ts @@ -0,0 +1,150 @@ +/** + * Python-parity re-encoding of a *previously signed* JSON segment. + * + * Signature verification re-encodes the decoded header/payload objects (never + * the raw signed bytes) so in-memory mutations are caught. Python's + * `json.loads`/`json.dumps` round-trip preserves the int/float distinction + * (`1.0` → `1.0`), but JS `JSON.parse` collapses `1.0` to `1` and rounds + * integers beyond ±2^53, so a plain `JSON.stringify` re-encode diverges from + * the signed bytes for those numbers and verification silently fails on + * credentials Python accepts. + * + * This module closes that gap: {@link parsePySegment} re-parses the raw + * segment text capturing each number's original lexeme (via `JSON.parse` + * source access), and re-serializes the value exactly as Python's + * `json.dumps(obj, separators=(",", ":"))` would — `1.0` stays `1.0`, `1e2` + * becomes `100.0`, big integers keep all their digits, floats outside + * [1e-4, 1e16) use Python's `e±NN` notation. The caller only uses this + * re-encoding when the in-memory object is still deep-equal to the parsed + * segment ({@link pyDeepEqual}), so mutation detection is preserved: any + * value change falls back to the strict `compactJson` path and the signature + * check fails, exactly as it does in Python. + * + * On runtimes without `JSON.parse` source access (pre-V8 12 / Node < 21), + * {@link parsePySegment} returns null and callers keep today's stricter + * behavior (such credentials verify as false instead of matching Python). + */ + +import { asciiEscape } from './json.js'; + +/** A number captured with its original JSON lexeme. */ +class PyNum { + constructor( + readonly value: number, + readonly source: string, + ) {} +} + +// Feature-detect JSON.parse source access (the `context` reviver parameter). +const HAS_SOURCE_ACCESS = ((): boolean => { + let seen = false; + try { + JSON.parse('1', ((_k: string, v: unknown, ctx?: { source?: string }) => { + seen = typeof ctx?.source === 'string'; + return v; + }) as unknown as (key: string, value: unknown) => unknown); + } catch { + return false; + } + return seen; +})(); + +export interface ParsedSegment { + /** The parsed value, identical to what `JSON.parse(text)` yields. */ + value: unknown; + /** The segment re-serialized as Python `json.dumps(separators=(",", ":"))` would. */ + pyText: string; +} + +/** + * Parse a JSON segment and compute its Python-compact re-serialization. + * Returns null when the runtime lacks source access or the text is not JSON. + */ +export function parsePySegment(text: string): ParsedSegment | null { + if (!HAS_SOURCE_ACCESS) return null; + let tree: unknown; + try { + tree = JSON.parse(text, ((_k: string, v: unknown, ctx?: { source?: string }) => + typeof v === 'number' && typeof ctx?.source === 'string' ? new PyNum(v, ctx.source) : v) as unknown as ( + key: string, + value: unknown, + ) => unknown); + } catch { + return null; + } + return walk(tree); +} + +function walk(node: unknown): ParsedSegment { + if (node instanceof PyNum) { + return { value: node.value, pyText: pyNumberText(node) }; + } + if (node === null || typeof node === 'boolean') { + return { value: node, pyText: String(node) }; + } + if (typeof node === 'string') { + return { value: node, pyText: asciiEscape(JSON.stringify(node)) }; + } + if (Array.isArray(node)) { + const items = node.map(walk); + return { value: items.map((i) => i.value), pyText: `[${items.map((i) => i.pyText).join(',')}]` }; + } + // JSON.parse can only produce plain objects beyond the cases above. + const obj = node as Record; + const value: Record = {}; + const parts: string[] = []; + for (const key of Object.keys(obj)) { + const item = walk(obj[key]); + value[key] = item.value; + parts.push(`${asciiEscape(JSON.stringify(key))}:${item.pyText}`); + } + return { value, pyText: `{${parts.join(',')}}` }; +} + +/** + * Render a parsed number as Python `json.dumps` would. + * + * A lexeme without `.`/`e`/`E` is a Python int: all digits are kept exactly + * (BigInt, so nothing rounds) and `-0` canonicalizes to `0`. Anything else is + * a Python float, rendered with `repr` semantics: shortest round-trip digits + * (shared with JS), `.0` suffix on integral values, and `e±NN` scientific + * notation only outside [1e-4, 1e16) — where JS `String()` would disagree. + */ +function pyNumberText(num: PyNum): string { + const { value, source } = num; + if (!/[.eE]/.test(source)) { + return BigInt(source).toString(); + } + if (Object.is(value, -0)) return '-0.0'; + if (Number.isInteger(value) && Math.abs(value) < 1e16) return `${value}.0`; + const [mantissa, expPart] = value.toExponential().split('e') as [string, string]; + const exp = Number(expPart); + if (exp < -4 || exp >= 16) { + return `${mantissa}e${exp < 0 ? '-' : '+'}${String(Math.abs(exp)).padStart(2, '0')}`; + } + return String(value); +} + +/** + * Order-sensitive deep equality between a parsed segment value and a live + * object. Numbers compare with `Object.is` so a `-0` → `0` mutation is caught + * (Python re-encodes the mutated value and the signature fails). Object key + * *order* participates because Python's `json.dumps` serializes dicts in + * insertion order — reordered keys change the signed bytes. + */ +export function pyDeepEqual(a: unknown, b: unknown): boolean { + if (typeof a === 'number' || typeof b === 'number') { + return Object.is(a, b); + } + if (a === null || b === null || typeof a !== 'object' || typeof b !== 'object') { + return a === b; + } + if (Array.isArray(a) || Array.isArray(b)) { + if (!Array.isArray(a) || !Array.isArray(b) || a.length !== b.length) return false; + return a.every((v, i) => pyDeepEqual(v, b[i])); + } + const aKeys = Object.keys(a); + const bKeys = Object.keys(b); + if (aKeys.length !== bKeys.length) return false; + return aKeys.every((k, i) => k === bKeys[i] && pyDeepEqual((a as Record)[k], (b as Record)[k])); +} diff --git a/typescript/src/crypto/sd-jwt.ts b/typescript/src/crypto/sd-jwt.ts new file mode 100644 index 0000000..502e9bb --- /dev/null +++ b/typescript/src/crypto/sd-jwt.ts @@ -0,0 +1,196 @@ +/** SD-JWT creation, parsing, signature verification, and disclosure resolution. */ + +import { b64urlDecode, b64urlEncode, utf8, utf8Decode } from './base64url.js'; +import { type DecodedDisclosure, decodeDisclosure, hashDisclosure } from './disclosure.js'; +import { compactJson } from './json.js'; +import { parsePySegment, pyDeepEqual } from './py-json.js'; +import { type Es256Jwk, jwtDecodeParts, jwtEncode, makeSigner, makeVerifier } from './signing.js'; + +export interface SdJwtInit { + header: Record; + payload: Record; + signature: Uint8Array; + disclosures?: string[]; + disclosureValues?: DecodedDisclosure[]; + rawHeaderB64?: string | null; + rawPayloadB64?: string | null; +} + +/** A parsed SD-JWT with its disclosures. */ +export class SdJwt { + header: Record; + payload: Record; + signature: Uint8Array; + disclosures: string[]; + disclosureValues: DecodedDisclosure[]; + rawHeaderB64: string | null; + rawPayloadB64: string | null; + + constructor(init: SdJwtInit) { + this.header = init.header; + this.payload = init.payload; + this.signature = init.signature; + this.disclosures = init.disclosures ?? []; + this.disclosureValues = init.disclosureValues ?? []; + this.rawHeaderB64 = init.rawHeaderB64 ?? null; + this.rawPayloadB64 = init.rawPayloadB64 ?? null; + } + + /** `
..` — prefers captured raw segments for round-trip stability. */ + get issuerJwt(): string { + const h = this.rawHeaderB64 ?? b64urlEncode(utf8(compactJson(this.header))); + const p = this.rawPayloadB64 ?? b64urlEncode(utf8(compactJson(this.payload))); + return `${h}.${p}.${b64urlEncode(this.signature)}`; + } + + /** + * Serialize to `~~~`. If `includeDisclosures` is given, only + * those indices — with Python list semantics: negative indices count from + * the end, and an out-of-range index throws (Python raises IndexError) + * rather than being silently skipped, since a dropped disclosure would + * change the produced presentation bytes. + */ + serialize(includeDisclosures?: number[]): string { + const parts = [this.issuerJwt]; + if (includeDisclosures !== undefined) { + for (const i of includeDisclosures) { + const d = this.disclosures.at(i); + if (d === undefined) { + throw new RangeError(`Disclosure index ${i} out of range (have ${this.disclosures.length})`); + } + parts.push(d); + } + } else { + parts.push(...this.disclosures); + } + return parts.join('~') + '~'; + } +} + +/** Create an SD-JWT. The payload must already include `_sd`/`_sd_alg` as needed. */ +export async function createSdJwt( + header: Record, + payload: Record, + disclosures: string[], + privateJwk: Es256Jwk, +): Promise { + const signer = await makeSigner(privateJwk); + const token = await jwtEncode(header, payload, signer); + const [rawHeaderB64, rawPayloadB64] = token.split('.'); + const decoded = jwtDecodeParts(token); + const disclosureValues = disclosures.map(decodeDisclosure); + return new SdJwt({ + header: decoded.header, + payload: decoded.payload, + signature: decoded.signature, + disclosures, + disclosureValues, + rawHeaderB64: rawHeaderB64 ?? null, + rawPayloadB64: rawPayloadB64 ?? null, + }); +} + +/** Parse a serialized SD-JWT. Throws on malformed input. */ +export function decodeSdJwt(serialized: string): SdJwt { + try { + const parts = serialized.split('~'); + const jwtPart = parts[0] ?? ''; + const disclosures = parts.slice(1).filter((d) => d.length > 0); + const decoded = jwtDecodeParts(jwtPart); + const seg = jwtPart.split('.'); + const disclosureValues = disclosures.map(decodeDisclosure); + return new SdJwt({ + header: decoded.header, + payload: decoded.payload, + signature: decoded.signature, + disclosures, + disclosureValues, + rawHeaderB64: seg[0] ?? null, + rawPayloadB64: seg[1] ?? null, + }); + } catch (e) { + throw new Error(`Invalid SD-JWT: ${(e as Error).message}`); + } +} + +/** + * Verify the issuer signature. Always re-encodes from the current header/payload + * objects (never the cached raw segments) so in-memory mutations are caught. + * Returns false (never rejects) on a malformed key or signature. + */ +export async function verifySdJwtSignature(sdJwt: SdJwt, publicJwk: Es256Jwk): Promise { + try { + const h = reencodeSegmentB64(sdJwt.rawHeaderB64, sdJwt.header); + const p = reencodeSegmentB64(sdJwt.rawPayloadB64, sdJwt.payload); + if (sdJwt.signature.length !== 64) return false; + const verifier = await makeVerifier(publicJwk); + return await verifier(`${h}.${p}`, b64urlEncode(sdJwt.signature)); + } catch { + // Re-encoding failure or a malformed public JWK (importKey reject): fail closed. + return false; + } +} + +/** + * Re-encode one JWT segment for signature verification, as Python's + * `json.dumps(current_object)` would. + * + * Python's loads/dumps round-trip preserves number spellings JS can't hold in + * a `number` (`1.0`, big integers), so a plain `compactJson` re-encode falsely + * fails on such credentials. When the current object is still deep-equal to + * what the raw segment parses to (i.e. unmutated), we re-serialize the segment + * with Python semantics — its number lexemes tell us int vs float. Any + * mutation makes the deep-equal fail and drops to the strict `compactJson` + * path, whose output then mismatches the signed bytes — same rejection Python + * produces by re-encoding the mutated object. + */ +function reencodeSegmentB64(rawB64: string | null, current: Record): string { + if (rawB64 !== null) { + const seg = parsePySegment(utf8Decode(b64urlDecode(rawB64))); + if (seg !== null && pyDeepEqual(seg.value, current)) { + return b64urlEncode(utf8(seg.pyText)); + } + } + return b64urlEncode(utf8(compactJson(current))); +} + +/** Resolve all disclosures into the payload, returning a full claim set. */ +export async function resolveDisclosures(sdJwt: SdJwt): Promise> { + const result: Record = { ...sdJwt.payload }; + + const sdRaw = result['_sd']; + const sdHashes = new Set(Array.isArray(sdRaw) ? sdRaw.filter((v): v is string => typeof v === 'string') : []); + + // Object-property disclosures (3-element) whose digest is listed in `_sd` + // become payload claims, applied in *presentation order* — on a claim-name + // collision the last presented disclosure wins, matching Python's + // `zip(disclosures, disclosure_values)` loop (NOT the `_sd` array order, + // which can resolve a different value for identical hostile input). + // length-2 array-element disclosures are resolved via delegate_payload below. + const valueByHash = new Map(); + for (let i = 0; i < sdJwt.disclosures.length; i++) { + const disc = sdJwt.disclosures[i]; + const dv = sdJwt.disclosureValues[i]; + if (disc === undefined || dv === undefined) continue; + const discHash = await hashDisclosure(disc); + valueByHash.set(discHash, dv); + if (sdHashes.has(discHash) && dv.length === 3 && typeof dv[1] === 'string') { + result[dv[1]] = dv[2]; + } + } + + // Array-element disclosures referenced by `delegate_payload` resolve to their value. + const delegatePayload = result['delegate_payload']; + if (Array.isArray(delegatePayload) && delegatePayload.length > 0) { + result['delegate_payload'] = delegatePayload.map((item: unknown) => { + if (item && typeof item === 'object' && '...' in item) { + const refHash: unknown = (item as Record)['...']; + const dv = typeof refHash === 'string' ? valueByHash.get(refHash) : undefined; + return dv ? dv[dv.length - 1] : item; + } + return item; + }); + } + + return result; +} diff --git a/typescript/src/crypto/signing.ts b/typescript/src/crypto/signing.ts new file mode 100644 index 0000000..8e3bc0b --- /dev/null +++ b/typescript/src/crypto/signing.ts @@ -0,0 +1,110 @@ +/** + * ES256 (ECDSA P-256) signing built on WebCrypto (`crypto.subtle` — a global in + * browsers and Node >= 20 alike), plus the compact JWT encode/decode used + * throughout the Verifiable Intent layers. + * + * The signer returns a base64url-encoded raw 64-byte (r‖s) JOSE signature — + * exactly what WebCrypto's ECDSA produces (IEEE P1363 form) and the same form + * the Python reference produces — so signatures verify across both + * implementations. + */ + +import { b64urlDecode, b64urlEncode, utf8, utf8Decode } from './base64url.js'; +import { compactJson } from './json.js'; + +/** An EC P-256 JSON Web Key. `d` is present only for private keys. */ +export interface Es256Jwk { + kty: string; + crv: string; + x: string; + y: string; + d?: string; + kid?: string; + [key: string]: unknown; +} + +export const ALG = 'ES256'; + +const ECDSA_P256 = { name: 'ECDSA', namedCurve: 'P-256' } as const; +const ECDSA_SHA256 = { name: 'ECDSA', hash: 'SHA-256' } as const; + +/** + * JOSE header for JWTs this library constructs. `alg` is pinned to ES256 — + * the spec's whitelist — so a widened algorithm can't compile on the issuance + * path. Inbound headers on the verification path are untrusted and stay + * `unknown` until `validateHeader` runs. + */ +export interface IssuanceHeader { + alg: typeof ALG; + typ: string; + kid?: string; + [param: string]: unknown; +} + +export type Signer = (data: string) => Promise; +export type Verifier = (data: string, signatureBase64url: string) => Promise; + +export async function makeSigner(privateJwk: Es256Jwk): Promise { + const privateKey = await crypto.subtle.importKey('jwk', privateJwk, ECDSA_P256, false, ['sign']); + return async (data: string): Promise => { + const signature = await crypto.subtle.sign(ECDSA_SHA256, privateKey, utf8(data)); + return b64urlEncode(new Uint8Array(signature)); + }; +} + +export async function makeVerifier(publicJwk: Es256Jwk): Promise { + const publicKey = await crypto.subtle.importKey('jwk', publicJwk, ECDSA_P256, false, ['verify']); + return async (data: string, signatureBase64url: string): Promise => { + return crypto.subtle.verify(ECDSA_SHA256, publicKey, b64urlDecode(signatureBase64url), utf8(data)); + }; +} + +export async function generateEs256Key(): Promise<{ publicKey: Es256Jwk; privateKey: Es256Jwk }> { + const keyPair = await crypto.subtle.generateKey(ECDSA_P256, true, ['sign', 'verify']); + const publicKey = (await crypto.subtle.exportKey('jwk', keyPair.publicKey)) as Es256Jwk; + const privateKey = (await crypto.subtle.exportKey('jwk', keyPair.privateKey)) as Es256Jwk; + return { publicKey, privateKey }; +} + +export interface JwtParts { + header: Record; + payload: Record; + signature: Uint8Array; +} + +/** Encode and ES256-sign a compact JWT: `b64url(header).b64url(payload).b64url(sig)`. */ +export async function jwtEncode(header: object, payload: object, signer: Signer): Promise { + const h = b64urlEncode(utf8(compactJson(header))); + const p = b64urlEncode(utf8(compactJson(payload))); + const sig = await signer(`${h}.${p}`); + return `${h}.${p}.${sig}`; +} + +export function jwtDecodeParts(token: string): JwtParts { + const parts = token.split('.'); + const [headerB64, payloadB64, sigB64] = parts; + if (parts.length !== 3 || headerB64 === undefined || payloadB64 === undefined || sigB64 === undefined) { + throw new Error(`Invalid JWT: expected 3 parts, got ${parts.length}`); + } + // All three segments go through b64urlDecode so impossible base64 lengths and + // non-ASCII input are rejected, matching Python's urlsafe_b64decode. + const header = JSON.parse(utf8Decode(b64urlDecode(headerB64))) as Record; + const payload = JSON.parse(utf8Decode(b64urlDecode(payloadB64))) as Record; + const signature = b64urlDecode(sigB64); + return { header, payload, signature }; +} + +/** + * Verify a detached ES256 signature over `signingInput` (raw bytes), given a public JWK. + * Returns false (never rejects) on a malformed key or signature. + */ +export async function es256Verify(signingInput: string, signature: Uint8Array, publicJwk: Es256Jwk): Promise { + if (signature.length !== 64) return false; + try { + const verifier = await makeVerifier(publicJwk); + return await verifier(signingInput, b64urlEncode(signature)); + } catch { + // A malformed/non-P-256 public JWK makes importKey reject; fail closed, don't throw. + return false; + } +} diff --git a/typescript/src/index.ts b/typescript/src/index.ts new file mode 100644 index 0000000..9d7b160 --- /dev/null +++ b/typescript/src/index.ts @@ -0,0 +1,16 @@ +/** + * Verifiable Intent — TypeScript implementation. + * + * A layered SD-JWT delegation chain (Issuer → User → Agent) for cryptographic + * agent authorization in commerce. Port of the Python reference implementation. + * + * Surface: crypto primitives, models, issuance, and verification + * (chain verification, integrity bindings, and the constraint checker). + */ + +export const VERSION = '0.1.0'; + +export * from './crypto/index.js'; +export * from './models/index.js'; +export * from './issuance/index.js'; +export * from './verification/index.js'; diff --git a/typescript/src/internal/guards.ts b/typescript/src/internal/guards.ts new file mode 100644 index 0000000..3fc08da --- /dev/null +++ b/typescript/src/internal/guards.ts @@ -0,0 +1,16 @@ +/** + * Internal runtime type guards shared across modules. + * + * Intentionally NOT re-exported from src/index.ts — these are implementation + * details. Centralized so the security-critical `isJsonObject` check (which underpins + * fail-closed validation throughout the verifier) can never drift between call + * sites. + */ + +import type { JsonObject } from '../models/constraints.js'; + +/** True for a plain object — not null, not an array. */ +export const isJsonObject = (v: unknown): v is JsonObject => typeof v === 'object' && v !== null && !Array.isArray(v); + +/** Coerce to an array, or `[]` when the value isn't one (defensive handling of untrusted input). */ +export const asArray = (v: unknown): unknown[] => (Array.isArray(v) ? v : []); diff --git a/typescript/src/issuance/agent.ts b/typescript/src/issuance/agent.ts new file mode 100644 index 0000000..674e2be --- /dev/null +++ b/typescript/src/issuance/agent.ts @@ -0,0 +1,126 @@ +/** Layer 3: Agent mandate creation (autonomous mode only). + * + * L3 is split into two credentials: + * createLayer3Payment() → L3a for the payment network + * createLayer3Checkout() → L3b for the merchant + */ + +import { + buildSelectivePresentation, + createDelegateRef, + createDisclosure, + generateSalt, + hashAscii, + hashDisclosure, +} from '../crypto/disclosure.js'; +import { createSdJwt, SdJwt } from '../crypto/sd-jwt.js'; +import type { Es256Jwk, IssuanceHeader } from '../crypto/signing.js'; +import type { CheckoutL3Mandate, PaymentL3Mandate } from '../models/agent-mandate.js'; +import type { IssueOptions } from './options.js'; + +/** The slice of the L2 presentation an L3a payment mandate's `sd_hash` binds to. */ +export interface Layer3PaymentPresentation { + /** The L2 issuer JWT (base compact JWT, without disclosures). */ + l2BaseJwt: string; + /** The L2 payment-mandate disclosure the network receives. */ + paymentDisclosure: string; + /** The L2 merchant disclosure the network receives. */ + merchantDisclosure: string; +} + +/** + * Create L3a: the payment mandate for the network. The `sd_hash` binds to the + * L2 presentation as the network sees it (L2 base JWT + payment + merchant + * disclosures). + */ +export async function createLayer3Payment( + mandate: PaymentL3Mandate, + agentPrivateJwk: Es256Jwk, + presentation: Layer3PaymentPresentation, + opts: IssueOptions = {}, +): Promise { + const kid = opts.kid ?? 'agent-key-1'; + const nextSalt = opts.saltSource ?? generateSalt; + + const disclosures: string[] = []; + if (mandate.finalMerchant) { + disclosures.push(await createDisclosure(null, mandate.finalMerchant, await nextSalt())); + } + if (mandate.finalPayment) { + disclosures.push(await createDisclosure(null, mandate.finalPayment.toJSON(), await nextSalt())); + } + + const delegatePayload = await Promise.all(disclosures.map(async (d) => createDelegateRef(await hashDisclosure(d)))); + + const selectivePresentation = buildSelectivePresentation(presentation.l2BaseJwt, [ + presentation.paymentDisclosure, + presentation.merchantDisclosure, + ]); + const sdHash = await hashAscii(selectivePresentation); + + const payload: Record = { + nonce: mandate.nonce, + aud: mandate.aud, + sd_hash: sdHash, + iat: mandate.iat, + delegate_payload: delegatePayload, + _sd_alg: 'sha-256', + }; + if (mandate.iss !== null) payload.iss = mandate.iss; + if (mandate.exp !== null) payload.exp = mandate.exp; + + const header: IssuanceHeader = { alg: 'ES256', typ: 'kb-sd-jwt', kid }; + return createSdJwt(header, payload, disclosures, agentPrivateJwk); +} + +/** The slice of the L2 presentation an L3b checkout mandate's `sd_hash` binds to. */ +export interface Layer3CheckoutPresentation { + /** The L2 issuer JWT (base compact JWT, without disclosures). */ + l2BaseJwt: string; + /** The L2 checkout-mandate disclosure the merchant receives. */ + checkoutDisclosure: string; + /** The L2 line-item disclosure the merchant receives. */ + itemDisclosure: string; +} + +/** + * Create L3b: the checkout mandate for the merchant. The `sd_hash` binds to the + * L2 presentation as the merchant sees it (L2 base JWT + checkout + item + * disclosures). + */ +export async function createLayer3Checkout( + mandate: CheckoutL3Mandate, + agentPrivateJwk: Es256Jwk, + presentation: Layer3CheckoutPresentation, + opts: IssueOptions = {}, +): Promise { + const kid = opts.kid ?? 'agent-key-1'; + const nextSalt = opts.saltSource ?? generateSalt; + + const disclosures: string[] = []; + if (mandate.finalCheckout) { + disclosures.push(await createDisclosure(null, mandate.finalCheckout.toJSON(), await nextSalt())); + } + + const delegatePayload = await Promise.all(disclosures.map(async (d) => createDelegateRef(await hashDisclosure(d)))); + + const selectivePresentation = buildSelectivePresentation(presentation.l2BaseJwt, [ + presentation.checkoutDisclosure, + presentation.itemDisclosure, + ]); + const sdHash = await hashAscii(selectivePresentation); + + const payload: Record = { + nonce: mandate.nonce, + aud: mandate.aud, + sd_hash: sdHash, + iat: mandate.iat, + delegate_payload: delegatePayload, + _sd_alg: 'sha-256', + }; + if (mandate.iss !== null) payload.iss = mandate.iss; + if (mandate.exp !== null) payload.exp = mandate.exp; + + const header: IssuanceHeader = { alg: 'ES256', typ: 'kb-sd-jwt', kid }; + return createSdJwt(header, payload, disclosures, agentPrivateJwk); +} diff --git a/typescript/src/issuance/index.ts b/typescript/src/issuance/index.ts new file mode 100644 index 0000000..b674c3e --- /dev/null +++ b/typescript/src/issuance/index.ts @@ -0,0 +1,4 @@ +export * from './options.js'; +export * from './issuer.js'; +export * from './user.js'; +export * from './agent.js'; diff --git a/typescript/src/issuance/issuer.ts b/typescript/src/issuance/issuer.ts new file mode 100644 index 0000000..22c00f0 --- /dev/null +++ b/typescript/src/issuance/issuer.ts @@ -0,0 +1,34 @@ +/** Layer 1: Issuer credential creation. */ + +import { createDisclosure, createSdArray, generateSalt } from '../crypto/disclosure.js'; +import { createSdJwt, SdJwt } from '../crypto/sd-jwt.js'; +import type { Es256Jwk, IssuanceHeader } from '../crypto/signing.js'; +import { IssuerCredential } from '../models/issuer-credential.js'; +import type { IssueOptions } from './options.js'; + +/** + * Create a Layer 1 Issuer SD-JWT. + * + * One selectively disclosable claim (email only). Always visible: iss, sub, + * iat, exp, vct, cnf, pan_last_four, scheme (+ optional aud, card_id). + */ +export async function createLayer1( + credential: IssuerCredential, + issuerPrivateJwk: Es256Jwk, + opts: IssueOptions = {}, +): Promise { + const kid = opts.kid ?? 'mastercard-issuer-key-1'; + const nextSalt = opts.saltSource ?? generateSalt; + + const disclosures: string[] = []; + if (credential.email !== null) { + disclosures.push(await createDisclosure('email', credential.email, await nextSalt())); + } + + const payload = credential.toJSON(); + payload['_sd'] = await createSdArray(disclosures); + payload['_sd_alg'] = 'sha-256'; + + const header: IssuanceHeader = { alg: 'ES256', typ: 'sd+jwt', kid }; + return createSdJwt(header, payload, disclosures, issuerPrivateJwk); +} diff --git a/typescript/src/issuance/options.ts b/typescript/src/issuance/options.ts new file mode 100644 index 0000000..7d37826 --- /dev/null +++ b/typescript/src/issuance/options.ts @@ -0,0 +1,14 @@ +/** Shared issuance options. */ + +/** + * Produces disclosure salts. Defaults to a cryptographically random source. + * Tests inject a deterministic FIFO source to reproduce recorded vectors. + */ +export type SaltSource = () => string | Promise; + +export interface IssueOptions { + /** Key identifier placed in the JWT header. Defaults per layer. */ + kid?: string; + /** Override the disclosure salt source (e.g. for deterministic tests). */ + saltSource?: SaltSource; +} diff --git a/typescript/src/issuance/user.ts b/typescript/src/issuance/user.ts new file mode 100644 index 0000000..46244c8 --- /dev/null +++ b/typescript/src/issuance/user.ts @@ -0,0 +1,230 @@ +/** Layer 2: User mandate creation for both Immediate and Autonomous modes. */ + +import { + createDelegateRef, + createDisclosure, + type DelegateRef, + generateSalt, + hashAscii, + hashDisclosure, +} from '../crypto/disclosure.js'; +import { createSdJwt, SdJwt } from '../crypto/sd-jwt.js'; +import type { Es256Jwk, IssuanceHeader } from '../crypto/signing.js'; +import { type JsonObject, ReferenceConstraint } from '../models/constraints.js'; +import { MandateMode, type UserMandate } from '../models/user-mandate.js'; +import type { IssueOptions } from './options.js'; +import { asArray } from '../internal/guards.js'; + +/** + * Result of Layer 2 creation in Immediate mode. The user signs final values + * directly — there is no onward agent delegation. + */ +export class ImmediateL2Result { + constructor(public sdJwt: SdJwt) {} + + serialize(): string { + return this.sdJwt.serialize(); + } +} + +/** Create Layer 2 KB-SD-JWT for Immediate mode (final values, no delegation). */ +export async function createLayer2Immediate( + mandate: UserMandate, + userPrivateJwk: Es256Jwk, + opts: IssueOptions = {}, +): Promise { + if (mandate.mode !== MandateMode.IMMEDIATE) { + throw new Error(`createLayer2Immediate() requires mode=IMMEDIATE, got ${String(mandate.mode)}`); + } + const kid = opts.kid ?? 'user-device-key-1'; + const nextSalt = opts.saltSource ?? generateSalt; + + const disclosures: string[] = []; + + // Auto-compute checkout_hash and transaction_id BEFORE disclosure serialization. + const cm = mandate.checkoutMandate; + if (cm && cm.checkoutJwt) { + const computedHash = await hashAscii(cm.checkoutJwt); + if (!cm.checkoutHash) cm.checkoutHash = computedHash; + if (mandate.paymentMandate && !mandate.paymentMandate.transactionId) { + mandate.paymentMandate.transactionId = computedHash; + } + } + + if (mandate.checkoutMandate) { + disclosures.push(await createDisclosure(null, mandate.checkoutMandate.toJSON(), await nextSalt())); + } + if (mandate.paymentMandate) { + disclosures.push(await createDisclosure(null, mandate.paymentMandate.toJSON(), await nextSalt())); + } + + const delegatePayload = await Promise.all(disclosures.map(async (d) => createDelegateRef(await hashDisclosure(d)))); + + const payload: Record = { + nonce: mandate.nonce, + aud: mandate.aud, + iat: mandate.iat, + sd_hash: mandate.sdHash, + delegate_payload: delegatePayload, + _sd_alg: 'sha-256', + }; + if (mandate.iss !== null) payload.iss = mandate.iss; + if (mandate.exp !== null) payload.exp = mandate.exp; + + const sdHashes = await Promise.all(disclosures.map(hashDisclosure)); + if (sdHashes.length) payload._sd = sdHashes; + + const header: IssuanceHeader = { alg: 'ES256', typ: 'kb-sd-jwt', kid }; + const sdJwt = await createSdJwt(header, payload, disclosures, userPrivateJwk); + return new ImmediateL2Result(sdJwt); +} + +/** Create Layer 2 KB-SD-JWT for Autonomous mode (open mandates + nested disclosures). */ +export async function createLayer2Autonomous( + mandate: UserMandate, + userPrivateJwk: Es256Jwk, + opts: IssueOptions = {}, +): Promise { + if (mandate.mode !== MandateMode.AUTONOMOUS) { + throw new Error(`createLayer2Autonomous() requires mode=AUTONOMOUS, got ${String(mandate.mode)}`); + } + const kid = opts.kid ?? 'user-device-key-1'; + const nextSalt = opts.saltSource ?? generateSalt; + + const disclosures: string[] = []; + + // 1. Standalone merchant disclosures + const merchantDiscHashes: string[] = []; + for (const merchant of mandate.merchants) { + const d = await createDisclosure(null, merchant, await nextSalt()); + disclosures.push(d); + merchantDiscHashes.push(await hashDisclosure(d)); + } + + // 2. Standalone acceptable item disclosures + const itemDiscHashes: string[] = []; + for (const item of mandate.acceptableItems) { + const d = await createDisclosure(null, item, await nextSalt()); + disclosures.push(d); + itemDiscHashes.push(await hashDisclosure(d)); + } + + // 3. Open checkout mandate disclosure (constraint merchant/item refs scoped to subset) + let checkoutDisc: string | null = null; + if (mandate.checkoutMandate) { + const checkoutObj = mandate.checkoutMandate.toJSON(); + for (const c of (checkoutObj.constraints as JsonObject[] | undefined) ?? []) { + if (c.type === 'mandate.checkout.allowed_merchants') { + c.allowed = matchMerchantRefs(asArray(c.allowed), mandate.merchants, merchantDiscHashes); + } else if (c.type === 'mandate.checkout.line_items') { + for (const itemEntry of (c.items as JsonObject[] | undefined) ?? []) { + itemEntry.acceptable_items = matchItemRefs(asArray(itemEntry.acceptable_items), mandate.acceptableItems, itemDiscHashes); + } + } + } + checkoutDisc = await createDisclosure(null, checkoutObj, await nextSalt()); + disclosures.push(checkoutDisc); + } + + // 4. Open payment mandate disclosure (payee refs + injected reference constraint) + let paymentDisc: string | null = null; + if (mandate.paymentMandate) { + const paymentObj = mandate.paymentMandate.toJSON(); + for (const c of (paymentObj.constraints as JsonObject[] | undefined) ?? []) { + if (c.type === 'mandate.payment.allowed_payees') { + c.allowed = matchMerchantRefs(asArray(c.allowed), mandate.merchants, merchantDiscHashes); + } + } + if (checkoutDisc !== null) { + const refConstraint = new ReferenceConstraint({ conditionalTransactionId: await hashDisclosure(checkoutDisc) }); + if (!Array.isArray(paymentObj.constraints)) paymentObj.constraints = []; + (paymentObj.constraints as JsonObject[]).push(refConstraint.toJSON()); + } + paymentDisc = await createDisclosure(null, paymentObj, await nextSalt()); + disclosures.push(paymentDisc); + } + + // 5. delegate_payload references the two mandate disclosures + const delegatePayload: DelegateRef[] = []; + if (checkoutDisc) delegatePayload.push(createDelegateRef(await hashDisclosure(checkoutDisc))); + if (paymentDisc) delegatePayload.push(createDelegateRef(await hashDisclosure(paymentDisc))); + + const sdHashes = await Promise.all(disclosures.map(hashDisclosure)); + + const payload: Record = { + nonce: mandate.nonce, + aud: mandate.aud, + iat: mandate.iat, + sd_hash: mandate.sdHash, + delegate_payload: delegatePayload, + _sd_alg: 'sha-256', + }; + if (sdHashes.length) payload._sd = sdHashes; + if (mandate.iss !== null) payload.iss = mandate.iss; + if (mandate.exp !== null) payload.exp = mandate.exp; + + const header: IssuanceHeader = { alg: 'ES256', typ: 'kb-sd-jwt+kb', kid }; + return createSdJwt(header, payload, disclosures, userPrivateJwk); +} + +/** + * Match a constraint's merchant list against the mandate merchants, returning + * scoped SD refs. Empty input → empty list (fail-closed at verification). + */ +function matchMerchantRefs(originalMerchants: unknown[], mandateMerchants: JsonObject[], discHashes: string[]): DelegateRef[] { + if (originalMerchants.length === 0) return []; + const matched: DelegateRef[] = []; + for (const origRaw of originalMerchants) { + const orig = origRaw as JsonObject; + const origId = orig.id; + const origName = orig.name; + if (!origId && !origName) { + throw new Error(`Constraint merchant missing both 'id' and 'name': ${JSON.stringify(orig)}`); + } + let found = false; + for (const [idx, m] of mandateMerchants.entries()) { + const mId = m.id; + const match = origId && mId ? mId === origId : m.name === origName && Boolean(origName); + if (match) { + const disc = discHashes[idx]; + if (disc !== undefined) matched.push(createDelegateRef(disc)); + found = true; + break; + } + } + if (!found) { + throw new Error(`Constraint references unknown merchant: ${String(origId ?? origName)}`); + } + } + return matched; +} + +/** + * Match a constraint's item list against the mandate items, returning scoped SD + * refs. Empty input → empty list (any SKU allowed per spec). + */ +function matchItemRefs(originalItems: unknown[], mandateItems: JsonObject[], discHashes: string[]): DelegateRef[] { + if (originalItems.length === 0) return []; + const matched: DelegateRef[] = []; + for (const origRaw of originalItems) { + if (typeof origRaw !== 'object' || origRaw === null) { + throw new Error(`Constraint item must be an object: ${JSON.stringify(origRaw)}`); + } + const orig = origRaw as JsonObject; + const origKeys = [orig.id, orig.sku].filter((k): k is unknown => Boolean(k)); + let found = false; + for (const [idx, item] of mandateItems.entries()) { + const itemKeys = [item.id, item.sku].filter((k) => Boolean(k)); + if (origKeys.length > 0 && origKeys.some((k) => itemKeys.includes(k))) { + const disc = discHashes[idx]; + if (disc !== undefined) matched.push(createDelegateRef(disc)); + found = true; + break; + } + } + if (!found) { + throw new Error(`Constraint references unknown item: ${String(orig.id ?? orig.sku ?? JSON.stringify(orig))}`); + } + } + return matched; +} diff --git a/typescript/src/models/agent-mandate.ts b/typescript/src/models/agent-mandate.ts new file mode 100644 index 0000000..2909cbe --- /dev/null +++ b/typescript/src/models/agent-mandate.ts @@ -0,0 +1,115 @@ +/** Layer 3: Agent KB-SD-JWT mandate models (autonomous mode only). + * + * L3 is split into two credentials: + * PaymentL3Mandate (L3a) → payment network, final payment values + * CheckoutL3Mandate (L3b) → merchant, final checkout JWT + */ + +import type { JsonObject } from './constraints.js'; + +/** L3b final checkout mandate for the merchant. */ +export class FinalCheckoutMandate { + vct: string; + checkoutJwt: string; + checkoutHash: string; + + constructor(init: { vct?: string; checkoutJwt?: string; checkoutHash?: string } = {}) { + this.vct = init.vct ?? 'mandate.checkout.1'; + this.checkoutJwt = init.checkoutJwt ?? ''; + this.checkoutHash = init.checkoutHash ?? ''; + } + + toJSON(): JsonObject { + return { vct: this.vct, checkout_jwt: this.checkoutJwt, checkout_hash: this.checkoutHash }; + } +} + +/** L3a final payment mandate for the network. */ +export class FinalPaymentMandate { + vct: string; + transactionId: string; + payee: JsonObject; + paymentAmount: JsonObject; + paymentInstrument: JsonObject; + + constructor( + init: { vct?: string; transactionId?: string; payee?: JsonObject; paymentAmount?: JsonObject; paymentInstrument?: JsonObject } = {}, + ) { + this.vct = init.vct ?? 'mandate.payment.1'; + this.transactionId = init.transactionId ?? ''; + this.payee = init.payee ?? {}; + this.paymentAmount = init.paymentAmount ?? {}; + this.paymentInstrument = init.paymentInstrument ?? {}; + } + + toJSON(): JsonObject { + return { + vct: this.vct, + transaction_id: this.transactionId, + payee: this.payee, + payment_amount: this.paymentAmount, + payment_instrument: this.paymentInstrument, + }; + } +} + +/** L3a KB-SD-JWT: the agent's payment fulfillment for the network. */ +export class PaymentL3Mandate { + nonce: string; + aud: string; + iat: number; + iss: string | null; + exp: number | null; + sdHash: string; + finalPayment: FinalPaymentMandate | null; + finalMerchant: JsonObject | null; + + constructor(init: { + nonce: string; + aud: string; + iat: number; + iss?: string | null; + exp?: number | null; + sdHash?: string; + finalPayment?: FinalPaymentMandate | null; + finalMerchant?: JsonObject | null; + }) { + this.nonce = init.nonce; + this.aud = init.aud; + this.iat = init.iat; + this.iss = init.iss ?? null; + this.exp = init.exp ?? null; + this.sdHash = init.sdHash ?? ''; + this.finalPayment = init.finalPayment ?? null; + this.finalMerchant = init.finalMerchant ?? null; + } +} + +/** L3b KB-SD-JWT: the agent's checkout fulfillment for the merchant. */ +export class CheckoutL3Mandate { + nonce: string; + aud: string; + iat: number; + iss: string | null; + exp: number | null; + sdHash: string; + finalCheckout: FinalCheckoutMandate | null; + + constructor(init: { + nonce: string; + aud: string; + iat: number; + iss?: string | null; + exp?: number | null; + sdHash?: string; + finalCheckout?: FinalCheckoutMandate | null; + }) { + this.nonce = init.nonce; + this.aud = init.aud; + this.iat = init.iat; + this.iss = init.iss ?? null; + this.exp = init.exp ?? null; + this.sdHash = init.sdHash ?? ''; + this.finalCheckout = init.finalCheckout ?? null; + } +} diff --git a/typescript/src/models/constraints.ts b/typescript/src/models/constraints.ts new file mode 100644 index 0000000..6e61c8b --- /dev/null +++ b/typescript/src/models/constraints.ts @@ -0,0 +1,309 @@ +/** Constraint type definitions for autonomous-mode mandates. + * + * `toJSON()` preserves the exact key insertion order of the Python reference — + * this is load-bearing for `sd_hash` and disclosure hashing. + * + * Parsing mirrors the Python reference exactly: `parseConstraint` stores each + * known field's RAW value (no type coercion), just as Python's dataclass keeps + * whatever `parse_constraint` puts in its kwargs. A present-but-wrongly-typed + * field (e.g. a string `min`, a numeric `match_mode`) is preserved verbatim so + * the runtime guards in the constraint checker reject it — rather than being + * silently coerced to a default, which would fail open. The declared field + * types describe the *intended* shape; at runtime a field may hold a raw value, + * which the checker validates with `isInt`/`typeof` guards (the same way Python + * dataclass annotations are not enforced at runtime). + */ + +export type JsonObject = Record; + +/** + * Compatibility alias for {@link JsonObject}. Downstream consumers (the AP2 + * TypeScript samples) import `Dict`; keep this exported until they migrate. + */ +export type Dict = JsonObject; + +/** + * RFC 7800 §3.1 confirmation object. Deliberately OPEN (index signature): + * not-understood confirmation members MUST be ignored, so a closed/exact type + * would reject inputs the RFC requires accepting. Layer rules (e.g. L1/L2 + * MUST contain `cnf.jwk`) are enforced at runtime by the verifier, not here. + */ +export interface CnfClaim { + jwk?: JsonObject; + kid?: string; + jku?: string; + jwe?: string; + [member: string]: unknown; +} + +// Apply a default only when the field is genuinely absent (`undefined`). A +// present `null` (or any other raw value) is preserved, matching Python's +// `kwargs[k] = v` for present keys vs. the dataclass default for absent keys. +const orDefault = (v: unknown, dflt: T): T => (v !== undefined ? (v as T) : dflt); + +/** Base constraint type. Unknown constraint types are preserved as-is. */ +export class Constraint { + type: string; + extraFields: JsonObject; + + constructor(type = '', extraFields: JsonObject = {}) { + this.type = type; + this.extraFields = extraFields; + } + + toJSON(): JsonObject { + return { type: this.type, ...this.extraFields }; + } +} + +export class AllowedMerchantConstraint extends Constraint { + allowed: unknown[]; + + constructor(opts: { allowed?: unknown; extraFields?: JsonObject } = {}) { + super('mandate.checkout.allowed_merchants', opts.extraFields ?? {}); + this.allowed = orDefault(opts.allowed, [] as unknown[]); + } + + override toJSON(): JsonObject { + return { type: this.type, allowed: this.allowed, ...this.extraFields }; + } +} + +export class CheckoutLineItemsConstraint extends Constraint { + items: unknown[]; + matchMode: string; + + constructor(opts: { items?: unknown; matchMode?: unknown; extraFields?: JsonObject } = {}) { + super('mandate.checkout.line_items', opts.extraFields ?? {}); + this.items = orDefault(opts.items, [] as unknown[]); + this.matchMode = orDefault(opts.matchMode, 'minimum'); + } + + override toJSON(): JsonObject { + return { type: this.type, items: this.items, match_mode: this.matchMode, ...this.extraFields }; + } +} + +export class AllowedPayeeConstraint extends Constraint { + allowed: unknown[]; + + constructor(opts: { allowed?: unknown; extraFields?: JsonObject } = {}) { + super('mandate.payment.allowed_payees', opts.extraFields ?? {}); + this.allowed = orDefault(opts.allowed, [] as unknown[]); + } + + override toJSON(): JsonObject { + return { type: this.type, allowed: this.allowed, ...this.extraFields }; + } +} + +export class PaymentAmountConstraint extends Constraint { + currency: string; + min: number | null; + max: number | null; + + constructor(opts: { currency?: unknown; min?: unknown; max?: unknown; extraFields?: JsonObject } = {}) { + super('mandate.payment.amount_range', opts.extraFields ?? {}); + this.currency = orDefault(opts.currency, 'USD'); + this.min = orDefault(opts.min, null); + this.max = orDefault(opts.max, null); + } + + override toJSON(): JsonObject { + const d: JsonObject = { type: this.type, currency: this.currency }; + if (this.min !== null) d.min = this.min; + if (this.max !== null) d.max = this.max; + return { ...d, ...this.extraFields }; + } +} + +export class ReferenceConstraint extends Constraint { + conditionalTransactionId: string; + + constructor(opts: { conditionalTransactionId?: unknown; extraFields?: JsonObject } = {}) { + super('mandate.payment.reference', opts.extraFields ?? {}); + this.conditionalTransactionId = orDefault(opts.conditionalTransactionId, ''); + } + + override toJSON(): JsonObject { + return { type: this.type, conditional_transaction_id: this.conditionalTransactionId, ...this.extraFields }; + } +} + +export class PaymentBudgetConstraint extends Constraint { + currency: string; + max: number; + min: number | null; + + constructor(opts: { currency?: unknown; max?: unknown; min?: unknown; extraFields?: JsonObject } = {}) { + super('mandate.payment.budget', opts.extraFields ?? {}); + this.currency = orDefault(opts.currency, 'USD'); + this.max = orDefault(opts.max, 0); + this.min = orDefault(opts.min, null); + // Python raises in __post_init__ when max is not a positive integer; a + // non-numeric raw max would raise a TypeError on comparison there, so we + // reject it here too (parity: malformed budget is rejected, not coerced). + if (typeof this.max !== 'number' || this.max <= 0) { + throw new Error('PaymentBudgetConstraint.max must be a positive integer'); + } + if (this.min !== null && (typeof this.min !== 'number' || this.min <= 0)) { + throw new Error('PaymentBudgetConstraint.min must be a positive integer'); + } + } + + override toJSON(): JsonObject { + const d: JsonObject = { type: this.type, currency: this.currency, max: this.max }; + if (this.min !== null) d.min = this.min; + return { ...d, ...this.extraFields }; + } +} + +export class PaymentRecurrenceConstraint extends Constraint { + frequency: string; + startDate: string; + endDate: string | null; + number: number | null; + + constructor( + opts: { frequency?: unknown; startDate?: unknown; endDate?: unknown; number?: unknown; extraFields?: JsonObject } = {}, + ) { + super('mandate.payment.recurrence', opts.extraFields ?? {}); + this.frequency = orDefault(opts.frequency, ''); + this.startDate = orDefault(opts.startDate, ''); + this.endDate = orDefault(opts.endDate, null); + this.number = orDefault(opts.number, null); + } + + override toJSON(): JsonObject { + const d: JsonObject = { type: this.type, frequency: this.frequency, start_date: this.startDate }; + if (this.endDate !== null) d.end_date = this.endDate; + if (this.number !== null) d.number = this.number; + return { ...d, ...this.extraFields }; + } +} + +export class AgentRecurrenceConstraint extends Constraint { + frequency: string; + startDate: string; + endDate: string; + maxOccurrences: number | null; + + constructor( + opts: { frequency?: unknown; startDate?: unknown; endDate?: unknown; maxOccurrences?: unknown; extraFields?: JsonObject } = {}, + ) { + super('mandate.payment.agent_recurrence', opts.extraFields ?? {}); + this.frequency = orDefault(opts.frequency, ''); + this.startDate = orDefault(opts.startDate, ''); + this.endDate = orDefault(opts.endDate, ''); + this.maxOccurrences = orDefault(opts.maxOccurrences, null); + } + + override toJSON(): JsonObject { + const d: JsonObject = { type: this.type, frequency: this.frequency, start_date: this.startDate, end_date: this.endDate }; + if (this.maxOccurrences !== null) d.max_occurrences = this.maxOccurrences; + return { ...d, ...this.extraFields }; + } +} + +/** + * The 8 constraint types registered in spec §6.2 (v0.1-draft). The registry + * may grow (an IETF draft signals an `environment.*` family); unknown types + * still parse to the base `Constraint` and are rejected by the checker's + * fail-closed path, so this union constrains the REGISTRY keys — not inputs. + */ +export type KnownConstraintType = + | 'mandate.checkout.allowed_merchants' + | 'mandate.checkout.line_items' + | 'mandate.payment.allowed_payees' + | 'mandate.payment.amount_range' + | 'mandate.payment.reference' + | 'mandate.payment.budget' + | 'mandate.payment.recurrence' + | 'mandate.payment.agent_recurrence'; + +/** Union of the concrete constraint classes for the registered types. */ +export type KnownConstraint = + | AllowedMerchantConstraint + | CheckoutLineItemsConstraint + | AllowedPayeeConstraint + | PaymentAmountConstraint + | ReferenceConstraint + | PaymentBudgetConstraint + | PaymentRecurrenceConstraint + | AgentRecurrenceConstraint; + +function extraFieldsOf(obj: JsonObject, known: string[]): JsonObject { + const extra: JsonObject = {}; + for (const [k, v] of Object.entries(obj)) { + if (k === 'type' || known.includes(k)) continue; + extra[k] = v; + } + return extra; +} + +// Each factory passes the RAW field value straight through (no coercion). An +// absent key reads as `undefined`, which the constructor maps to the field's +// default; a present-but-wrong-type value is preserved so the checker rejects +// it — exactly matching Python's `parse_constraint`. +const REGISTRY: Record KnownConstraint> = { + 'mandate.checkout.allowed_merchants': (o) => + new AllowedMerchantConstraint({ allowed: o.allowed, extraFields: extraFieldsOf(o, ['allowed']) }), + 'mandate.checkout.line_items': (o) => + new CheckoutLineItemsConstraint({ + items: o.items, + matchMode: o.match_mode, + extraFields: extraFieldsOf(o, ['items', 'match_mode']), + }), + 'mandate.payment.allowed_payees': (o) => + new AllowedPayeeConstraint({ allowed: o.allowed, extraFields: extraFieldsOf(o, ['allowed']) }), + 'mandate.payment.amount_range': (o) => + new PaymentAmountConstraint({ + currency: o.currency, + min: o.min, + max: o.max, + extraFields: extraFieldsOf(o, ['currency', 'min', 'max']), + }), + 'mandate.payment.reference': (o) => + new ReferenceConstraint({ + conditionalTransactionId: o.conditional_transaction_id, + extraFields: extraFieldsOf(o, ['conditional_transaction_id']), + }), + 'mandate.payment.budget': (o) => + new PaymentBudgetConstraint({ + currency: o.currency, + max: o.max, + min: o.min, + extraFields: extraFieldsOf(o, ['currency', 'max', 'min']), + }), + 'mandate.payment.recurrence': (o) => + new PaymentRecurrenceConstraint({ + frequency: o.frequency, + startDate: o.start_date, + endDate: o.end_date, + number: o.number, + extraFields: extraFieldsOf(o, ['frequency', 'start_date', 'end_date', 'number']), + }), + 'mandate.payment.agent_recurrence': (o) => + new AgentRecurrenceConstraint({ + frequency: o.frequency, + startDate: o.start_date, + endDate: o.end_date, + maxOccurrences: o.max_occurrences, + extraFields: extraFieldsOf(o, ['frequency', 'start_date', 'end_date', 'max_occurrences']), + }), +}; + +/** Parse a constraint object into the appropriate typed constraint. */ +export function parseConstraint(data: unknown): Constraint { + if (data === null || typeof data !== 'object' || Array.isArray(data)) { + return new Constraint('unknown'); + } + const obj = data as JsonObject; + const ctype = typeof obj.type === 'string' ? obj.type : ''; + // Widen for the lookup: `ctype` is attacker-controlled and may be any string. + const factory = (REGISTRY as Partial KnownConstraint>>)[ctype]; + if (!factory) { + return new Constraint(ctype, extraFieldsOf(obj, [])); + } + return factory(obj); +} diff --git a/typescript/src/models/index.ts b/typescript/src/models/index.ts new file mode 100644 index 0000000..38dc979 --- /dev/null +++ b/typescript/src/models/index.ts @@ -0,0 +1,4 @@ +export * from './constraints.js'; +export * from './issuer-credential.js'; +export * from './user-mandate.js'; +export * from './agent-mandate.js'; diff --git a/typescript/src/models/issuer-credential.ts b/typescript/src/models/issuer-credential.ts new file mode 100644 index 0000000..6574178 --- /dev/null +++ b/typescript/src/models/issuer-credential.ts @@ -0,0 +1,63 @@ +/** Layer 1: Issuer SD-JWT credential model. */ + +import type { CnfClaim, JsonObject } from './constraints.js'; + +export interface IssuerCredentialInit { + iss: string; + sub: string; + iat: number; + exp: number; + vct?: string; + aud?: string | null; + cnfJwk?: JsonObject; + panLastFour?: string; + scheme?: string; + cardId?: string | null; + email?: string | null; +} + +/** Layer 1 Issuer SD-JWT: binds user identity to a public key. */ +export class IssuerCredential { + iss: string; + sub: string; + iat: number; + exp: number; + vct: string; + aud: string | null; + cnfJwk: JsonObject; + panLastFour: string; + scheme: string; + cardId: string | null; + email: string | null; + + constructor(init: IssuerCredentialInit) { + this.iss = init.iss; + this.sub = init.sub; + this.iat = init.iat; + this.exp = init.exp; + this.vct = init.vct ?? 'https://credentials.mastercard.com/card'; + this.aud = init.aud ?? null; + this.cnfJwk = init.cnfJwk ?? {}; + this.panLastFour = init.panLastFour ?? ''; + this.scheme = init.scheme ?? ''; + this.cardId = init.cardId ?? null; + this.email = init.email ?? null; + } + + /** Non-SD claims for the JWT payload; `_sd`/`_sd_alg` are added during issuance. */ + toJSON(): JsonObject { + const d: JsonObject = { + iss: this.iss, + sub: this.sub, + iat: this.iat, + exp: this.exp, + vct: this.vct, + cnf: { jwk: this.cnfJwk } satisfies CnfClaim, + }; + if (this.aud) d.aud = this.aud; + d.pan_last_four = this.panLastFour; + d.scheme = this.scheme; + if (this.cardId !== null) d.card_id = this.cardId; + return d; + } +} diff --git a/typescript/src/models/user-mandate.ts b/typescript/src/models/user-mandate.ts new file mode 100644 index 0000000..14c3012 --- /dev/null +++ b/typescript/src/models/user-mandate.ts @@ -0,0 +1,176 @@ +/** Layer 2: User KB-SD-JWT mandate models. */ + +import type { CnfClaim, Constraint, JsonObject } from './constraints.js'; + +export const MandateMode = { + IMMEDIATE: 'IMMEDIATE', + AUTONOMOUS: 'AUTONOMOUS', +} as const; +export type MandateMode = (typeof MandateMode)[keyof typeof MandateMode]; + +// Match Python's truthiness of `cnf_jwk` (`if self.cnf_jwk:`): an empty object — +// like an empty object in Python — counts as "no cnf" for emission and for the +// CheckoutMandate both-modes guard. (PaymentMandate.__post_init__ deliberately +// uses `is not None`, so its constructor guard keeps `!== null` below.) +const hasCnf = (jwk: JsonObject | null): boolean => jwk !== null && Object.keys(jwk).length > 0; + +/** + * Checkout mandate — open (L2 autonomous) or final (L2 immediate). + * - Autonomous: `cnfJwk` + `constraints`, vct = `mandate.checkout.open.1` + * - Immediate: `checkoutJwt` + `checkoutHash`, vct = `mandate.checkout.1` + */ +export class CheckoutMandate { + vct: string; + cnfJwk: JsonObject | null; + cnfKid: string | null; + constraints: Constraint[]; + checkoutJwt: string | null; + checkoutHash: string | null; + + constructor( + init: { + vct?: string; + cnfJwk?: JsonObject | null; + cnfKid?: string | null; + constraints?: Constraint[]; + checkoutJwt?: string | null; + checkoutHash?: string | null; + } = {}, + ) { + this.vct = init.vct ?? 'mandate.checkout.open.1'; + this.cnfJwk = init.cnfJwk ?? null; + this.cnfKid = init.cnfKid ?? null; + this.constraints = init.constraints ?? []; + this.checkoutJwt = init.checkoutJwt ?? null; + this.checkoutHash = init.checkoutHash ?? null; + if (hasCnf(this.cnfJwk) && this.checkoutJwt !== null) { + throw new Error('CheckoutMandate cannot have both cnf_jwk (autonomous) and checkout_jwt (immediate)'); + } + } + + toJSON(): JsonObject { + const d: JsonObject = { vct: this.vct }; + if (hasCnf(this.cnfJwk)) { + const jwk: JsonObject = { ...this.cnfJwk }; + if (this.cnfKid) jwk.kid = this.cnfKid; + d.cnf = { jwk } satisfies CnfClaim; + } + if (this.constraints.length) { + d.constraints = this.constraints.map((c) => c.toJSON()); + } + if (this.checkoutJwt !== null) d.checkout_jwt = this.checkoutJwt; + if (this.checkoutHash !== null) d.checkout_hash = this.checkoutHash; + return d; + } +} + +/** + * Payment mandate — open (L2 autonomous) or final (L2 immediate). + * - Autonomous: `cnfJwk` + `constraints` + `paymentInstrument` + `riskData` + * - Immediate: `currency` + `amount` + `payee` + `paymentInstrument` + `transactionId` + */ +export class PaymentMandate { + vct: string; + cnfJwk: JsonObject | null; + cnfKid: string | null; + constraints: Constraint[]; + paymentInstrument: JsonObject | null; + riskData: JsonObject | null; + payee: JsonObject | null; + currency: string | null; + amount: number | null; + transactionId: string | null; + + constructor( + init: { + vct?: string; + cnfJwk?: JsonObject | null; + cnfKid?: string | null; + constraints?: Constraint[]; + paymentInstrument?: JsonObject | null; + riskData?: JsonObject | null; + payee?: JsonObject | null; + currency?: string | null; + amount?: number | null; + transactionId?: string | null; + } = {}, + ) { + this.vct = init.vct ?? 'mandate.payment.open.1'; + this.cnfJwk = init.cnfJwk ?? null; + this.cnfKid = init.cnfKid ?? null; + this.constraints = init.constraints ?? []; + this.paymentInstrument = init.paymentInstrument ?? null; + this.riskData = init.riskData ?? null; + this.payee = init.payee ?? null; + this.currency = init.currency ?? null; + this.amount = init.amount ?? null; + this.transactionId = init.transactionId ?? null; + if (this.amount !== null && this.cnfJwk !== null) { + throw new Error('PaymentMandate cannot have both cnf_jwk (autonomous) and amount (immediate)'); + } + } + + toJSON(): JsonObject { + const d: JsonObject = { vct: this.vct }; + if (hasCnf(this.cnfJwk)) { + const jwk: JsonObject = { ...this.cnfJwk }; + if (this.cnfKid) jwk.kid = this.cnfKid; + d.cnf = { jwk } satisfies CnfClaim; + } + if (this.constraints.length) { + d.constraints = this.constraints.map((c) => c.toJSON()); + } + if (this.paymentInstrument !== null) d.payment_instrument = this.paymentInstrument; + if (this.riskData !== null) d.risk_data = this.riskData; + if (this.payee !== null) d.payee = this.payee; + if (this.currency !== null && this.amount !== null) { + d.payment_amount = { currency: this.currency, amount: this.amount }; + } + if (this.transactionId !== null) d.transaction_id = this.transactionId; + return d; + } +} + +/** Layer 2 KB-SD-JWT: the user's consent with mandates. */ +export class UserMandate { + nonce: string; + aud: string; + iat: number; + mode: MandateMode; + iss: string | null; + exp: number | null; + sdHash: string; + promptSummary: string | null; + checkoutMandate: CheckoutMandate | null; + paymentMandate: PaymentMandate | null; + merchants: JsonObject[]; + acceptableItems: JsonObject[]; + + constructor(init: { + nonce: string; + aud: string; + iat: number; + mode: MandateMode; + iss?: string | null; + exp?: number | null; + sdHash?: string; + promptSummary?: string | null; + checkoutMandate?: CheckoutMandate | null; + paymentMandate?: PaymentMandate | null; + merchants?: JsonObject[]; + acceptableItems?: JsonObject[]; + }) { + this.nonce = init.nonce; + this.aud = init.aud; + this.iat = init.iat; + this.mode = init.mode; + this.iss = init.iss ?? null; + this.exp = init.exp ?? null; + this.sdHash = init.sdHash ?? ''; + this.promptSummary = init.promptSummary ?? null; + this.checkoutMandate = init.checkoutMandate ?? null; + this.paymentMandate = init.paymentMandate ?? null; + this.merchants = init.merchants ?? []; + this.acceptableItems = init.acceptableItems ?? []; + } +} diff --git a/typescript/src/verification/chain.ts b/typescript/src/verification/chain.ts new file mode 100644 index 0000000..ffdde80 --- /dev/null +++ b/typescript/src/verification/chain.ts @@ -0,0 +1,1120 @@ +/** + * Full chain verification (L1 → L2 → L3a/L3b). + * + * This file — including its 500+-line `verifyChain` and its numbered step + * sequence — deliberately mirrors + * python/src/verifiable_intent/verification/chain.py line-for-line for + * auditability. Both the control flow AND the `result.errors` strings must stay + * byte-for-byte identical to the Python reference; several of those error + * strings are pinned by the shared golden vectors in test-vectors/vectors.json. + * Refactor only alongside a matching change on the Python side. + */ + +import { hashAscii, hashDisclosure } from '../crypto/disclosure.js'; +import { type SdJwt, resolveDisclosures, verifySdJwtSignature } from '../crypto/sd-jwt.js'; +import type { Es256Jwk } from '../crypto/signing.js'; +import type { JsonObject, KnownConstraintType } from '../models/constraints.js'; +import { asArray, isJsonObject } from '../internal/guards.js'; +import { verifyCheckoutHashBinding, verifyL2ReferenceBinding, verifyL3CrossReference } from './integrity.js'; + +const ALLOWED_ALGS = new Set(['ES256']); + +/** + * Constraint types the spec assigns to the PAYMENT NETWORK to enforce: they are + * stateful (spend-so-far, occurrence counts), so this stateless verifier parses + * but never evaluates them (see constraint-checker.ts, "network-enforced + * constraints"). They are surfaced on `ChainVerificationResult.networkEnforced` + * so the caller/network knows what it still must enforce. + */ +const NETWORK_ENFORCED_TYPES: ReadonlySet = new Set([ + 'mandate.payment.budget', + 'mandate.payment.recurrence', + 'mandate.payment.agent_recurrence', +]); + +const L1_VCT = 'https://credentials.mastercard.com/card'; +const L2_CHECKOUT_VCT_OPEN = 'mandate.checkout.open.1'; +const L2_PAYMENT_VCT_OPEN = 'mandate.payment.open.1'; +const L2_CHECKOUT_VCT_FINAL = 'mandate.checkout.1'; +const L2_PAYMENT_VCT_FINAL = 'mandate.payment.1'; +const L3_PAYMENT_VCT = 'mandate.payment.1'; +const L3_CHECKOUT_VCT = 'mandate.checkout.1'; + +const CHECKOUT_VCTS = new Set([L2_CHECKOUT_VCT_OPEN, L2_CHECKOUT_VCT_FINAL]); +const PAYMENT_VCTS = new Set([L2_PAYMENT_VCT_OPEN, L2_PAYMENT_VCT_FINAL]); +const ALL_KNOWN_VCTS = new Set([ + L2_CHECKOUT_VCT_OPEN, + L2_CHECKOUT_VCT_FINAL, + L2_PAYMENT_VCT_OPEN, + L2_PAYMENT_VCT_FINAL, + L3_PAYMENT_VCT, + L3_CHECKOUT_VCT, +]); + +const isFiniteNumber = (v: unknown): v is number => typeof v === 'number' && Number.isFinite(v); +const truncate = (s: string): string => (s.length > 64 ? s.slice(0, 64) : s); + +/** RFC 9901 §7.1: a digest MUST NOT appear more than once in `_sd`. Returns the first duplicate, or null. */ +function duplicateSdDigest(payload: Record): string | null { + const sd = payload._sd; + if (!Array.isArray(sd)) return null; + const seen = new Set(); + for (const h of sd) { + if (typeof h !== 'string') continue; + if (seen.has(h)) return h; + seen.add(h); + } + return null; +} + +/** True if `exp` is expired; null if absent. */ +function isExpired(exp: unknown, now: number, skew: number): boolean | null { + if (exp === undefined || exp === null) return null; + if (typeof exp === 'boolean') return true; + if (typeof exp !== 'number') return true; + if (!Number.isFinite(exp)) return true; + return now > exp + skew; +} + +/** True if `iat` is in the future; null if absent. */ +function isFutureDated(iat: unknown, now: number, skew: number): boolean | null { + if (iat === undefined || iat === null) return null; + if (typeof iat === 'boolean') return true; + if (typeof iat !== 'number') return true; + if (!Number.isFinite(iat)) return true; + return iat > now + skew; +} + +function validateHeader(header: unknown, layer: string, expectedTyp: string): string | null { + if (!isJsonObject(header)) return `${layer} header must be a JSON object, got ${header === null ? 'null' : typeof header}`; + const alg = header.alg; + if (typeof alg !== 'string' || !ALLOWED_ALGS.has(alg)) { + return `${layer} header alg must be one of {ES256}, got ${typeof alg} '${truncate(String(alg))}'`; + } + const typ = header.typ; + if (typeof typ !== 'string' || typ !== expectedTyp) { + return `${layer} header typ must be '${expectedTyp}', got ${typeof typ} '${truncate(String(typ))}'`; + } + return null; +} + +function validateEcPublicJwk(jwk: unknown): string | null { + if (!isJsonObject(jwk)) return 'cnf.jwk must be an object'; + if (typeof jwk.x !== 'string' || !jwk.x || typeof jwk.y !== 'string' || !jwk.y) { + return 'missing x/y coordinate'; + } + return null; +} + +export interface SplitL3 { + l3Payment?: SdJwt | null; + l3Checkout?: SdJwt | null; + l2PaymentSerialized?: string | null; + l2CheckoutSerialized?: string | null; +} + +export class MandatePairResult { + pairIndex = 0; + pairingKey = ''; + checkoutMandate: JsonObject = {}; + paymentMandate: JsonObject = {}; + l3PaymentClaims: JsonObject = {}; + l3CheckoutClaims: JsonObject = {}; + checksPerformed: string[] = []; + checksSkipped: string[] = []; + errors: string[] = []; +} + +export class ChainVerificationResult { + valid = false; + errors: string[] = []; + l1Claims: JsonObject = {}; + l2Claims: JsonObject = {}; + l3PaymentClaims: JsonObject = {}; + l3CheckoutClaims: JsonObject = {}; + l2CheckoutDisclosed = false; + l2PaymentDisclosed = false; + checksPerformed: string[] = []; + checksSkipped: string[] = []; + pairResults: MandatePairResult[] = []; + mandatePairCount = 0; + networkEnforced: NetworkEnforcedConstraint[] = []; +} + +/** + * A network-enforced constraint found in a mandate pair's payment mandate. + * Purely informational: the stateless verifier did NOT evaluate it — the spec + * assigns enforcement of budget/recurrence to the payment network. + */ +export interface NetworkEnforcedConstraint { + /** Index of the mandate pair (into `ChainVerificationResult.pairResults`). */ + pairIndex: number; + type: KnownConstraintType; + /** The raw constraint object as resolved from the payment mandate. */ + constraint: JsonObject; +} + +interface MandateInfo { + resolved: JsonObject; + refHash: string | null; + discB64: string | null; +} + +type MandatePair = [checkout: MandateInfo | null, payment: MandateInfo | null]; + +/** Result of grouping L2 delegate disclosures into checkout/payment mandate pairs. */ +interface MandatePairsResult { + pairs: MandatePair[]; + errors: string[]; +} + +/** Result of validating one mandate pair: blocking errors plus the checks run/skipped. */ +interface MandatePairCheck { + errors: string[]; + checksPerformed: string[]; + checksSkipped: string[]; +} + +/** The agent delegation key resolved from the L2 open mandates, or an error if inconsistent. */ +interface AgentKeyResult { + jwk: JsonObject | null; + kid: string | null; + error: string | null; +} + +export interface VerifyChainOptions { + l3Payment?: SdJwt | null; + l3Checkout?: SdJwt | null; + issuerPublicJwk?: Es256Jwk | null; + skipIssuerVerification?: boolean; + clockSkewSeconds?: number; + l1Serialized?: string | null; + l2Serialized?: string | null; + l2PaymentSerialized?: string | null; + l2CheckoutSerialized?: string | null; + splitL3s?: SplitL3[] | null; + expectedL2Aud?: string | null; + expectedL2Nonce?: string | null; + expectedL3PaymentAud?: string | null; + expectedL3PaymentNonce?: string | null; + expectedL3CheckoutAud?: string | null; + expectedL3CheckoutNonce?: string | null; + expectedL1Vct?: string; + /** Unix seconds; defaults to the wall clock. Inject for deterministic verification. */ + currentTime?: number; +} + +async function tryVerify(sj: SdJwt, jwk: Es256Jwk): Promise { + try { + return await verifySdJwtSignature(sj, jwk); + } catch { + return false; + } +} + +/** + * Verify the full VI delegation chain (split L3). Returns a result whose `valid` + * is `false` with populated `errors` on any failure; never rejects on a malformed + * or hostile credential (parsing with `decodeSdJwt` may throw — verification does not). + */ +export async function verifyChain(l1: SdJwt, l2: SdJwt, opts: VerifyChainOptions = {}): Promise { + const result = new ChainVerificationResult(); + const now = opts.currentTime ?? Math.floor(Date.now() / 1000); + const skew = opts.clockSkewSeconds ?? 300; + const expectedL1Vct = opts.expectedL1Vct ?? L1_VCT; + const l3Payment = opts.l3Payment ?? null; + const l3Checkout = opts.l3Checkout ?? null; + const splitL3s = opts.splitL3s ?? null; + + // 0. Mutual exclusion: split_l3s vs individual l3 params + if (splitL3s !== null && (l3Payment !== null || l3Checkout !== null)) { + // Every string pushed to result.errors in this function is parity-pinned to + // Python (and some are pinned by test-vectors/vectors.json); never reword one + // without changing chain.py and regenerating the vectors. + result.errors.push('Cannot provide both split_l3s and individual l3_payment/l3_checkout parameters'); + return result; + } + + let hasL3Args = l3Payment !== null || l3Checkout !== null; + if (splitL3s !== null) { + hasL3Args = hasL3Args || splitL3s.some((p) => (p.l3Payment ?? null) !== null || (p.l3Checkout ?? null) !== null); + } + + // 0. Validate payload types are objects (fail-closed on malformed JWTs) + if (!isJsonObject(l1.payload)) { + result.errors.push(`L1 payload must be a JSON object, got ${typeof l1.payload}`); + return result; + } + if (!isJsonObject(l2.payload)) { + result.errors.push(`L2 payload must be a JSON object, got ${typeof l2.payload}`); + return result; + } + + // 1. Verify L1 signature (fail-closed: require key unless explicitly skipped) + if (opts.issuerPublicJwk) { + if (!(await tryVerify(l1, opts.issuerPublicJwk))) { + result.errors.push('L1 signature verification failed'); + return result; + } + } else if (!opts.skipIssuerVerification) { + result.errors.push( + 'issuer_public_key is required for chain verification (pass skip_issuer_verification=True to bypass in tests)', + ); + return result; + } + + // 1a0. Validate L1 header + const l1HeaderErr = validateHeader(l1.header, 'L1', 'sd+jwt'); + if (l1HeaderErr) { + result.errors.push(l1HeaderErr); + return result; + } + + // 1a. Validate L1 vct + const l1Vct = l1.payload.vct; + if (l1Vct !== expectedL1Vct) { + result.errors.push(`L1 vct must be '${expectedL1Vct}', got '${String(l1Vct)}'`); + return result; + } + + // 1b. Validate L1 _sd_alg + const l1SdAlg = l1.payload._sd_alg; + if (l1SdAlg !== undefined && l1SdAlg !== null && l1SdAlg !== 'sha-256') { + result.errors.push(`L1 _sd_alg must be 'sha-256', got '${String(l1SdAlg)}'`); + return result; + } + + // 1b-bis. RFC 9901 §7.1: reject duplicate disclosure digests in _sd + const l1SdDup = duplicateSdDigest(l1.payload); + if (l1SdDup !== null) { + result.errors.push(`L1 _sd contains a duplicate disclosure digest (RFC 9901 section 7.1): ${l1SdDup}`); + return result; + } + + // 2. L1 expiration + if (isExpired(l1.payload.exp, now, skew)) { + result.errors.push(`L1 credential expired at ${String(l1.payload.exp)}`); + return result; + } + // 2a. L1 iat not in the future + if (isFutureDated(l1.payload.iat, now, skew)) { + result.errors.push(`L1 credential iat is in the future: ${String(l1.payload.iat)}`); + return result; + } + + result.l1Claims = await resolveDisclosures(l1); + + // 3. Extract user's public key from L1 cnf + const l1Cnf = l1.payload.cnf ?? {}; + if (!isJsonObject(l1Cnf)) { + result.errors.push('L1 cnf must be a JSON object'); + return result; + } + const userJwk = l1Cnf.jwk; + if (!userJwk || !isJsonObject(userJwk) || Object.keys(userJwk).length === 0) { + result.errors.push('L1 missing cnf.jwk (user public key)'); + return result; + } + const userJwkErr = validateEcPublicJwk(userJwk); + if (userJwkErr) { + result.errors.push(`L1 cnf.jwk is malformed: ${userJwkErr}`); + return result; + } + + // 4. Verify L2 signature with user's key + if (!(await tryVerify(l2, userJwk as Es256Jwk))) { + result.errors.push('L2 signature verification failed (user key mismatch)'); + return result; + } + + // 4a. Verify L2 sd_hash binds to the presented L1 + const l1Ser = opts.l1Serialized ?? l1.serialize(); + const actualHash = l2.payload.sd_hash ?? ''; + if (!actualHash) { + result.errors.push('L2 missing required sd_hash binding to L1'); + return result; + } + if (actualHash !== (await hashAscii(l1Ser))) { + result.errors.push('L2 sd_hash does not match L1 serialized form'); + return result; + } + + // 4a2. L2 _sd_alg + const l2SdAlg = l2.payload._sd_alg; + if (l2SdAlg !== undefined && l2SdAlg !== null && l2SdAlg !== 'sha-256') { + result.errors.push(`L2 _sd_alg must be 'sha-256', got '${String(l2SdAlg)}'`); + return result; + } + + // 4a2-bis. RFC 9901 §7.1: reject duplicate disclosure digests in _sd + const l2SdDup = duplicateSdDigest(l2.payload); + if (l2SdDup !== null) { + result.errors.push(`L2 _sd contains a duplicate disclosure digest (RFC 9901 section 7.1): ${l2SdDup}`); + return result; + } + + // 4a3. L2 iat not in the future + if (isFutureDated(l2.payload.iat, now, skew)) { + result.errors.push(`L2 iat is in the future: ${String(l2.payload.iat)}`); + return result; + } + // 4a4. L2 exp + if (isExpired(l2.payload.exp, now, skew)) { + result.errors.push(`L2 expired at ${String(l2.payload.exp)}`); + return result; + } + + // 4a5. L2 aud and nonce + const l2Aud = l2.payload.aud; + const l2Nonce = l2.payload.nonce; + if (opts.expectedL2Aud !== undefined && opts.expectedL2Aud !== null) { + if (l2Aud !== opts.expectedL2Aud) { + result.errors.push(`L2 aud mismatch: expected '${opts.expectedL2Aud}', got '${String(l2Aud)}'`); + return result; + } + result.checksPerformed.push('l2_aud'); + } else if (typeof l2Aud === 'string' && l2Aud) { + result.checksSkipped.push('l2_aud (no expected value provided)'); + } + if (opts.expectedL2Nonce !== undefined && opts.expectedL2Nonce !== null) { + if (l2Nonce !== opts.expectedL2Nonce) { + result.errors.push(`L2 nonce mismatch: expected '${opts.expectedL2Nonce}', got '${String(l2Nonce)}'`); + return result; + } + result.checksPerformed.push('l2_nonce'); + } else if (typeof l2Nonce === 'string' && l2Nonce) { + result.checksSkipped.push('l2_nonce (no expected value provided)'); + } + + result.l2Claims = await resolveDisclosures(l2); + + // 4a-mode. Infer execution mode from L2 mandate VCTs. + const resolvedDelegatesForMode = result.l2Claims.delegate_payload; + let hasOpenMandate = false; + let hasFinalMandate = false; + for (const item of asArray(resolvedDelegatesForMode)) { + if (isJsonObject(item)) { + const vct = item.vct ?? ''; + if (vct === L2_CHECKOUT_VCT_OPEN || vct === L2_PAYMENT_VCT_OPEN) hasOpenMandate = true; + else if (vct === L2_CHECKOUT_VCT_FINAL || vct === L2_PAYMENT_VCT_FINAL) hasFinalMandate = true; + } + } + if (hasOpenMandate && hasFinalMandate) { + result.errors.push( + 'L2 contains both open (autonomous) and final (immediate) mandate VCTs — open mandates are not allowed in immediate mode', + ); + return result; + } + const isAutonomous = hasOpenMandate; + + // 4a0. Validate L2 header typ now that we know the mode + const expectedL2Typ = isAutonomous ? 'kb-sd-jwt+kb' : 'kb-sd-jwt'; + const l2HeaderErr = validateHeader(l2.header, 'L2', expectedL2Typ); + if (l2HeaderErr) { + result.errors.push(l2HeaderErr); + return result; + } + + // 4a-cross. Immediate L2 + L3 args is a caller error + if (!isAutonomous && hasL3Args) { + result.errors.push('L3 credentials provided but L2 contains only immediate-mode (final) mandates'); + return result; + } + + // 4b. Extract and pair mandate disclosures + const discStrByHash = new Map(); + for (const ds of l2.disclosures) discStrByHash.set(await hashDisclosure(ds), ds); + + const rawDelegates = l2.payload.delegate_payload; + if (!Array.isArray(rawDelegates)) { + result.errors.push(`L2 delegate_payload must be a list, got ${rawDelegates === null ? 'null' : typeof rawDelegates}`); + return result; + } + const resolvedDelegates = asArray(result.l2Claims.delegate_payload); + + for (const resolvedItem of resolvedDelegates) { + if (isJsonObject(resolvedItem)) { + const itemVct = resolvedItem.vct; + if (typeof itemVct === 'string' && itemVct && !ALL_KNOWN_VCTS.has(itemVct)) { + result.checksSkipped.push(`unrecognized_vct_in_delegate_payload: ${itemVct}`); + } + } + } + + const { pairs: mandatePairs, errors: pairErrors } = extractMandatePairs( + rawDelegates, + resolvedDelegates, + discStrByHash, + isAutonomous, + ); + if (pairErrors.length) { + result.errors.push(...pairErrors); + return result; + } + + result.mandatePairCount = mandatePairs.length; + const anyCheckout = mandatePairs.some((p) => p[0] !== null); + const anyPayment = mandatePairs.some((p) => p[1] !== null); + result.l2CheckoutDisclosed = anyCheckout; + result.l2PaymentDisclosed = anyPayment; + + if (mandatePairs.length === 0) { + result.errors.push('L2 delegate_payload resolved zero mandate disclosures'); + return result; + } + + // 4b-bis. Surface network-enforced constraints (additive, informational). + // The stateless verifier parses but never evaluates budget/recurrence — the + // spec assigns them to the payment network — so expose them here to tell the + // caller what it still must enforce. Collected from each pair's payment + // mandate; does not affect any verdict, error, or other result field. + for (const [pairIdx, pair] of mandatePairs.entries()) { + const paymentInfo = pair[1]; + if (!paymentInfo) continue; + for (const c of asArray(paymentInfo.resolved.constraints)) { + if (isJsonObject(c) && NETWORK_ENFORCED_TYPES.has(c.type as KnownConstraintType)) { + result.networkEnforced.push({ pairIndex: pairIdx, type: c.type as KnownConstraintType, constraint: c }); + } + } + } + + // 4c. Per-pair mandate validation + for (const [pairIdx, pair] of mandatePairs.entries()) { + const [checkoutInfo, paymentInfo] = pair; + const checkoutMandate = checkoutInfo ? checkoutInfo.resolved : null; + const paymentMandate = paymentInfo ? paymentInfo.resolved : null; + const checkoutDiscB64 = checkoutInfo ? checkoutInfo.discB64 : null; + let pairingKey = ''; + if (checkoutInfo && checkoutInfo.refHash) pairingKey = checkoutInfo.refHash; + else if (paymentInfo && paymentInfo.refHash) pairingKey = paymentInfo.refHash; + + const pairResult = new MandatePairResult(); + pairResult.pairIndex = pairIdx; + pairResult.pairingKey = pairingKey; + pairResult.checkoutMandate = checkoutMandate ?? {}; + pairResult.paymentMandate = paymentMandate ?? {}; + + const { + errors: mpErrors, + checksPerformed: mpChecks, + checksSkipped: mpSkipped, + } = await verifyMandatePair(checkoutMandate, paymentMandate, checkoutDiscB64, isAutonomous); + pairResult.checksPerformed.push(...mpChecks); + pairResult.checksSkipped.push(...mpSkipped); + result.checksPerformed.push(...mpChecks); + result.checksSkipped.push(...mpSkipped); + + if (mpErrors.length) { + pairResult.errors.push(...mpErrors); + result.errors.push(...mpErrors); + result.pairResults.push(pairResult); + return result; + } + result.pairResults.push(pairResult); + } + + // 4c-bis. Optional card_id cross-check + const l1CardId = l1.payload.card_id; + if (l1CardId) { + for (const pairResult of result.pairResults) { + const pm = pairResult.paymentMandate; + const pi = isJsonObject(pm) ? pm.payment_instrument : {}; + const piId = isJsonObject(pi) ? pi.id : null; + if (piId && piId !== l1CardId) { + result.checksPerformed.push('l1_card_id_cross_check'); + pairResult.checksPerformed.push('l1_card_id_cross_check'); + result.errors.push(`L1 card_id (${String(l1CardId)}) does not match payment_instrument.id (${String(piId)})`); + return result; + } else if (piId) { + result.checksPerformed.push('l1_card_id_cross_check'); + pairResult.checksPerformed.push('l1_card_id_cross_check'); + } else { + result.checksPerformed.push('l1_card_id_cross_check'); + pairResult.checksPerformed.push('l1_card_id_cross_check'); + result.errors.push( + `L1 card_id (${String(l1CardId)}) present but payment_instrument.id is missing — cannot verify binding`, + ); + return result; + } + } + } else { + result.checksSkipped.push('l1_card_id_cross_check'); + } + + // 4d. Autonomous mode: extract agent key and verify L3s + if (isAutonomous) { + if (!anyCheckout && !anyPayment) { + result.errors.push( + 'Autonomous mode requires at least one L2 mandate disclosure to extract the agent delegation key (cnf.jwk)', + ); + return result; + } + + const { jwk: agentJwk, kid: agentKid, error: cnfError } = extractAgentKeyFromAllPairs(mandatePairs); + if (cnfError) { + result.errors.push(cnfError); + return result; + } + if (!agentJwk) { + result.errors.push('L2 mandates missing cnf.jwk for agent delegation'); + return result; + } + const agentJwkErr = validateEcPublicJwk(agentJwk); + if (agentJwkErr) { + result.errors.push(`L2 mandate cnf.jwk is malformed: ${agentJwkErr}`); + return result; + } + + let effectiveSplitL3s: SplitL3[]; + if (splitL3s !== null) { + effectiveSplitL3s = splitL3s; + } else if (l3Payment !== null || l3Checkout !== null) { + effectiveSplitL3s = [ + { + l3Payment, + l3Checkout, + l2PaymentSerialized: opts.l2PaymentSerialized ?? null, + l2CheckoutSerialized: opts.l2CheckoutSerialized ?? null, + }, + ]; + } else { + effectiveSplitL3s = []; + } + + if (effectiveSplitL3s.length && effectiveSplitL3s.length !== mandatePairs.length) { + result.errors.push( + `Split L3 count (${effectiveSplitL3s.length}) does not match mandate pair count (${mandatePairs.length})`, + ); + return result; + } + + for (const [pairIdx, l3p] of effectiveSplitL3s.entries()) { + const pairResult = result.pairResults[pairIdx]; + const mandatePair = mandatePairs[pairIdx]; + // Indices are kept 1:1 (split-L3 count == pair count == pairResults count); + // if that invariant is ever violated, fail closed rather than skip L3 checks. + if (!pairResult || !mandatePair) { + result.errors.push('Internal: split-L3 / mandate-pair index mismatch'); + return result; + } + const [checkoutInfo, paymentInfo] = mandatePair; + const l2Pm = paymentInfo ? paymentInfo.resolved : null; + + const l3Specs = [ + { + l3: l3p.l3Payment ?? null, + label: 'L3a (payment)', + serOverride: (l3p.l2PaymentSerialized ?? null) || (opts.l2PaymentSerialized ?? null), + isPayment: true, + requiredVct: L3_PAYMENT_VCT, + expectedPairDisc: paymentInfo ? paymentInfo.discB64 : null, + }, + { + l3: l3p.l3Checkout ?? null, + label: 'L3b (checkout)', + serOverride: (l3p.l2CheckoutSerialized ?? null) || (opts.l2CheckoutSerialized ?? null), + isPayment: false, + requiredVct: L3_CHECKOUT_VCT, + expectedPairDisc: checkoutInfo ? checkoutInfo.discB64 : null, + }, + ]; + + for (const spec of l3Specs) { + const l3 = spec.l3; + if (l3 === null) continue; + const label = spec.label; + + if (!isJsonObject(l3.payload)) { + result.errors.push(`${label} payload must be a JSON object, got ${typeof l3.payload}`); + return result; + } + if ('cnf' in l3.payload) { + result.errors.push(`${label} payload MUST NOT contain cnf claim`); + return result; + } + if (!(await tryVerify(l3, agentJwk as Es256Jwk))) { + result.errors.push(`${label} signature verification failed (agent key mismatch)`); + return result; + } + const l3HeaderErr = validateHeader(l3.header, label, 'kb-sd-jwt'); + if (l3HeaderErr) { + result.errors.push(l3HeaderErr); + return result; + } + + const l3L2Ser = spec.serOverride || opts.l2Serialized || l2.serialize(); + const actualSdHash = l3.payload.sd_hash ?? ''; + if (!actualSdHash) { + result.errors.push(`${label} missing required sd_hash binding to L2`); + return result; + } + if (actualSdHash !== (await hashAscii(l3L2Ser))) { + result.errors.push(`${label} sd_hash does not match L2 serialized form`); + return result; + } + + // 5a-bind. L3 presentation must include the correct mandate pair's disclosure. + if (spec.expectedPairDisc) { + const segments = l3L2Ser.split('~'); + if (!segments.includes(spec.expectedPairDisc)) { + result.errors.push( + `${label} L2 presentation does not include mandate pair ${pairIdx} disclosure (L3-to-mandate-pair identity mismatch)`, + ); + return result; + } + pairResult.checksPerformed.push(`pair_${pairIdx}_identity_binding`); + result.checksPerformed.push(`pair_${pairIdx}_identity_binding`); + } + + const l3SdAlg = l3.payload._sd_alg; + if (l3SdAlg !== undefined && l3SdAlg !== null && l3SdAlg !== 'sha-256') { + result.errors.push(`${label} _sd_alg must be 'sha-256', got '${String(l3SdAlg)}'`); + return result; + } + + const l3SdDup = duplicateSdDigest(l3.payload); + if (l3SdDup !== null) { + result.errors.push(`${label} _sd contains a duplicate disclosure digest (RFC 9901 section 7.1): ${l3SdDup}`); + return result; + } + + const l3Iat = l3.payload.iat; + if (isFutureDated(l3Iat, now, skew)) { + result.errors.push(`${label} iat is in the future: ${String(l3Iat)}`); + return result; + } + const l3Exp = l3.payload.exp; + if (isExpired(l3Exp, now, skew)) { + result.errors.push(`${label} expired at ${String(l3Exp)}`); + return result; + } + if (isFiniteNumber(l3Iat) && isFiniteNumber(l3Exp) && l3Exp - l3Iat > 3600) { + result.errors.push(`${label} exp MUST NOT exceed 1 hour from iat`); + return result; + } + + const l3Aud = l3.payload.aud; + const l3Nonce = l3.payload.nonce; + const expAud = spec.isPayment ? opts.expectedL3PaymentAud : opts.expectedL3CheckoutAud; + const expNonce = spec.isPayment ? opts.expectedL3PaymentNonce : opts.expectedL3CheckoutNonce; + const l3Tag = label.toLowerCase().replace(/ /g, '_'); + if (expAud !== undefined && expAud !== null) { + if (l3Aud !== expAud) { + result.errors.push(`${label} aud mismatch: expected '${expAud}', got '${String(l3Aud)}'`); + return result; + } + result.checksPerformed.push(`${l3Tag}_aud`); + } else if (typeof l3Aud === 'string' && l3Aud) { + result.checksSkipped.push(`${l3Tag}_aud (no expected value provided)`); + } + if (expNonce !== undefined && expNonce !== null) { + if (l3Nonce !== expNonce) { + result.errors.push(`${label} nonce mismatch: expected '${expNonce}', got '${String(l3Nonce)}'`); + return result; + } + result.checksPerformed.push(`${l3Tag}_nonce`); + } else if (typeof l3Nonce === 'string' && l3Nonce) { + result.checksSkipped.push(`${l3Tag}_nonce (no expected value provided)`); + } + + const l3HeaderKid = l3.header.kid; + if (typeof l3HeaderKid !== 'string' || !l3HeaderKid) { + result.errors.push(`${label} header missing required kid parameter`); + return result; + } + if (agentKid !== null && l3HeaderKid !== agentKid) { + result.errors.push(`${label} header kid '${l3HeaderKid}' does not match L2 cnf.jwk.kid '${agentKid}'`); + return result; + } + + const l3Claims = await resolveDisclosures(l3); + if (spec.isPayment) pairResult.l3PaymentClaims = l3Claims; + else pairResult.l3CheckoutClaims = l3Claims; + + const l3Err = validateL3MandateFields(l3Claims, label, spec.requiredVct, l2Pm); + if (l3Err) { + result.errors.push(l3Err); + return result; + } + + pairResult.checksPerformed.push(`${l3Tag}_structural_chain`); + result.checksPerformed.push(`${l3Tag}_structural_chain`); + } + + // 5b. Cross-reference check per pair + if ((l3p.l3Payment ?? null) !== null && (l3p.l3Checkout ?? null) !== null) { + const { valid: xrefValid, error: xrefError } = verifyL3CrossReference( + pairResult.l3PaymentClaims, + pairResult.l3CheckoutClaims, + ); + if (!xrefValid) { + result.errors.push(`L3 cross-reference check failed: ${xrefError}`); + return result; + } + pairResult.checksPerformed.push('l3_cross_reference'); + result.checksPerformed.push('l3_cross_reference'); + } else if ((l3p.l3Payment ?? null) !== null || (l3p.l3Checkout ?? null) !== null) { + pairResult.checksSkipped.push('l3_cross_reference (requires both L3a and L3b)'); + result.checksSkipped.push('l3_cross_reference (requires both L3a and L3b)'); + } + } + } + + // 6. Backward-compat: populate legacy fields from first pair + const firstPair = result.pairResults[0]; + if (firstPair) { + result.l3PaymentClaims = firstPair.l3PaymentClaims; + result.l3CheckoutClaims = firstPair.l3CheckoutClaims; + } + + result.valid = true; + return result; +} + +function isNonEmptyString(value: unknown): boolean { + return typeof value === 'string' && value.trim() !== ''; +} + +function validatePaymentMandateRequiredFields(mandate: JsonObject, context: string): string | null { + if (!isNonEmptyString(mandate.transaction_id)) return `${context} missing required field: transaction_id`; + + const payee = mandate.payee; + if (payee === undefined || payee === null) return `${context} missing required field: payee`; + if (!isJsonObject(payee)) return `${context} payee must be an object`; + if (!isNonEmptyString(payee.name)) return `${context} payee missing required field: name`; + if (!isNonEmptyString(payee.website)) return `${context} payee missing required field: website`; + + const paymentAmount = mandate.payment_amount; + if (!isJsonObject(paymentAmount) || Object.keys(paymentAmount).length === 0) { + return `${context} missing required field: payment_amount`; + } + if (!isNonEmptyString(paymentAmount.currency)) return `${context} payment_amount missing required field: currency`; + const amount = paymentAmount.amount; + if (amount === undefined || amount === null) return `${context} payment_amount missing required field: amount`; + if (typeof amount === 'boolean' || !Number.isInteger(amount)) { + return `${context} payment_amount field 'amount' must be an integer`; + } + + const paymentInstrument = mandate.payment_instrument; + if (paymentInstrument === undefined || paymentInstrument === null) { + return `${context} missing required field: payment_instrument`; + } + if (!isJsonObject(paymentInstrument) || !isNonEmptyString(paymentInstrument.id) || !isNonEmptyString(paymentInstrument.type)) { + return `${context} payment_instrument missing required field: id and type are required`; + } + return null; +} + +function validateL3PaymentInstrument(l3Delegate: JsonObject, l2PaymentMandate: JsonObject | null, l3Label: string): string | null { + if (!isJsonObject(l2PaymentMandate)) return null; + const l2Pi = l2PaymentMandate.payment_instrument; + if (!isJsonObject(l2Pi)) return null; + const l3Pi = l3Delegate.payment_instrument; + if (!isJsonObject(l3Pi)) return null; + if (l3Pi.id !== l2Pi.id || l3Pi.type !== l2Pi.type) { + return ( + `${l3Label} payment_instrument does not match L2 authorized value: ` + + `L3 id=${String(l3Pi.id)}, type=${String(l3Pi.type)} vs L2 id=${String(l2Pi.id)}, type=${String(l2Pi.type)}` + ); + } + return null; +} + +function validateL3MandateFields( + l3Claims: JsonObject, + l3Label: string, + requiredVct: string, + l2PaymentMandate: JsonObject | null, +): string | null { + const delegates = asArray(l3Claims.delegate_payload); + let foundRequiredVct = false; + for (const delegate of delegates) { + if (!isJsonObject(delegate)) continue; + const vct = delegate.vct; + if (vct === requiredVct) foundRequiredVct = true; + if (vct === L3_PAYMENT_VCT) { + const paymentFieldError = validatePaymentMandateRequiredFields(delegate, `${l3Label} payment mandate`); + if (paymentFieldError) return paymentFieldError; + const piErr = validateL3PaymentInstrument(delegate, l2PaymentMandate, l3Label); + if (piErr) return piErr; + } else if (vct === L3_CHECKOUT_VCT) { + for (const reqField of ['checkout_jwt', 'checkout_hash']) { + if (!delegate[reqField]) return `${l3Label} checkout mandate missing required field: ${reqField}`; + } + } + } + if (!foundRequiredVct) { + if (requiredVct === L3_PAYMENT_VCT) return `${l3Label} missing required Layer 3 payment mandate disclosure: ${requiredVct}`; + if (requiredVct === L3_CHECKOUT_VCT) return `${l3Label} missing required Layer 3 checkout mandate disclosure: ${requiredVct}`; + return `${l3Label} missing required mandate disclosure: ${requiredVct}`; + } + return null; +} + +function extractMandatePairs( + rawDelegates: unknown[], + resolvedDelegates: unknown[], + discStrByHash: Map, + isAutonomous: boolean, +): MandatePairsResult { + const checkouts: MandateInfo[] = []; + const payments: MandateInfo[] = []; + + const seenRefs = new Set(); + const n = Math.min(rawDelegates.length, resolvedDelegates.length); + for (let i = 0; i < n; i++) { + const rawItem = rawDelegates[i]; + const resolvedItem = resolvedDelegates[i]; + if (!isJsonObject(resolvedItem)) continue; + const vct = (resolvedItem.vct as string) ?? ''; + const refHash = isJsonObject(rawItem) ? ((rawItem['...'] as string) ?? null) : null; + + if (refHash) { + if (seenRefs.has(refHash)) { + return { pairs: [], errors: ['L2 delegate_payload contains duplicate disclosure reference (mandate smuggling)'] }; + } + seenRefs.add(refHash); + } + + const discB64 = refHash ? (discStrByHash.get(refHash) ?? null) : null; + const entry: MandateInfo = { resolved: resolvedItem, refHash, discB64 }; + + if (CHECKOUT_VCTS.has(vct)) checkouts.push(entry); + else if (PAYMENT_VCTS.has(vct)) payments.push(entry); + } + + if (checkouts.length === 0 && payments.length === 0) { + return { pairs: [], errors: ['L2 delegate_payload resolved zero mandate disclosures'] }; + } + + if (checkouts.length && payments.length) { + return isAutonomous ? pairAutonomous(checkouts, payments) : pairImmediate(checkouts, payments); + } + + if (!isAutonomous) { + return { pairs: [], errors: ['Immediate mode requires both checkout and payment mandate disclosures'] }; + } + + const pairs: MandatePair[] = []; + for (const c of checkouts) pairs.push([c, null]); + for (const p of payments) pairs.push([null, p]); + return { pairs, errors: [] }; +} + +function pairImmediate(checkouts: MandateInfo[], payments: MandateInfo[]): MandatePairsResult { + const checkoutByHash = new Map(); + for (const c of checkouts) { + if (c.resolved.vct === L2_CHECKOUT_VCT_OPEN) { + return { pairs: [], errors: ['Immediate mode does not allow open checkout mandates (requires final values)'] }; + } + const ch = (c.resolved.checkout_hash as string) ?? ''; + if (!ch) return { pairs: [], errors: ['Closed checkout mandate missing checkout_hash for pairing'] }; + if (checkoutByHash.has(ch)) { + return { pairs: [], errors: ['L2 contains duplicate checkout mandates with same pairing key (checkout_hash collision)'] }; + } + checkoutByHash.set(ch, c); + } + + const paymentByTid = new Map(); + for (const p of payments) { + if (p.resolved.vct === L2_PAYMENT_VCT_OPEN) { + return { pairs: [], errors: ['Immediate mode does not allow open payment mandates (requires final values)'] }; + } + const tid = (p.resolved.transaction_id as string) ?? ''; + if (!tid) return { pairs: [], errors: ['Closed payment mandate missing transaction_id for pairing'] }; + if (paymentByTid.has(tid)) { + return { pairs: [], errors: ['L2 contains duplicate payment mandates with same pairing key (transaction_id collision)'] }; + } + paymentByTid.set(tid, p); + } + + const pairs: MandatePair[] = []; + const matchedPayments = new Set(); + for (const [ch, checkout] of checkoutByHash) { + const payment = paymentByTid.get(ch); + if (payment) { + pairs.push([checkout, payment]); + matchedPayments.add(ch); + } else { + return { pairs: [], errors: ['Orphaned checkout mandate: no payment mandate with matching transaction_id'] }; + } + } + for (const tid of paymentByTid.keys()) { + if (!matchedPayments.has(tid)) { + return { pairs: [], errors: ['Orphaned payment mandate: no checkout mandate with matching checkout_hash'] }; + } + } + return { pairs, errors: [] }; +} + +function pairAutonomous(checkouts: MandateInfo[], payments: MandateInfo[]): MandatePairsResult { + const checkoutByRef = new Map(); + for (const c of checkouts) { + if (!c.refHash) return { pairs: [], errors: ['Checkout mandate missing disclosure reference hash for pairing'] }; + if (checkoutByRef.has(c.refHash)) { + return { pairs: [], errors: ['L2 contains duplicate checkout mandate disclosure references (pairing key collision)'] }; + } + checkoutByRef.set(c.refHash, c); + } + + const pairs: MandatePair[] = []; + const matchedCheckouts = new Set(); + for (const p of payments) { + let refConstraint: JsonObject | null = null; + for (const c of asArray(p.resolved.constraints)) { + if (isJsonObject(c) && c.type === 'mandate.payment.reference') { + refConstraint = c; + break; + } + } + if (refConstraint === null) { + return { pairs: [], errors: ['Open payment mandate missing mandate.payment.reference constraint for pairing'] }; + } + const condTid = (refConstraint.conditional_transaction_id as string) ?? ''; + if (!condTid) { + return { pairs: [], errors: ['mandate.payment.reference constraint missing conditional_transaction_id for pairing'] }; + } + if (matchedCheckouts.has(condTid)) { + return { pairs: [], errors: ['L2 contains duplicate payment mandates referencing same checkout (pairing key collision)'] }; + } + const checkout = checkoutByRef.get(condTid); + if (checkout) { + pairs.push([checkout, p]); + matchedCheckouts.add(condTid); + } else { + return { pairs: [], errors: ['Orphaned payment mandate: conditional_transaction_id does not match any checkout disclosure'] }; + } + } + for (const refHash of checkoutByRef.keys()) { + if (!matchedCheckouts.has(refHash)) { + return { pairs: [], errors: ['Orphaned checkout mandate: no payment mandate references this checkout'] }; + } + } + return { pairs, errors: [] }; +} + +async function verifyMandatePair( + checkoutMandate: JsonObject | null, + paymentMandate: JsonObject | null, + checkoutDiscB64: string | null, + isAutonomous: boolean, +): Promise { + const checksPerformed: string[] = []; + const checksSkipped: string[] = []; + const fail = (error: string): MandatePairCheck => ({ errors: [error], checksPerformed: [], checksSkipped: [] }); + + if (checkoutMandate && checkoutMandate.vct === L2_CHECKOUT_VCT_OPEN) { + const constraints = asArray(checkoutMandate.constraints); + const hasLineItems = constraints.some((c) => isJsonObject(c) && c.type === 'mandate.checkout.line_items'); + if (!hasLineItems) return fail('Open checkout mandate must contain a mandate.checkout.line_items constraint'); + checksPerformed.push('open_checkout_contains_line_items'); + } + + if (paymentMandate && paymentMandate.vct === L2_PAYMENT_VCT_OPEN) { + const constraints = asArray(paymentMandate.constraints); + const hasReference = constraints.some((c) => isJsonObject(c) && c.type === 'mandate.payment.reference'); + if (!hasReference) return fail('Open payment mandate must contain a mandate.payment.reference constraint'); + const pi = paymentMandate.payment_instrument; + if (!isJsonObject(pi) || !pi.id || !pi.type) { + return fail('Open payment mandate missing required field: payment_instrument (must have id and type)'); + } + checksPerformed.push('open_payment_has_payment_instrument'); + checksPerformed.push('open_payment_contains_reference'); + } + + if (!isAutonomous) { + if (!checkoutMandate || !paymentMandate) { + return fail('Immediate mode requires both checkout and payment mandate disclosures'); + } + if (checkoutMandate.vct === L2_CHECKOUT_VCT_OPEN) { + return fail('Immediate mode does not allow open checkout mandates (requires final values)'); + } + if (paymentMandate.vct === L2_PAYMENT_VCT_OPEN) { + return fail('Immediate mode does not allow open payment mandates (requires final values)'); + } + for (const [mandate, label] of [ + [checkoutMandate, 'checkout'], + [paymentMandate, 'payment'], + ] as const) { + if ('cnf' in mandate) { + return fail(`Immediate mode ${label} mandate must not contain cnf claim (cnf is for autonomous delegation only)`); + } + } + if (checkoutMandate.vct === L2_CHECKOUT_VCT_FINAL) { + for (const reqField of ['checkout_jwt', 'checkout_hash']) { + if (!checkoutMandate[reqField]) return fail(`Closed checkout mandate missing required field: ${reqField}`); + } + checksPerformed.push('closed_checkout_required_fields'); + } + if (paymentMandate.vct === L2_PAYMENT_VCT_FINAL) { + const paymentFieldError = validatePaymentMandateRequiredFields(paymentMandate, 'Closed payment mandate'); + if (paymentFieldError) return fail(paymentFieldError); + checksPerformed.push('closed_payment_required_fields'); + } + const { valid: bindingValid, error: bindingError } = await verifyCheckoutHashBinding(checkoutMandate, paymentMandate); + if (!bindingValid) return fail(`L2 checkout-payment binding failed: ${bindingError}`); + checksPerformed.push('l2_checkout_payment_binding'); + } + + if (isAutonomous) { + if (checkoutMandate && paymentMandate) { + if (!checkoutDiscB64) { + return fail('L2 checkout mandate disclosure string is missing (required for reference binding verification)'); + } + const { valid: bindingValid, error: bindingError } = await verifyL2ReferenceBinding( + checkoutMandate, + paymentMandate, + checkoutDiscB64, + ); + if (!bindingValid) return fail(`L2 reference binding failed: ${bindingError}`); + checksPerformed.push('l2_reference_binding'); + } else { + checksSkipped.push('l2_reference_binding (requires both checkout and payment mandates)'); + } + } + + return { errors: [], checksPerformed, checksSkipped }; +} + +function extractAgentKeyFromAllPairs(mandatePairs: MandatePair[]): AgentKeyResult { + const agentKeys: JsonObject[] = []; + const agentKids: (string | null)[] = []; + + for (const [checkoutInfo, paymentInfo] of mandatePairs) { + for (const [label, info, expectedVct] of [ + ['checkout', checkoutInfo, L2_CHECKOUT_VCT_OPEN], + ['payment', paymentInfo, L2_PAYMENT_VCT_OPEN], + ] as const) { + if (info === null) continue; + if (info.resolved.vct !== expectedVct) continue; + const cnf = info.resolved.cnf; + const jwk = isJsonObject(cnf) ? cnf.jwk : null; + if (!isJsonObject(jwk) || Object.keys(jwk).length === 0) { + return { jwk: null, kid: null, error: `L2 ${label} open mandate missing cnf.jwk for agent delegation` }; + } + agentKeys.push(jwk); + agentKids.push(typeof jwk.kid === 'string' ? jwk.kid : null); + } + } + + if (agentKeys.length === 0) return { jwk: null, kid: null, error: null }; + + const [first, ...restKeys] = agentKeys; + if (!first) return { jwk: null, kid: null, error: null }; + for (const other of restKeys) { + if (other.x !== first.x || other.y !== first.y) { + return { jwk: null, kid: null, error: 'L2 mandate cnf.jwk values must be identical across all pairs but differ' }; + } + } + + const [firstKid, ...restKids] = agentKids.filter((k): k is string => k !== null); + if (firstKid !== undefined) { + for (const otherKid of restKids) { + if (otherKid !== firstKid) { + return { jwk: null, kid: null, error: 'L2 mandate cnf.jwk.kid values must be identical across all pairs but differ' }; + } + } + return { jwk: first, kid: firstKid, error: null }; + } + return { jwk: first, kid: null, error: null }; +} diff --git a/typescript/src/verification/constraint-checker.ts b/typescript/src/verification/constraint-checker.ts new file mode 100644 index 0000000..ffa1f9c --- /dev/null +++ b/typescript/src/verification/constraint-checker.ts @@ -0,0 +1,519 @@ +/** Constraint validation: verify Layer 3 fulfillment values satisfy Layer 2 constraints. */ + +import { + AgentRecurrenceConstraint, + AllowedMerchantConstraint, + AllowedPayeeConstraint, + CheckoutLineItemsConstraint, + PaymentAmountConstraint, + PaymentBudgetConstraint, + PaymentRecurrenceConstraint, + parseConstraint, + ReferenceConstraint, +} from '../models/constraints.js'; +import type { JsonObject } from '../models/constraints.js'; +import { isJsonObject } from '../internal/guards.js'; + +export const StrictnessMode = { + PERMISSIVE: 'permissive', // skip unknown constraint types + STRICT: 'strict', // fail on unknown constraint types +} as const; +export type StrictnessMode = (typeof StrictnessMode)[keyof typeof StrictnessMode]; + +export class ConstraintCheckResult { + satisfied = true; + violations: string[] = []; + checked: string[] = []; + skipped: string[] = []; +} + +const isInt = (v: unknown): v is number => typeof v === 'number' && Number.isInteger(v); +const nonEmpty = (v: unknown): boolean => isJsonObject(v) && Object.keys(v).length > 0; +const truthy = (v: unknown): boolean => Boolean(v); + +export interface CheckConstraintsOptions { + mode?: StrictnessMode; + isOpenMandate?: boolean; + constraintPolicy?: Record; +} + +/** Check whether the fulfillment values satisfy all constraints. */ +export function checkConstraints( + constraints: unknown, + fulfillment: unknown, + opts: CheckConstraintsOptions = {}, +): ConstraintCheckResult { + const mode = opts.mode ?? StrictnessMode.PERMISSIVE; + const isOpenMandate = opts.isOpenMandate ?? false; + const constraintPolicy = opts.constraintPolicy; + + const result = new ConstraintCheckResult(); + + if (!isJsonObject(fulfillment)) { + result.satisfied = false; + result.violations.push(`Fulfillment must be an object, got ${fulfillment === null ? 'null' : typeof fulfillment}`); + return result; + } + if (!Array.isArray(constraints)) { + result.satisfied = false; + result.violations.push(`Constraints must be a list, got ${constraints === null ? 'null' : typeof constraints}`); + return result; + } + + for (const cData of constraints) { + if (!isJsonObject(cData)) { + result.satisfied = false; + result.violations.push(`Constraint entry must be an object, got ${cData === null ? 'null' : typeof cData}`); + continue; + } + const constraint = parseConstraint(cData); + const ctype = constraint.type; + + if (constraint instanceof PaymentAmountConstraint) { + checkPaymentAmount(constraint, fulfillment, result); + } else if (constraint instanceof AllowedPayeeConstraint) { + checkAllowedPayee(constraint, fulfillment, result); + } else if (constraint instanceof AllowedMerchantConstraint) { + checkAllowedMerchant(constraint, fulfillment, result); + } else if (constraint instanceof CheckoutLineItemsConstraint) { + checkLineItems(constraint, fulfillment, result); + } else if (constraint instanceof ReferenceConstraint) { + result.checked.push(ctype); // verified by the integrity module + } else if ( + constraint instanceof PaymentBudgetConstraint || + constraint instanceof PaymentRecurrenceConstraint || + constraint instanceof AgentRecurrenceConstraint + ) { + result.checked.push(ctype); // network-enforced constraints + } else { + // Determine effective strictness: a per-type policy overrides the global mode. + let effectiveMode = mode; + const policyMode = constraintPolicy?.[ctype]; + if (policyMode !== undefined) { + effectiveMode = policyMode; + } + // Fail closed on an unknown (hence unenforceable) constraint under STRICT, and + // ALWAYS for an open mandate: an open mandate leaves the agent's authority + // unbounded, so a constraint we cannot check must never be silently skipped. + if (isOpenMandate || effectiveMode === StrictnessMode.STRICT) { + result.satisfied = false; + result.violations.push(`Unknown constraint type: ${ctype}`); + } else { + result.skipped.push(ctype); + } + } + } + + return result; +} + +/** + * Match merchants: by id if both have it, else by name+website. + * + * Python uses `==` / `bool()`; this port uses `===` / `Boolean()`. For the + * JSON-scalar identity fields (id, name, website) the two agree. Where they can + * diverge — a non-primitive field (Python `==` compares objects by value, `===` + * by reference) or JS treating `[]` / `{}` as truthy where `bool()` is falsy — + * `===` can only FAIL a match that Python would make, never invent one (two + * distinct parsed objects are never `===`, and that inequality short-circuits + * before the truthiness guard matters). A non-match means "not in the allowlist", + * so the caller fails closed: the translation stays equal-or-stricter than + * Python here, never more permissive. + */ +function merchantMatches(candidate: unknown, target: unknown): boolean { + if (!isJsonObject(candidate) || !isJsonObject(target)) return false; + const cId = candidate.id; + const tId = target.id; + if (cId && tId) return cId === tId; + return ( + candidate.name === target.name && + truthy(candidate.name) && + candidate.website === target.website && + truthy(candidate.website) + ); +} + +/** + * Check the payment amount is within min/max bounds (integer minor units). + * + * Per AP2 schema, L3a nests amount/currency under a payment_amount object. + */ +function checkPaymentAmount(c: PaymentAmountConstraint, fulfillment: JsonObject, result: ConstraintCheckResult): void { + result.checked.push('mandate.payment.amount_range'); + const paymentAmount = fulfillment.payment_amount; + if (!nonEmpty(paymentAmount)) { + result.satisfied = false; + result.violations.push('Missing or invalid payment_amount in fulfillment'); + return; + } + const amountRaw = (paymentAmount as JsonObject).amount; + if (amountRaw === undefined || amountRaw === null) { + result.satisfied = false; + result.violations.push('Missing amount in fulfillment payment_amount'); + return; + } + if (typeof amountRaw === 'boolean' || !isInt(amountRaw)) { + result.satisfied = false; + result.violations.push(`Invalid amount: must be an integer, got ${typeof amountRaw}: ${JSON.stringify(amountRaw)}`); + return; + } + const actual = amountRaw; + + if (c.min !== null) { + if (typeof c.min === 'boolean' || !isInt(c.min)) { + result.satisfied = false; + result.violations.push(`Constraint min must be an integer, got ${typeof c.min}: ${JSON.stringify(c.min)}`); + return; + } + if (actual < c.min) { + result.satisfied = false; + result.violations.push(`Amount below minimum: ${actual} < ${c.min} ${c.currency}`); + } + } + + if (c.max !== null) { + if (typeof c.max === 'boolean' || !isInt(c.max)) { + result.satisfied = false; + result.violations.push(`Constraint max must be an integer, got ${typeof c.max}: ${JSON.stringify(c.max)}`); + return; + } + if (actual > c.max) { + result.satisfied = false; + result.violations.push(`Amount exceeds maximum: ${actual} > ${c.max} ${c.currency}`); + } + } + + // Mirror Python's .get("currency", c.currency): substitute the constraint + // currency only when the key is ABSENT. A present-but-null currency must be + // compared (and fail), not defaulted. + const pa = paymentAmount as JsonObject; + const fulfillmentCurrency = 'currency' in pa ? pa.currency : c.currency; + if (fulfillmentCurrency !== c.currency) { + result.satisfied = false; + result.violations.push(`Currency mismatch: expected ${c.currency}, got ${String(fulfillmentCurrency)}`); + } +} + +/** + * Reduce a constraint `allowed` list to usable inline merchant objects: drop + * SD-ref placeholders (entries carrying a "..." key) and any entry lacking both + * an id and a name. Used only when the fulfillment did not supply resolved + * allowed_merchants, i.e. the allowlist is inline rather than SD-referenced. + */ +function resolveAllowed(constraintAllowed: unknown[]): unknown[] { + return constraintAllowed.filter((m) => isJsonObject(m) && !('...' in m) && (m.id || m.name)); +} + +// checkAllowedPayee and checkAllowedMerchant are near-duplicates, but they mirror +// two separate Python functions (_check_allowed_payee / _check_allowed_merchant) +// with distinct constraint types and error strings. They are kept separate — not +// factored into one helper — to preserve line-for-line parity with Python. +function checkAllowedPayee(c: AllowedPayeeConstraint, fulfillment: JsonObject, result: ConstraintCheckResult): void { + result.checked.push('mandate.payment.allowed_payees'); + const payee = fulfillment.payee ?? {}; + if (!nonEmpty(payee)) { + result.satisfied = false; + result.violations.push('Missing or invalid payee in fulfillment'); + return; + } + if (!Array.isArray(c.allowed)) { + result.satisfied = false; + result.violations.push(`mandate.payment.allowed_payees 'allowed' must be a list, got ${typeof c.allowed}`); + return; + } + if (c.allowed.length === 0) { + result.satisfied = false; + result.violations.push("mandate.payment.allowed_payees constraint missing required 'allowed' field"); + return; + } + + // Check whether the payee matches any allowed merchant. In L2 `allowed` holds + // SD disclosure refs; the resolved merchants should arrive via + // fulfillment.allowed_merchants (matches Python dict.get("allowed_merchants", [])). + let allowedMerchants: unknown[] = Array.isArray(fulfillment.allowed_merchants) ? fulfillment.allowed_merchants : []; + let constraintAllowed: unknown[] = []; + if (allowedMerchants.length === 0) { + // Support inline allowlists when constraints are not represented as SD refs. + constraintAllowed = Array.isArray(c.allowed) ? c.allowed : []; + allowedMerchants = resolveAllowed(constraintAllowed); + } + if (allowedMerchants.length === 0) { + // Distinguish: all SD refs → skip (unresolved, not a violation); inline + // merchants that failed validation → fail closed. + const source = constraintAllowed.length ? constraintAllowed : c.allowed; + const allSdRefs = source.every((m) => isJsonObject(m) && '...' in m); + if (allSdRefs) { + result.checked.push('mandate.payment.allowed_payees (skipped: no resolved payees)'); + return; + } + result.satisfied = false; + result.violations.push('allowed_payees constraint present but no payees resolved'); + return; + } + + const found = allowedMerchants.some((m) => merchantMatches(m, payee)); + if (!found) { + const p = payee as JsonObject; + result.satisfied = false; + result.violations.push(`Payee ${String(p.name ?? '')} (id=${String(p.id ?? '')}) not in allowed merchants`); + } +} + +function checkAllowedMerchant(c: AllowedMerchantConstraint, fulfillment: JsonObject, result: ConstraintCheckResult): void { + result.checked.push('mandate.checkout.allowed_merchants'); + const merchant = fulfillment.merchant ?? {}; + if (!nonEmpty(merchant)) { + result.satisfied = false; + result.violations.push('Missing or invalid merchant in fulfillment'); + return; + } + if (!Array.isArray(c.allowed)) { + result.satisfied = false; + result.violations.push(`mandate.checkout.allowed_merchants 'allowed' must be a list, got ${typeof c.allowed}`); + return; + } + if (c.allowed.length === 0) { + result.satisfied = false; + result.violations.push("mandate.checkout.allowed_merchants constraint missing required 'allowed' field"); + return; + } + + let allowedMerchants: unknown[] = Array.isArray(fulfillment.allowed_merchants) ? fulfillment.allowed_merchants : []; + let constraintMerchants: unknown[] = []; + if (allowedMerchants.length === 0) { + // Support inline allowlists when constraints are not represented as SD refs. + constraintMerchants = Array.isArray(c.allowed) ? c.allowed : []; + allowedMerchants = resolveAllowed(constraintMerchants); + } + if (allowedMerchants.length === 0) { + // Distinguish: all SD refs → skip (unresolved, not a violation); inline + // merchants that failed validation → fail closed. + const source = constraintMerchants.length ? constraintMerchants : c.allowed; + const allSdRefs = source.every((m) => isJsonObject(m) && '...' in m); + if (allSdRefs) { + result.checked.push('mandate.checkout.allowed_merchants (skipped: no resolved merchants)'); + return; + } + result.satisfied = false; + result.violations.push('allowed_merchants constraint present but no merchants resolved'); + return; + } + + const found = allowedMerchants.some((m) => merchantMatches(m, merchant)); + if (!found) { + const m = merchant as JsonObject; + result.satisfied = false; + result.violations.push(`Merchant ${String(m.name ?? '')} (id=${String(m.id ?? '')}) not in allowed list`); + } +} + +/** + * Check selected items match the line items constraint. + * + * items: list of {id, acceptable_items, quantity} — each defines an allowed + * line item with its own product ID allowlist and quantity limit. + */ +function checkLineItems(c: CheckoutLineItemsConstraint, fulfillment: JsonObject, result: ConstraintCheckResult): void { + result.checked.push('mandate.checkout.line_items'); + + if (!c.items || c.items.length === 0) { + // AP2 schema enforces minItems: 1 on line_items.items — an empty items list + // is always a malformed constraint regardless of cart state. + result.satisfied = false; + result.violations.push('line_items constraint must have at least one item entry'); + return; + } + + // L2-side schema validation: acceptable_items entries must have a title. This + // runs regardless of whether line_items are present (constraint validity is + // independent of fulfillment). + const allowedIds = new Set(); + const idQuantityLimits = new Map(); // item id -> summed quantity cap across matching requirements + let hasNonemptyAcceptable = false; + let hasWildcardAcceptable = false; + let totalQuantityLimit = 0; + let hasQuantityLimit = false; + + for (const itemEntry of c.items) { + if (!isJsonObject(itemEntry)) { + result.satisfied = false; + result.violations.push(`line_items item entry must be an object, got ${itemEntry === null ? 'null' : typeof itemEntry}`); + continue; + } + + const acceptableItems = itemEntry.acceptable_items; + if (Array.isArray(acceptableItems) && acceptableItems.length > 0) hasNonemptyAcceptable = true; + if (Array.isArray(acceptableItems) && acceptableItems.length === 0) hasWildcardAcceptable = true; + + const itemId = itemEntry.id; + if (typeof itemId !== 'string' || !itemId) { + result.satisfied = false; + result.violations.push("line_items item entry missing required 'id' field"); + continue; + } + if (!('acceptable_items' in itemEntry)) { + result.satisfied = false; + result.violations.push(`line_items item '${itemId}' missing required 'acceptable_items' field`); + continue; + } + + const quantityRaw = itemEntry.quantity; + if (typeof quantityRaw === 'boolean' || !isInt(quantityRaw)) { + result.satisfied = false; + result.violations.push(`line_items item quantity must be an integer, got ${JSON.stringify(quantityRaw)}`); + continue; + } + const quantityLimit = quantityRaw; + if (quantityLimit <= 0) { + result.satisfied = false; + result.violations.push('line_items item quantity must be positive'); + continue; + } + + hasQuantityLimit = true; + totalQuantityLimit += quantityLimit; + + if (!Array.isArray(acceptableItems)) { + result.satisfied = false; + result.violations.push('line_items acceptable_items must be an array'); + continue; + } + + const itemIds = new Set(); + for (const ai of acceptableItems) { + if (isJsonObject(ai) && !('...' in ai)) { + if (!ai.title) { + result.satisfied = false; + result.violations.push(`Item ${String(ai.id ?? '?')} in acceptable_items missing required 'title'`); + } + const itemIdVal = ai.id || ai.sku; + if (itemIdVal && typeof itemIdVal === 'string') { + itemIds.add(itemIdVal); + allowedIds.add(itemIdVal); + } else if (itemIdVal !== undefined && itemIdVal !== null && typeof itemIdVal !== 'string') { + result.satisfied = false; + result.violations.push(`acceptable_items entry has non-string id: ${typeof itemIdVal}`); + } + } + } + + for (const itemIdVal of itemIds) { + idQuantityLimits.set(itemIdVal, (idQuantityLimits.get(itemIdVal) ?? 0) + quantityLimit); + } + } + + // Fail-closed: constraint has non-empty acceptable_items, but none resolved to usable IDs. + // Empty acceptable_items entries are wildcards and allow any item for that line-item requirement. + if (hasNonemptyAcceptable && allowedIds.size === 0 && !hasWildcardAcceptable) { + result.satisfied = false; + result.violations.push('line_items constraint present but no item IDs resolved'); + return; + } + + const lineItems = fulfillment.line_items; + if (!Array.isArray(lineItems)) { + result.satisfied = false; + result.violations.push(`line_items must be a list, got ${lineItems === null ? 'null' : typeof lineItems}`); + return; + } + if (lineItems.length === 0) { + if (c.items.length > 0) { + result.satisfied = false; + result.violations.push('Empty line_items does not satisfy line_items constraint with required items'); + } + return; + } + + let totalQuantity = 0; + const quantityById = new Map(); + for (const lineItem of lineItems) { + if (!isJsonObject(lineItem)) { + result.satisfied = false; + result.violations.push(`Line item must be an object, got ${lineItem === null ? 'null' : typeof lineItem}`); + continue; + } + const itemIdVal = lineItem.id || lineItem.sku; + if (!itemIdVal) { + result.satisfied = false; + result.violations.push("Line item missing 'id' field"); + continue; + } + if (typeof itemIdVal !== 'string') { + result.satisfied = false; + result.violations.push(`Line item 'id' must be a non-empty string, got ${typeof itemIdVal}: ${JSON.stringify(itemIdVal)}`); + continue; + } + + // Match Python's line_item.get("quantity", 0): default only when the key is + // ABSENT. A present-but-null quantity must be rejected as a non-integer. + const quantityRaw = 'quantity' in lineItem ? lineItem.quantity : 0; + if (typeof quantityRaw === 'boolean' || !isInt(quantityRaw)) { + result.satisfied = false; + result.violations.push(`Invalid quantity for item ${itemIdVal}: ${JSON.stringify(quantityRaw)}`); + continue; + } + const quantity = quantityRaw; + if (quantity < 0) { + result.satisfied = false; + result.violations.push(`Negative quantity for item ${itemIdVal}: ${quantity}`); + continue; + } + + if (allowedIds.size > 0 && itemIdVal && !allowedIds.has(itemIdVal) && !hasWildcardAcceptable) { + result.satisfied = false; + result.violations.push(`Item ${itemIdVal} not in acceptable items: ${JSON.stringify([...allowedIds].sort())}`); + } + + totalQuantity += quantity; + if (itemIdVal) quantityById.set(itemIdVal, (quantityById.get(itemIdVal) ?? 0) + quantity); + } + + // Aggregate quantity cap across all line-item requirements. + if (hasQuantityLimit && totalQuantity > totalQuantityLimit) { + result.satisfied = false; + result.violations.push(`Total quantity ${totalQuantity} exceeds limit ${totalQuantityLimit}`); + } + + // Per-item quantity caps derived from the line-item requirement -> acceptable ID mapping. + for (const [itemIdVal, itemQty] of quantityById) { + const idCap = idQuantityLimits.get(itemIdVal); + if (idCap !== undefined && itemQty > idCap) { + result.satisfied = false; + result.violations.push(`Quantity for item ${itemIdVal} exceeds per-item limit ${idCap}`); + } + } + + // match_mode controls whether fulfillment may be a subset of the allowed + // line-item requirements or must cover each requirement at least once. + const matchMode = c.matchMode; + if (typeof matchMode !== 'string' || (matchMode !== 'minimum' && matchMode !== 'exact')) { + result.satisfied = false; + result.violations.push(`line_items match_mode must be 'minimum' or 'exact', got ${JSON.stringify(matchMode)}`); + return; + } + + if (matchMode === 'exact') { + const missingEntries: string[] = []; + for (const itemEntry of c.items) { + if (!isJsonObject(itemEntry)) continue; + const acceptableItems = itemEntry.acceptable_items; + if (!Array.isArray(acceptableItems) || acceptableItems.length === 0) continue; + + const entryId = itemEntry.id ?? '?'; + const resolvedIds = new Set(); + for (const ai of acceptableItems) { + if (isJsonObject(ai) && !('...' in ai)) { + const id = ai.id || ai.sku; + if (typeof id === 'string' && id) resolvedIds.add(id); + } + } + if (resolvedIds.size > 0 && ![...resolvedIds].some((id) => (quantityById.get(id) ?? 0) > 0)) { + missingEntries.push(`${String(entryId)}: ${JSON.stringify([...resolvedIds].sort())}`); + } + } + if (missingEntries.length > 0) { + result.satisfied = false; + result.violations.push('match_mode=exact: fulfillment missing required line item(s): ' + missingEntries.join(', ')); + } + } +} diff --git a/typescript/src/verification/index.ts b/typescript/src/verification/index.ts new file mode 100644 index 0000000..4c6763c --- /dev/null +++ b/typescript/src/verification/index.ts @@ -0,0 +1,3 @@ +export * from './integrity.js'; +export * from './constraint-checker.js'; +export * from './chain.js'; diff --git a/typescript/src/verification/integrity.ts b/typescript/src/verification/integrity.ts new file mode 100644 index 0000000..e9c4be0 --- /dev/null +++ b/typescript/src/verification/integrity.ts @@ -0,0 +1,107 @@ +/** Checkout-payment integrity verification (SHA-256 cross-referencing). */ + +import { hashAscii, hashDisclosure } from '../crypto/disclosure.js'; +import { isJsonObject } from '../internal/guards.js'; +import type { JsonObject } from '../models/constraints.js'; + +/** A binding-check verdict: whether it held, and the error message when it didn't. */ +export interface IntegrityResult { + valid: boolean; + error: string; +} + +/** Verify checkout_hash = SHA-256(checkout_jwt) and transaction_id = checkout_hash. */ +export function verifyCheckoutHashBinding(checkoutMandate: JsonObject, paymentMandate: JsonObject): Promise { + const checkoutJwt = checkoutMandate.checkout_jwt; + if (checkoutJwt !== undefined && checkoutJwt !== null && typeof checkoutJwt !== 'string') { + return Promise.resolve({ valid: false, error: `checkout_jwt must be a string, got ${typeof checkoutJwt}` }); + } + if (!checkoutJwt) return Promise.resolve({ valid: true, error: '' }); // no checkout_jwt to bind + + const checkoutHash = checkoutMandate.checkout_hash; + if (!checkoutHash) { + return Promise.resolve({ valid: false, error: 'checkout_jwt present but checkout_hash missing from checkout mandate' }); + } + + // Deliberately NOT declared `async`: hashAscii throws synchronously on + // non-ASCII input, and letting that propagate matches Python's uncaught + // UnicodeEncodeError (pinned by the parity tests). An `async` function would + // convert the throw into a rejection. + return hashAscii(checkoutJwt as string).then((computed) => { + if (computed !== checkoutHash) { + return { valid: false, error: `checkout_hash mismatch: computed ${computed} != expected ${String(checkoutHash)}` }; + } + + const transactionId = paymentMandate.transaction_id; + if (!transactionId) { + return { valid: false, error: 'checkout_jwt present but transaction_id missing from payment mandate' }; + } + if (transactionId !== checkoutHash) { + return { + valid: false, + error: `transaction_id mismatch: ${String(transactionId)} != checkout_hash ${String(checkoutHash)}`, + }; + } + + return { valid: true, error: '' }; + }); +} + +/** Verify the L2 mandate.payment.reference constraint binds to the L2 checkout disclosure. */ +export async function verifyL2ReferenceBinding( + _checkoutMandate: JsonObject, + paymentMandate: JsonObject, + checkoutDisclosureB64: string, +): Promise { + const constraints = paymentMandate.constraints; + let refConstraint: JsonObject | null = null; + for (const c of Array.isArray(constraints) ? constraints : []) { + if (isJsonObject(c) && c.type === 'mandate.payment.reference') { + refConstraint = c; + break; + } + } + if (refConstraint === null) return { valid: true, error: '' }; // no reference constraint to check + + const expectedId = (refConstraint.conditional_transaction_id as string) || ''; + if (!expectedId) { + return { valid: false, error: 'mandate.payment.reference missing required conditional_transaction_id' }; + } + + const computedHash = await hashDisclosure(checkoutDisclosureB64); + if (computedHash !== expectedId) { + return { valid: false, error: `conditional_transaction_id mismatch: computed ${computedHash} != expected ${expectedId}` }; + } + return { valid: true, error: '' }; +} + +/** Verify L3a transaction_id matches L3b checkout_hash. */ +export function verifyL3CrossReference(l3PaymentClaims: JsonObject, l3CheckoutClaims: JsonObject): IntegrityResult { + const l3aDelegates = l3PaymentClaims.delegate_payload; + let transactionId: unknown = null; + for (const d of Array.isArray(l3aDelegates) ? l3aDelegates : []) { + if (isJsonObject(d) && d.vct === 'mandate.payment.1') { + transactionId = d.transaction_id ?? null; + break; + } + } + + const l3bDelegates = l3CheckoutClaims.delegate_payload; + let checkoutHash: unknown = null; + for (const d of Array.isArray(l3bDelegates) ? l3bDelegates : []) { + if (isJsonObject(d) && d.vct === 'mandate.checkout.1') { + checkoutHash = d.checkout_hash ?? null; + break; + } + } + + if (transactionId === null) return { valid: false, error: 'L3a payment mandate missing transaction_id' }; + if (checkoutHash === null) return { valid: false, error: 'L3b checkout mandate missing checkout_hash' }; + if (transactionId !== checkoutHash) { + return { + valid: false, + error: `L3 cross-reference mismatch: transaction_id=${String(transactionId)} != checkout_hash=${String(checkoutHash)}`, + }; + } + return { valid: true, error: '' }; +} diff --git a/typescript/test/chain-failclosed.test.ts b/typescript/test/chain-failclosed.test.ts new file mode 100644 index 0000000..8866315 --- /dev/null +++ b/typescript/test/chain-failclosed.test.ts @@ -0,0 +1,94 @@ +/** + * Fail-closed regression tests for L1 header/claim validation in verifyChain. + * + * The chain verifier enforces several spec-mandated, security-critical rules at + * Layer 1 that are NOT exercised by the golden conformance vectors: + * - algorithm confusion (alg MUST be ES256) + * - type confusion (typ MUST be 'sd+jwt') + * - credential-type lock (vct MUST be the expected issuer VCT) + * - hash-algorithm lock (_sd_alg, when present, MUST be 'sha-256') + * + * Each rule is checked BEFORE the L2 signature step, so we take the known-valid + * `immediate_valid` golden chain, skip issuer signature verification, and apply a + * single in-memory mutation to the decoded L1. A regression that turned any of + * these guards into a fail-OPEN would flip one of these expectations to valid. + * + * These inputs are non-conformant by construction; a well-formed signed chain is + * unaffected (see the baseline test). + */ + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +import { decodeSdJwt, type Es256Jwk, verifySdJwtSignature, type VerifyChainOptions, verifyChain } from '../src/index.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const V: any = JSON.parse(readFileSync(join(here, '../../test-vectors/vectors.json'), 'utf8')); +const SCENARIO = V.verification_conformance.chain_scenarios.find((s: any) => s.name === 'immediate_valid'); + +/** Fresh decode each call so per-test mutations never leak between cases. */ +const freshL1 = () => decodeSdJwt(SCENARIO.l1); +const freshL2 = () => decodeSdJwt(SCENARIO.l2); + +/** Skip only the issuer signature so L1 header/claim checks run against our mutation. */ +const opts = (): VerifyChainOptions => ({ skipIssuerVerification: true, currentTime: SCENARIO.current_time }); + +describe('fail-closed: L1 header/claim validation (uncovered by golden vectors)', () => { + it('baseline: the unmutated immediate chain is valid when issuer sig is skipped', async () => { + const res = await verifyChain(freshL1(), freshL2(), opts()); + expect(res.valid).toBe(true); + expect(res.errors).toEqual([]); + }); + + it('rejects algorithm confusion (alg=HS256, not ES256)', async () => { + const l1 = freshL1(); + (l1.header as Record).alg = 'HS256'; + const res = await verifyChain(l1, freshL2(), opts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => /alg must be one of/i.test(e))).toBe(true); + }); + + it('rejects algorithm stripping (alg=none)', async () => { + const l1 = freshL1(); + (l1.header as Record).alg = 'none'; + const res = await verifyChain(l1, freshL2(), opts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => /alg must be one of/i.test(e))).toBe(true); + }); + + it("rejects type confusion (typ != 'sd+jwt')", async () => { + const l1 = freshL1(); + (l1.header as Record).typ = 'jwt'; + const res = await verifyChain(l1, freshL2(), opts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => /header typ must be/i.test(e))).toBe(true); + }); + + it('rejects an unexpected credential type (vct mismatch)', async () => { + const l1 = freshL1(); + (l1.payload as Record).vct = 'https://evil.example.com/card'; + const res = await verifyChain(l1, freshL2(), opts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => /vct must be/i.test(e))).toBe(true); + }); + + it("rejects a non-sha-256 hash algorithm (_sd_alg='sha-512')", async () => { + const l1 = freshL1(); + (l1.payload as Record)._sd_alg = 'sha-512'; + const res = await verifyChain(l1, freshL2(), opts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => /_sd_alg must be 'sha-256'/i.test(e))).toBe(true); + }); +}); + +describe('fail-closed: verifySdJwtSignature returns false (never throws) on a hostile public key', () => { + it('resolves to false for a non-importable JWK instead of rejecting', async () => { + const sj = freshL1(); + // x/y are not valid base64url EC coordinates → WebCrypto importKey rejects. + const badJwk = { kty: 'EC', crv: 'P-256', x: '!!!not-base64!!!', y: '!!!not-base64!!!' } as Es256Jwk; + await expect(verifySdJwtSignature(sj, badJwk)).resolves.toBe(false); + }); +}); diff --git a/typescript/test/cnf-hardening.test.ts b/typescript/test/cnf-hardening.test.ts new file mode 100644 index 0000000..b14f606 --- /dev/null +++ b/typescript/test/cnf-hardening.test.ts @@ -0,0 +1,498 @@ +/** + * Fail-closed tests for the cnf / agent-delegation-key hardening in + * `src/verification/chain.ts` — the key-substitution attack surface. + * + * The verifier resolves three keys from confirmation (`cnf`) claims: + * - L1 `cnf.jwk` → the USER key that must sign L2. + * - the open L2 mandates' `cnf.jwk` → the AGENT key that must sign L3. + * If any of these could be swapped, forged, or sourced from an attacker- + * controlled disclosure, the whole delegation chain would fail open. These + * cases lock the guards closed: + * - L1 `cnf` shape validation (chain.ts §3, ~L268-282, validateEcPublicJwk ~L78) + * - agent-key extraction cross-checks (extractAgentKeyFromAllPairs ~L1024) + * - agent key must come from the two REFERENCED open mandates, not from an + * unreferenced or non-mandate disclosure + * - L3 payloads MUST NOT carry `cnf` (chain.ts ~L576, terminal delegation) + * - an in-memory L1 `cnf` swap is caught by the issuer signature + * + * These mirror the Python reference tests in + * `python/tests/test_verification_hardening.py` + * (TestDualMandateCnfCrossCheck, TestMalformedJwk, TestL3CnfRejection, + * TestMutationDetection.test_mutated_cnf_rejected_by_chain). Chains are issued + * fresh in-memory with a deterministic salt source and a fixed `currentTime`; + * no network, no golden vectors. + * + * NOTE: one genuine TS↔Python divergence is documented below as `it.fails` + * (L1 cnf.jwk with syntactically-present but undecodable base64 coordinates). + */ + +import { describe, expect, it } from 'vitest'; + +import { + AllowedMerchantConstraint, + buildSelectivePresentation, + CheckoutL3Mandate, + CheckoutLineItemsConstraint, + CheckoutMandate, + createDisclosure, + createLayer1, + createLayer2Autonomous, + createLayer2Immediate, + createLayer3Checkout, + createLayer3Payment, + createSdJwt, + type DecodedDisclosure, + decodeDisclosure, + decodeSdJwt, + type Es256Jwk, + FinalCheckoutMandate, + FinalPaymentMandate, + generateEs256Key, + hashAscii, + IssuerCredential, + type JsonObject, + jwtEncode, + makeSigner, + MandateMode, + PaymentAmountConstraint, + PaymentL3Mandate, + PaymentMandate, + type SdJwt, + UserMandate, + verifyChain, + type VerifyChainOptions, +} from '../src/index.js'; + +// --- Fixtures (ported from python/examples/helpers.py) -------------------- + +const MERCHANTS: JsonObject[] = [ + { id: 'merchant-uuid-1', name: 'Tennis Warehouse', website: 'https://tennis-warehouse.com' }, + { id: 'merchant-uuid-2', name: 'Babolat', website: 'https://babolat.com' }, +]; +const ACCEPTABLE_ITEMS: JsonObject[] = [ + { id: 'BAB86345', title: 'Babolat Pure Aero Tennis Racket' }, + { id: 'HEA23102', title: 'Head Graphene 360 Speed' }, +]; +const PAYMENT_INSTRUMENT: JsonObject = { + type: 'mastercard.srcDigitalCard', + id: 'f199c3dd-7106-478b-9b5f-7af9ca725170', + description: 'Mastercard **** 1234', +}; + +/** Fixed clock so every issued credential's iat/exp verifies deterministically. */ +const NOW = 1_700_000_000; + +type KeyPair = { publicKey: Es256Jwk; privateKey: Es256Jwk }; +const jwkOf = (k: KeyPair): JsonObject => k.publicKey as unknown as JsonObject; + +/** Monotonic FIFO salt source → unique, reproducible disclosures (no RNG). */ +function deterministicSalts(): () => string { + let n = 0; + return () => `salt${(n++).toString().padStart(22, '0')}`; +} + +/** Find an L2 disclosure whose decoded value satisfies `pred` (parity with helpers._find_disclosure). */ +function findDisclosure(sd: SdJwt, pred: (v: JsonObject) => boolean): string { + for (const disc of sd.disclosures) { + const dv: DecodedDisclosure = decodeDisclosure(disc); + const value = dv.length ? dv[dv.length - 1] : null; + if (value && typeof value === 'object' && !Array.isArray(value) && pred(value as JsonObject)) return disc; + } + throw new Error('expected disclosure not found'); +} + +async function merchantCheckoutJwt(merchant: KeyPair): Promise { + const signer = await makeSigner(merchant.privateKey); + return jwtEncode( + { alg: 'ES256', typ: 'JWT', kid: 'merchant-key-1' }, + { iss: 'https://tennis-warehouse.com', sub: 'cart_checkout', iat: NOW, exp: NOW + 3600 }, + signer, + ); +} + +interface ChainOptions { + /** Omit the agent cnf.jwk from the open checkout mandate. */ + includeCheckoutCnf?: boolean; + /** Omit the agent cnf.jwk from the open payment mandate. */ + includePaymentCnf?: boolean; + /** Override the checkout mandate's cnf.jwk (defaults to the agent key). */ + checkoutCnfJwk?: JsonObject; + /** Override the payment mandate's cnf.jwk (defaults to the agent key). */ + paymentCnfJwk?: JsonObject; +} + +interface Chain { + issuer: KeyPair; + user: KeyPair; + agent: KeyPair; + l1: SdJwt; + l1Ser: string; + l2: SdJwt; + l2Ser: string; + l3a: SdJwt; + l3b: SdJwt; + l2PaymentSer: string; + l2CheckoutSer: string; +} + +/** Build a valid autonomous 3-layer chain (parity with helpers `_make_autonomous_chain`). */ +async function makeAutonomousChain(opts: ChainOptions = {}): Promise { + const salts = deterministicSalts(); + const io = { saltSource: salts }; + + const issuer = await generateEs256Key(); + const user = await generateEs256Key(); + const agent = await generateEs256Key(); + const merchant = await generateEs256Key(); + + const l1 = await createLayer1( + new IssuerCredential({ + iss: 'https://www.mastercard.com', + sub: 'userCredentialId', + iat: NOW, + exp: NOW + 86400, + aud: 'https://wallet.example.com', + email: 'test@example.com', + panLastFour: '1234', + scheme: 'Mastercard', + cnfJwk: jwkOf(user), + }), + issuer.privateKey, + io, + ); + const l1Ser = l1.serialize(); + + const checkoutCnf = opts.includeCheckoutCnf === false ? null : (opts.checkoutCnfJwk ?? jwkOf(agent)); + const paymentCnf = opts.includePaymentCnf === false ? null : (opts.paymentCnfJwk ?? jwkOf(agent)); + + const checkoutMandate = new CheckoutMandate({ + vct: 'mandate.checkout.open.1', + cnfJwk: checkoutCnf, + cnfKid: checkoutCnf ? 'agent-key-1' : null, + constraints: [ + new AllowedMerchantConstraint({ allowed: MERCHANTS }), + new CheckoutLineItemsConstraint({ + items: [{ id: 'line-item-1', acceptable_items: ACCEPTABLE_ITEMS.slice(0, 1), quantity: 1 }], + }), + ], + }); + const paymentMandate = new PaymentMandate({ + vct: 'mandate.payment.open.1', + cnfJwk: paymentCnf, + cnfKid: paymentCnf ? 'agent-key-1' : null, + paymentInstrument: PAYMENT_INSTRUMENT, + constraints: [new PaymentAmountConstraint({ currency: 'USD', min: 10000, max: 40000 })], + }); + const userMandate = new UserMandate({ + nonce: 'n-auto', + aud: 'https://www.agent.com', + iat: NOW, + iss: 'https://wallet.example.com', + exp: NOW + 86400, + mode: MandateMode.AUTONOMOUS, + sdHash: await hashAscii(l1Ser), + checkoutMandate, + paymentMandate, + merchants: MERCHANTS, + acceptableItems: ACCEPTABLE_ITEMS, + }); + const l2 = await createLayer2Autonomous(userMandate, user.privateKey, io); + const l2Ser = l2.serialize(); + const l2BaseJwt = l2Ser.split('~')[0] ?? ''; + + const paymentDisc = findDisclosure(l2, (v) => v.vct === 'mandate.payment.open.1'); + const checkoutDisc = findDisclosure(l2, (v) => v.vct === 'mandate.checkout.open.1'); + const merchantDisc = findDisclosure(l2, (v) => v.name === 'Tennis Warehouse'); + const itemDisc = findDisclosure(l2, (v) => v.id === 'BAB86345'); + + const checkoutJwt = await merchantCheckoutJwt(merchant); + const cHash = await hashAscii(checkoutJwt); + + const l3a = await createLayer3Payment( + new PaymentL3Mandate({ + nonce: 'n-l3a', + aud: 'https://www.mastercard.com', + iat: NOW, + iss: 'https://agent.example.com', + exp: NOW + 300, + finalPayment: new FinalPaymentMandate({ + transactionId: cHash, + payee: MERCHANTS[0], + paymentAmount: { currency: 'USD', amount: 27999 }, + paymentInstrument: PAYMENT_INSTRUMENT, + }), + finalMerchant: MERCHANTS[0], + }), + agent.privateKey, + { l2BaseJwt, paymentDisclosure: paymentDisc, merchantDisclosure: merchantDisc }, + io, + ); + + const l3b = await createLayer3Checkout( + new CheckoutL3Mandate({ + nonce: 'n-l3b', + aud: 'https://tennis-warehouse.com', + iat: NOW, + iss: 'https://agent.example.com', + exp: NOW + 300, + finalCheckout: new FinalCheckoutMandate({ checkoutJwt, checkoutHash: cHash }), + }), + agent.privateKey, + { l2BaseJwt, checkoutDisclosure: checkoutDisc, itemDisclosure: itemDisc }, + io, + ); + + const l2PaymentSer = buildSelectivePresentation(l2BaseJwt, [paymentDisc, merchantDisc]); + const l2CheckoutSer = buildSelectivePresentation(l2BaseJwt, [checkoutDisc, itemDisc]); + + return { issuer, user, agent, l1, l1Ser, l2, l2Ser, l3a, l3b, l2PaymentSer, l2CheckoutSer }; +} + +/** verifyChain opts for the full split-L3 autonomous chain, with the issuer key present. */ +function fullVerifyOpts(c: Chain, l3Payment: SdJwt | null, l3Checkout: SdJwt | null): VerifyChainOptions { + return { + splitL3s: [ + { + l3Payment, + l3Checkout, + l2PaymentSerialized: c.l2PaymentSer, + l2CheckoutSerialized: c.l2CheckoutSer, + }, + ], + issuerPublicJwk: c.issuer.publicKey, + l1Serialized: c.l1Ser, + l2Serialized: c.l2Ser, + currentTime: NOW, + }; +} + +/** Re-sign an L3 with an injected `cnf` claim (parity with the Python create_sd_jwt tamper). */ +async function withInjectedCnf(l3: SdJwt, agent: KeyPair): Promise { + const bad = { ...l3.payload, cnf: { jwk: jwkOf(agent) } }; + return createSdJwt(l3.header, bad, l3.disclosures, agent.privateKey); +} + +/** Build a minimal L1/L2 with an arbitrary L1 `cnf` value, for cnf-shape tests. */ +async function buildL1L2WithCnf(cnf: unknown): Promise<{ l1: SdJwt; l2: SdJwt }> { + const salts = deterministicSalts(); + const issuer = await generateEs256Key(); + const user = await generateEs256Key(); + const l1 = await createSdJwt( + { alg: 'ES256', typ: 'sd+jwt' }, + { + iss: 'https://www.mastercard.com', + sub: 'test', + iat: NOW, + exp: NOW + 86400, + vct: 'https://credentials.mastercard.com/card', + pan_last_four: '1234', + scheme: 'Mastercard', + cnf, + }, + [], + issuer.privateKey, + ); + const l2 = await createSdJwt( + { alg: 'ES256', typ: 'kb-sd-jwt' }, + { nonce: salts(), aud: 'test', iat: NOW, sd_hash: await hashAscii(l1.serialize()), delegate_payload: [] }, + [], + user.privateKey, + ); + return { l1, l2 }; +} + +describe('cnf hardening: baseline', () => { + it('a fresh valid autonomous chain verifies (guards the test harness itself)', async () => { + const c = await makeAutonomousChain(); + const res = await verifyChain(c.l1, c.l2, fullVerifyOpts(c, c.l3a, c.l3b)); + expect(res.valid).toBe(true); + expect(res.errors).toEqual([]); + }); +}); + +describe('cnf hardening: L1 cnf shape validation (chain.ts §3)', () => { + it('rejects a non-object L1 cnf', async () => { + const { l1, l2 } = await buildL1L2WithCnf('not-a-json-object'); + const res = await verifyChain(l1, l2, { skipIssuerVerification: true, currentTime: NOW }); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.includes('L1 cnf must be a JSON object'))).toBe(true); + }); + + it('rejects an L1 with an empty cnf (no cnf.jwk)', async () => { + const { l1, l2 } = await buildL1L2WithCnf({}); + const res = await verifyChain(l1, l2, { skipIssuerVerification: true, currentTime: NOW }); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.includes('L1 missing cnf.jwk (user public key)'))).toBe(true); + }); + + it('rejects an L1 cnf.jwk missing the x coordinate (malformed, not a crash)', async () => { + const user = await generateEs256Key(); + const y = (user.publicKey as unknown as JsonObject).y; + const { l1, l2 } = await buildL1L2WithCnf({ jwk: { kty: 'EC', crv: 'P-256', y } }); + const res = await verifyChain(l1, l2, { skipIssuerVerification: true, currentTime: NOW }); + expect(res.valid).toBe(false); + // Python: "L1 cnf.jwk is malformed: 'x'"; TS: "...: missing x/y coordinate" — both carry "malformed". + expect(res.errors.some((e) => e.toLowerCase().includes('malformed'))).toBe(true); + }); + + // DIVERGENCE (error string only; accept/reject verdict is identical): + // Python's jwk_to_public_key eagerly decodes the EC point at cnf-extraction + // time, so syntactically-present-but-undecodable base64 coordinates are + // reported as "L1 cnf.jwk is malformed: Invalid EC key. Point is not on the + // curve specified." TS's validateEcPublicJwk only checks that x/y are + // non-empty strings (it never decodes them), so a bad-base64 key slips past + // the "malformed" guard and instead fails one step later at the L2 signature + // check with "L2 signature verification failed (user key mismatch)". Both + // still return valid=false. This assertion is written to Python's behavior; + // it.fails records that TS does not match the error string byte-for-byte. + it.fails('DIVERGENCE: L1 cnf.jwk with bad-base64 coordinates → Python says "malformed", TS does not', async () => { + const { l1, l2 } = await buildL1L2WithCnf({ jwk: { kty: 'EC', crv: 'P-256', x: '!!!invalid!!!', y: '!!!bad!!!' } }); + const res = await verifyChain(l1, l2, { skipIssuerVerification: true, currentTime: NOW }); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.toLowerCase().includes('malformed'))).toBe(true); + }); +}); + +describe('cnf hardening: agent delegation key extraction (chain.ts §4d)', () => { + it('rejects mismatched agent cnf.jwk across the checkout/payment mandates', async () => { + // Checkout keeps the agent key; payment carries a DIFFERENT key. The chain + // must refuse to guess which is the real agent, not accept either. + const other = await generateEs256Key(); + const c = await makeAutonomousChain({ paymentCnfJwk: jwkOf(other) }); + const res = await verifyChain(c.l1, c.l2, fullVerifyOpts(c, c.l3a, c.l3b)); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.includes('identical') || e.includes('differ'))).toBe(true); + }); + + it('rejects a disclosed open mandate that is missing its cnf.jwk', async () => { + // Both open mandates are disclosed; the payment one drops cnf.jwk. The agent + // key must be present on every disclosed open mandate, not just one. + const c = await makeAutonomousChain({ includePaymentCnf: false }); + const res = await verifyChain(c.l1, c.l2, { + l3Payment: c.l3a, + issuerPublicJwk: c.issuer.publicKey, + l1Serialized: c.l1Ser, + l2PaymentSerialized: c.l2PaymentSer, + currentTime: NOW, + }); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.toLowerCase().includes('payment open mandate missing cnf.jwk'))).toBe(true); + }); + + it('rejects a cnf.jwk sourced from an UNREFERENCED extra disclosure', async () => { + // Neither real open mandate carries cnf; an extra mandate-shaped disclosure + // with cnf is appended to the L2 but never referenced by delegate_payload. + // It must not satisfy agent-key extraction. + const c = await makeAutonomousChain({ includeCheckoutCnf: false, includePaymentCnf: false }); + const fake = await createDisclosure(null, { vct: 'mandate.checkout.open.1', cnf: { jwk: jwkOf(c.agent) } }); + const tamperedL2 = decodeSdJwt(`${c.l2Ser.slice(0, -1)}~${fake}~`); + const res = await verifyChain(c.l1, tamperedL2, { + l3Payment: c.l3a, + issuerPublicJwk: c.issuer.publicKey, + l1Serialized: c.l1Ser, + l2PaymentSerialized: c.l2PaymentSer, + currentTime: NOW, + }); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.toLowerCase().includes('missing cnf.jwk'))).toBe(true); + }); + + it('rejects a cnf.jwk sourced from a NON-mandate (merchant-shaped) disclosure', async () => { + // Same, but the injected disclosure is a merchant/payee object carrying cnf. + // A cnf on a non-mandate disclosure must not be used for delegation. + const c = await makeAutonomousChain({ includeCheckoutCnf: false, includePaymentCnf: false }); + const fake = await createDisclosure(null, { + id: 'merchant-injected', + name: 'Injected Merchant', + website: 'https://example.invalid', + cnf: { jwk: jwkOf(c.agent) }, + }); + const tamperedL2 = decodeSdJwt(`${c.l2Ser.slice(0, -1)}~${fake}~`); + const res = await verifyChain(c.l1, tamperedL2, { + l3Payment: c.l3a, + issuerPublicJwk: c.issuer.publicKey, + l1Serialized: c.l1Ser, + l2PaymentSerialized: c.l2PaymentSer, + currentTime: NOW, + }); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.toLowerCase().includes('missing cnf.jwk'))).toBe(true); + }); +}); + +describe('cnf hardening: L3 payloads MUST NOT carry cnf (chain.ts ~L576)', () => { + it('rejects an L3 payment (L3a) that carries a cnf claim', async () => { + const c = await makeAutonomousChain(); + const l3aTampered = await withInjectedCnf(c.l3a, c.agent); + const res = await verifyChain(c.l1, c.l2, fullVerifyOpts(c, l3aTampered, c.l3b)); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.includes('MUST NOT contain cnf'))).toBe(true); + }); + + it('rejects an L3 checkout (L3b) that carries a cnf claim', async () => { + const c = await makeAutonomousChain(); + const l3bTampered = await withInjectedCnf(c.l3b, c.agent); + const res = await verifyChain(c.l1, c.l2, fullVerifyOpts(c, c.l3a, l3bTampered)); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.includes('MUST NOT contain cnf'))).toBe(true); + }); +}); + +describe('cnf hardening: in-memory L1 cnf swap is caught by the issuer signature', () => { + it('rejects an L1 whose cnf.jwk was swapped to an attacker key after issuance', async () => { + // Immediate-mode chain; the issuer key IS provided (no skip), so mutating + // the signed L1 payload must break the L1 signature — the attacker cannot + // rebind the credential to their own key. + const salts = deterministicSalts(); + const io = { saltSource: salts }; + const issuer = await generateEs256Key(); + const user = await generateEs256Key(); + const attacker = await generateEs256Key(); + const merchant = await generateEs256Key(); + + const l1 = await createLayer1( + new IssuerCredential({ + iss: 'https://www.mastercard.com', + sub: 'userCredentialId', + iat: NOW, + exp: NOW + 86400, + aud: 'https://wallet.example.com', + email: 'test@example.com', + panLastFour: '1234', + scheme: 'Mastercard', + cnfJwk: jwkOf(user), + }), + issuer.privateKey, + io, + ); + + const checkoutJwt = await merchantCheckoutJwt(merchant); + const cHash = await hashAscii(checkoutJwt); + const userMandate = new UserMandate({ + nonce: 'n-imm', + aud: 'https://www.agent.com', + iat: NOW, + iss: 'https://wallet.example.com', + exp: NOW + 900, + mode: MandateMode.IMMEDIATE, + sdHash: await hashAscii(l1.serialize()), + checkoutMandate: new CheckoutMandate({ vct: 'mandate.checkout.1', checkoutJwt }), + paymentMandate: new PaymentMandate({ + vct: 'mandate.payment.1', + currency: 'USD', + amount: 27999, + payee: MERCHANTS[0], + paymentInstrument: PAYMENT_INSTRUMENT, + transactionId: cHash, + }), + }); + const l2 = (await createLayer2Immediate(userMandate, user.privateKey, io)).sdJwt; + + // Attacker swaps the confirmation key in the decoded L1 payload. + (l1.payload as JsonObject).cnf = { jwk: jwkOf(attacker) }; + + const res = await verifyChain(l1, l2, { issuerPublicJwk: issuer.publicKey, currentTime: NOW }); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.includes('L1 signature'))).toBe(true); + }); +}); diff --git a/typescript/test/crypto-parity.test.ts b/typescript/test/crypto-parity.test.ts new file mode 100644 index 0000000..ebbb959 --- /dev/null +++ b/typescript/test/crypto-parity.test.ts @@ -0,0 +1,237 @@ +/** + * Crypto-layer Python-parity fixes: locks in a set of just-applied fixes to the + * TypeScript port so the SDK rejects (or accepts) exactly what the authoritative + * Python reference does. Each `describe` maps to one fix; fixtures marked + * "Python-verified" were confirmed against the Python implementation out of band. + * + * These tests never invoke Python — they assert the TS behavior that must match. + */ + +import { describe, expect, it } from 'vitest'; + +import { + asciiBytes, + b64urlDecode, + b64urlEncode, + type DecodedDisclosure, + decodeDisclosure, + decodeSdJwt, + type Es256Jwk, + hashDisclosure, + jwtDecodeParts, + resolveDisclosures, + SdJwt, + utf8, + verifyCheckoutHashBinding, + verifySdJwtSignature, +} from '../src/index.js'; +// py-json is deliberately NOT re-exported from the public barrel; import direct. +import { parsePySegment, pyDeepEqual } from '../src/crypto/py-json.js'; + +// A non-ASCII string built from escapes so this source file stays ASCII (as the +// library sources deliberately are); at runtime it holds an actual "café". +const CAFE = 'café'; + +describe('b64urlDecode hardening (parity with Python urlsafe_b64decode)', () => { + it('throws on length % 4 === 1 (an impossible base64 length)', () => { + // 'AAAAA' is 5 chars, 5 % 4 === 1 — Python raises binascii.Error. + expect(() => b64urlDecode('AAAAA')).toThrow(); + }); + + it('throws on non-ASCII input (Python .encode("ascii") raises)', () => { + // length 4 (so the length guard does not fire first) but contains a + // non-ASCII code point, which Python rejects and Node would silently skip. + expect(() => b64urlDecode(CAFE)).toThrow(); + }); + + it('round-trips a valid encoded string', () => { + const bytes = new Uint8Array([1, 2, 3, 4, 5]); + expect(Array.from(b64urlDecode(b64urlEncode(bytes)))).toEqual([1, 2, 3, 4, 5]); + }); +}); + +describe('decodeDisclosure hardening', () => { + it('throws on a length % 4 === 1 disclosure (Python raises binascii.Error)', () => { + // 37 chars, 37 % 4 === 1. + expect(() => decodeDisclosure('WyJzYWx0MTIzNDU2NzgiLCJ4bmFtIiwidiJdA')).toThrow(); + }); + + it('still decodes a valid disclosure', () => { + // ["salt-aaaa-1234","vct","AAA"] + expect(decodeDisclosure('WyJzYWx0LWFhYWEtMTIzNCIsInZjdCIsIkFBQSJd')).toEqual(['salt-aaaa-1234', 'vct', 'AAA']); + }); +}); + +describe('jwtDecodeParts hardening', () => { + it('throws when the payload segment has length % 4 === 1', () => { + const header = b64urlEncode(utf8('{"alg":"ES256","typ":"JWT"}')); + // Payload segment 'AAAAA' is 5 chars (5 % 4 === 1); header decodes fine + // first, then the payload decode must reject. + const token = `${header}.AAAAA.AA`; + expect(() => jwtDecodeParts(token)).toThrow(); + }); +}); + +describe('asciiBytes / non-ASCII bindings (parity with Python str.encode("ascii"))', () => { + it('asciiBytes throws on non-ASCII', () => { + expect(() => asciiBytes(CAFE)).toThrow(); + }); + + it('hashDisclosure throws on a non-ASCII disclosure string', () => { + expect(() => hashDisclosure(CAFE)).toThrow(); + }); + + it('encodes a plain ASCII string', () => { + expect(Array.from(asciiBytes('AB'))).toEqual([0x41, 0x42]); + }); + + it('verifyCheckoutHashBinding PROPAGATES on a non-ASCII checkout_jwt (matches Python UnicodeEncodeError)', () => { + // Previously returned a verdict; the fix lets asciiBytes' error propagate, + // matching Python's uncaught UnicodeEncodeError rather than failing open. + expect(() => verifyCheckoutHashBinding({ checkout_jwt: CAFE, checkout_hash: 'x' }, {})).toThrow(); + }); +}); + +describe('SdJwt.serialize — Python list-index semantics', () => { + // Deterministic issuer JWT via raw segments + a fixed 3-byte signature. + // b64urlEncode([1,2,3]) === 'AQID', so issuerJwt === 'hdr.pl.AQID'. + function makeSdJwt(): SdJwt { + return new SdJwt({ + header: {}, + payload: {}, + signature: new Uint8Array([1, 2, 3]), + disclosures: ['d0', 'd1', 'd2'], + rawHeaderB64: 'hdr', + rawPayloadB64: 'pl', + }); + } + + it('negative indices wrap: serialize([-1]) picks the last disclosure', () => { + expect(makeSdJwt().serialize([-1])).toBe('hdr.pl.AQID~d2~'); + }); + + it('serializes selected indices in the given order', () => { + expect(makeSdJwt().serialize([0, 2])).toBe('hdr.pl.AQID~d0~d2~'); + }); + + it('serialize() with no argument includes every disclosure', () => { + expect(makeSdJwt().serialize()).toBe('hdr.pl.AQID~d0~d1~d2~'); + }); + + it('out-of-range positive index throws RangeError (Python raises IndexError)', () => { + expect(() => makeSdJwt().serialize([3])).toThrow(RangeError); + }); + + it('out-of-range negative index throws RangeError', () => { + expect(() => makeSdJwt().serialize([-4])).toThrow(RangeError); + }); +}); + +describe('resolveDisclosures — presentation-order resolution (last PRESENTED wins)', () => { + it('resolves the claim to the last presented disclosure, not the last _sd entry', async () => { + // dA/dB both disclose claim `vct`. `_sd` lists [hash(dA), hash(dB)] but the + // disclosures are PRESENTED as [dB, dA]. Python's zip(disclosures, values) + // loop applies them in presentation order, so dA (last presented) wins → 'AAA'. + // Resolving by `_sd` order instead would wrongly yield 'BBB'. + const dA = 'WyJzYWx0LWFhYWEtMTIzNCIsInZjdCIsIkFBQSJd'; // [salt-aaaa-1234, vct, AAA] + const dB = 'WyJzYWx0LWJiYmItNTY3OCIsInZjdCIsIkJCQiJd'; // [salt-bbbb-5678, vct, BBB] + + const sdJwt = new SdJwt({ + header: {}, + payload: { _sd: [await hashDisclosure(dA), await hashDisclosure(dB)] }, + signature: new Uint8Array(64), + disclosures: [dB, dA], + disclosureValues: [decodeDisclosure(dB), decodeDisclosure(dA)] as DecodedDisclosure[], + }); + + expect((await resolveDisclosures(sdJwt)).vct).toBe('AAA'); + }); +}); + +describe('verifySdJwtSignature — Python-number parity', () => { + // Real Python-signed fixture. Payload holds 1.0, a big int beyond 2^53, and + // 1e-05 — number spellings that a naive JS re-encode would mangle, causing a + // false rejection of a valid credential. + const SERIALIZED = + 'eyJhbGciOiJFUzI1NiIsInR5cCI6InRlc3QifQ.eyJpc3MiOiJ0ZXN0IiwiYW1vdW50IjoxLjAsImJpZyI6OTAwNzE5OTI1NDc0MDk5MywidGlueSI6MWUtMDUsInBsYWluIjoyNzkuOTl9.6amVMsxxixBc_vZJFqqkcbtQqIgs5PCUWxbm_YTfLax6YnhK9JG95DVo25V8Cn3OTdppZs0YVA86tkfKIgQTXg~'; + const PUBLIC_JWK: Es256Jwk = { + kty: 'EC', + crv: 'P-256', + x: 'PWZ7juBr2AJA-6S76KenML2WIQX3nf9aJ4ArD3eF6cU', + y: 'a6YQP3NuxdS5WgXDG7kB5zkb6vXKrIGXKPSjHM07kHU', + }; + + it('verifies a valid Python-signed credential with tricky number spellings', async () => { + const sd = decodeSdJwt(SERIALIZED); + expect(await verifySdJwtSignature(sd, PUBLIC_JWK)).toBe(true); + }); + + it('rejects a mutated numeric claim (amount → 2)', async () => { + const sd = decodeSdJwt(SERIALIZED); + sd.payload.amount = 2; + expect(await verifySdJwtSignature(sd, PUBLIC_JWK)).toBe(false); + }); + + it('rejects a mutated string claim (iss → evil)', async () => { + const sd = decodeSdJwt(SERIALIZED); + sd.payload.iss = 'evil'; + expect(await verifySdJwtSignature(sd, PUBLIC_JWK)).toBe(false); + }); + + it('rejects reordered payload keys (Python dumps preserves insertion order)', async () => { + const sd = decodeSdJwt(SERIALIZED); + const { iss, ...rest } = sd.payload; + sd.payload = { ...rest, iss }; + expect(await verifySdJwtSignature(sd, PUBLIC_JWK)).toBe(false); + }); +}); + +describe('py-json internals — parsePySegment.pyText matches Python json.dumps', () => { + // Each pair is [raw JSON segment, Python json.dumps(json.loads(raw), + // separators=(",",":"))]. All Python-verified. + const cases: Array<[string, string]> = [ + ['1.0', '1.0'], + ['1.00', '1.0'], + ['-0.0', '-0.0'], + ['1e2', '100.0'], + ['1e16', '1e+16'], + ['0.00001', '1e-05'], + ['1e-4', '0.0001'], + ['-0', '0'], + ['9007199254740993', '9007199254740993'], + ['123456789012345678901234567890', '123456789012345678901234567890'], + ['5e-324', '5e-324'], + ['1.7976931348623157e308', '1.7976931348623157e+308'], + ['279.99', '279.99'], + // Python ensure_ascii escapes every non-ASCII code point as \uXXXX. + ['{"s":"literal café 中"}', '{"s":"literal caf\\u00e9 \\u4e2d"}'], + // JSON duplicate keys: last value wins. + ['{"dup":1,"dup":2.5}', '{"dup":2.5}'], + ]; + + for (const [raw, expected] of cases) { + it(`${JSON.stringify(raw)} -> ${JSON.stringify(expected)}`, () => { + const seg = parsePySegment(raw); + expect(seg).not.toBeNull(); + expect(seg?.pyText).toBe(expected); + }); + } +}); + +describe('py-json internals — pyDeepEqual (Object.is + order-sensitive)', () => { + it('equal trees compare true', () => { + expect(pyDeepEqual({ a: 1, b: [2, 3], c: 'x' }, { a: 1, b: [2, 3], c: 'x' })).toBe(true); + }); + + it('-0 vs 0 is FALSE (Object.is semantics)', () => { + expect(pyDeepEqual(-0, 0)).toBe(false); + }); + + it('reordered object keys are FALSE', () => { + expect(pyDeepEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(false); + }); + + it('nested value mismatch is FALSE', () => { + expect(pyDeepEqual({ a: { b: 1 } }, { a: { b: 2 } })).toBe(false); + }); +}); diff --git a/typescript/test/isomorphic.test.ts b/typescript/test/isomorphic.test.ts new file mode 100644 index 0000000..3b1aadf --- /dev/null +++ b/typescript/test/isomorphic.test.ts @@ -0,0 +1,66 @@ +/** + * Browser-readiness proof: the whole issue → verify pipeline must run with no + * Node-only globals. `globalThis.Buffer` is deleted for the duration of the + * test (and restored afterwards) so any accidental Buffer dependency in the + * library would throw here. The package now relies only on Web-standard + * globals — `crypto.subtle`, `crypto.getRandomValues`, `TextEncoder`, + * `TextDecoder` — which exist in Node >= 20 and modern browsers alike. + */ + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { + createLayer1, + decodeSdJwt, + generateEs256Key, + hashAscii, + IssuerCredential, + resolveDisclosures, + verifySdJwtSignature, +} from '../src/index.js'; + +const g = globalThis as { Buffer?: unknown }; + +describe('isomorphic runtime (no Node Buffer global)', () => { + let savedBuffer: unknown; + + beforeEach(() => { + savedBuffer = g.Buffer; + delete g.Buffer; + }); + + afterEach(() => { + g.Buffer = savedBuffer; + }); + + it('issues and verifies an L1 credential with Buffer undefined', async () => { + expect(g.Buffer).toBeUndefined(); + + const now = Math.floor(Date.now() / 1000); + const issuer = await generateEs256Key(); + const user = await generateEs256Key(); + + const l1 = await createLayer1( + new IssuerCredential({ + iss: 'https://www.mastercard.com', + sub: 'user-123', + iat: now, + exp: now + 3600, + cnfJwk: user.publicKey, + email: 'user@example.com', + panLastFour: '1234', + scheme: 'Mastercard', + }), + issuer.privateKey, + ); + + const parsed = decodeSdJwt(l1.serialize()); + expect(await verifySdJwtSignature(parsed, issuer.publicKey)).toBe(true); + + const resolved = await resolveDisclosures(parsed); + expect(resolved.email).toBe('user@example.com'); + + // Hashing path (sd_hash / checkout_hash primitive) works without Buffer too. + expect(await hashAscii(l1.serialize())).toMatch(/^[A-Za-z0-9_-]{43}$/); + }); +}); diff --git a/typescript/test/network-enforced.test.ts b/typescript/test/network-enforced.test.ts new file mode 100644 index 0000000..7ab6dfc --- /dev/null +++ b/typescript/test/network-enforced.test.ts @@ -0,0 +1,185 @@ +/** + * `ChainVerificationResult.networkEnforced` surfacing. + * + * The spec assigns `mandate.payment.budget` / `.recurrence` / `.agent_recurrence` + * to the payment NETWORK (they are stateful), so the stateless verifier parses + * but never evaluates them — the constraint checker only records them in + * `checked[]`. `verifyChain` additionally surfaces them on the result so the + * caller knows what it still must enforce. These tests pin that surfacing: + * additive only — verdicts, errors, and all other result fields are unchanged. + */ + +import { randomUUID } from 'node:crypto'; + +import { describe, expect, it } from 'vitest'; + +import { + AllowedPayeeConstraint, + CheckoutLineItemsConstraint, + CheckoutMandate, + createLayer1, + createLayer2Autonomous, + createLayer2Immediate, + generateEs256Key, + hashAscii, + IssuerCredential, + type JsonObject, + jwtEncode, + makeSigner, + MandateMode, + PaymentBudgetConstraint, + PaymentMandate, + PaymentRecurrenceConstraint, + UserMandate, + verifyChain, +} from '../src/index.js'; + +const TENNIS_WAREHOUSE = { id: 'merchant-uuid-1', name: 'Tennis Warehouse', website: 'https://tennis-warehouse.com' }; + +const PAYMENT_INSTRUMENT = { + type: 'mastercard.srcDigitalCard', + id: 'f199c3dd-7106-478b-9b5f-7af9ca725170', + description: 'Mastercard **** 1234', +}; + +const L2_AUD = 'https://agent.verifiable-intent.example'; + +async function makeCheckoutJwt(merchantPrivateJwk: Parameters[0], now: number): Promise { + const signer = await makeSigner(merchantPrivateJwk); + const payload = { + iss: 'https://tennis-warehouse.com', + sub: 'cart_checkout', + iat: now, + exp: now + 3600, + cart: { items: [{ sku: 'BAB86345', quantity: 1 }], subTotal: { amount: 279.99, currencyCode: 'USD' } }, + }; + return jwtEncode({ alg: 'ES256', typ: 'JWT', kid: 'merchant-key-1' }, payload, signer); +} + +async function makeL1(issuer: Awaited>, userCnfJwk: JsonObject, now: number) { + const cred = new IssuerCredential({ + iss: 'https://www.mastercard.com', + sub: 'user-alice-001', + iat: now, + exp: now + 86400, + aud: 'https://wallet.example.com', + cnfJwk: userCnfJwk, + email: 'alice@example.com', + panLastFour: '1234', + scheme: 'Mastercard', + }); + return createLayer1(cred, issuer.privateKey); +} + +describe('networkEnforced surfacing', () => { + it('surfaces budget/recurrence constraints from an autonomous open payment mandate', async () => { + const now = Math.floor(Date.now() / 1000); + const issuer = await generateEs256Key(); + const user = await generateEs256Key(); + const agent = await generateEs256Key(); + + const l1 = await makeL1(issuer, user.publicKey, now); + + const l2Nonce = randomUUID(); + const mandate = new UserMandate({ + nonce: l2Nonce, + aud: L2_AUD, + iat: now, + iss: 'https://wallet.example.com', + exp: now + 86400, + mode: MandateMode.AUTONOMOUS, + sdHash: await hashAscii(l1.serialize()), + promptSummary: 'Buy tennis gear, $400/month budget', + checkoutMandate: new CheckoutMandate({ + vct: 'mandate.checkout.open.1', + cnfJwk: agent.publicKey, + cnfKid: 'agent-key-1', + constraints: [new CheckoutLineItemsConstraint({ items: [] })], + }), + paymentMandate: new PaymentMandate({ + vct: 'mandate.payment.open.1', + cnfJwk: agent.publicKey, + cnfKid: 'agent-key-1', + paymentInstrument: PAYMENT_INSTRUMENT, + constraints: [ + new AllowedPayeeConstraint({ allowed: [TENNIS_WAREHOUSE] }), + new PaymentBudgetConstraint({ currency: 'USD', max: 40000 }), + new PaymentRecurrenceConstraint({ frequency: 'monthly', startDate: '2026-01-01' }), + ], + }), + merchants: [TENNIS_WAREHOUSE], + acceptableItems: [], + }); + const l2 = await createLayer2Autonomous(mandate, user.privateKey); + + const res = await verifyChain(l1, l2, { + issuerPublicJwk: issuer.publicKey, + l1Serialized: l1.serialize(), + expectedL2Aud: L2_AUD, + expectedL2Nonce: l2Nonce, + }); + + // The network-enforced constraints do not affect the stateless verdict. + expect(res.valid).toBe(true); + expect(res.errors).toEqual([]); + + // Exactly the budget and recurrence constraints are surfaced — the + // verifier-enforced allowed_payees and the auto-injected reference are not. + expect(res.networkEnforced.length).toBe(2); + + const budget = res.networkEnforced.find((c) => c.type === 'mandate.payment.budget'); + expect(budget).toBeDefined(); + expect(budget?.pairIndex).toBe(0); + expect(budget?.constraint).toEqual({ type: 'mandate.payment.budget', currency: 'USD', max: 40000 }); + + const recurrence = res.networkEnforced.find((c) => c.type === 'mandate.payment.recurrence'); + expect(recurrence).toBeDefined(); + expect(recurrence?.pairIndex).toBe(0); + expect(recurrence?.constraint).toEqual({ + type: 'mandate.payment.recurrence', + frequency: 'monthly', + start_date: '2026-01-01', + }); + }); + + it('is an empty array for an immediate chain', async () => { + const now = Math.floor(Date.now() / 1000); + const issuer = await generateEs256Key(); + const user = await generateEs256Key(); + const merchant = await generateEs256Key(); + + const l1 = await makeL1(issuer, user.publicKey, now); + + const checkoutJwt = await makeCheckoutJwt(merchant.privateKey, now); + const l2Nonce = randomUUID(); + const mandate = new UserMandate({ + nonce: l2Nonce, + aud: L2_AUD, + iat: now, + iss: 'https://wallet.example.com', + exp: now + 900, + mode: MandateMode.IMMEDIATE, + sdHash: await hashAscii(l1.serialize()), + promptSummary: 'Purchase Babolat Pure Aero racket', + checkoutMandate: new CheckoutMandate({ vct: 'mandate.checkout.1', checkoutJwt }), + paymentMandate: new PaymentMandate({ + vct: 'mandate.payment.1', + paymentInstrument: PAYMENT_INSTRUMENT, + payee: TENNIS_WAREHOUSE, + currency: 'USD', + amount: 27999, + }), + }); + const l2 = await createLayer2Immediate(mandate, user.privateKey); + + const res = await verifyChain(l1, l2.sdJwt, { + issuerPublicJwk: issuer.publicKey, + l1Serialized: l1.serialize(), + expectedL2Aud: L2_AUD, + expectedL2Nonce: l2Nonce, + }); + + expect(res.valid).toBe(true); + expect(res.networkEnforced).toEqual([]); + }); +}); diff --git a/typescript/test/parity-failclosed.test.ts b/typescript/test/parity-failclosed.test.ts new file mode 100644 index 0000000..504e276 --- /dev/null +++ b/typescript/test/parity-failclosed.test.ts @@ -0,0 +1,87 @@ +/** + * Regression tests for TS↔Python fail-closed parity. + * + * Each case is a malformed/edge input where the TypeScript verifier previously + * "failed open" (accepted) while the Python reference "fails closed" (rejects). + * These lock in that TS now rejects exactly what Python rejects. None of these + * inputs is spec-conformant; a well-formed signed mandate is unaffected. + */ + +import { describe, expect, it } from 'vitest'; + +import { checkConstraints, compactJson, parseConstraint } from '../src/index.js'; + +describe('parity: constraint checker fails closed on malformed input', () => { + it('amount_range with string min does NOT bypass the bound (#1)', () => { + const constraints = [{ type: 'mandate.payment.amount_range', currency: 'USD', min: '100', max: 1000 }]; + const fulfillment = { payment_amount: { amount: 50, currency: 'USD' } }; + const res = checkConstraints(constraints, fulfillment); + expect(res.satisfied).toBe(false); + expect(res.violations.some((v) => /min must be an integer/i.test(v))).toBe(true); + }); + + it('amount_range with string max does NOT bypass the bound (#1)', () => { + const constraints = [{ type: 'mandate.payment.amount_range', currency: 'USD', max: '1000' }]; + const fulfillment = { payment_amount: { amount: 999999, currency: 'USD' } }; + const res = checkConstraints(constraints, fulfillment); + expect(res.satisfied).toBe(false); + expect(res.violations.some((v) => /max must be an integer/i.test(v))).toBe(true); + }); + + it('line_items with non-string match_mode is rejected, not coerced to minimum (#4)', () => { + const constraints = [ + { type: 'mandate.checkout.line_items', match_mode: 123, items: [{ id: 'x', acceptable_items: [], quantity: 1 }] }, + ]; + const fulfillment = { line_items: [{ id: 'x', quantity: 1 }] }; + const res = checkConstraints(constraints, fulfillment); + expect(res.satisfied).toBe(false); + expect(res.violations.some((v) => /match_mode/i.test(v))).toBe(true); + }); + + it("present-but-null fulfillment currency fails the currency check (#3)", () => { + const constraints = [{ type: 'mandate.payment.amount_range', currency: 'USD', min: 0, max: 100000 }]; + const fulfillment = { payment_amount: { amount: 500, currency: null } }; + const res = checkConstraints(constraints, fulfillment); + expect(res.satisfied).toBe(false); + expect(res.violations.some((v) => /Currency mismatch/i.test(v))).toBe(true); + }); + + it('present-but-null line item quantity is rejected as non-integer (#9)', () => { + const constraints = [ + { type: 'mandate.checkout.line_items', match_mode: 'minimum', items: [{ id: 'x', acceptable_items: [], quantity: 5 }] }, + ]; + const fulfillment = { line_items: [{ id: 'x', quantity: null }] }; + const res = checkConstraints(constraints, fulfillment); + expect(res.satisfied).toBe(false); + expect(res.violations.some((v) => /quantity/i.test(v))).toBe(true); + }); +}); + +describe('parity: parseConstraint preserves raw values like Python (#6)', () => { + it('keeps a wrongly-typed string min instead of dropping it', () => { + const c = parseConstraint({ type: 'mandate.payment.amount_range', currency: 'USD', min: '100', max: 40000 }); + // Python preserves min: "100" verbatim in to_dict(); TS must not drop it. + expect(c.toJSON()).toMatchObject({ min: '100', max: 40000 }); + }); + + it('keeps a wrongly-typed numeric match_mode instead of reverting to "minimum"', () => { + const c = parseConstraint({ type: 'mandate.checkout.line_items', items: [], match_mode: 5 }); + expect(c.toJSON()).toMatchObject({ match_mode: 5 }); + }); +}); + +describe('parity: compactJson matches Python json.dumps number formatting (#2)', () => { + it('serializes integers and ordinary decimals byte-identically to Python', () => { + expect(compactJson({ amount: 27999, exp: 1700000000 })).toBe('{"amount":27999,"exp":1700000000}'); + // 279.99 and 1.5 render identically in JS and Python — must be preserved, not rejected. + expect(compactJson({ price: 279.99 })).toBe('{"price":279.99}'); + expect(compactJson({ x: 1.5 })).toBe('{"x":1.5}'); + }); + + it('throws only on numbers that would serialize differently from Python', () => { + expect(() => compactJson({ n: 1e-7 })).toThrow(); // JS "1e-7" vs Python "1e-07" + expect(() => compactJson({ n: 1e20 })).toThrow(); // JS "100000000000000000000" vs Python "1e+20" + expect(() => compactJson({ n: Number.MAX_SAFE_INTEGER + 1 })).toThrow(); // precision loss + expect(() => compactJson({ n: Infinity })).toThrow(); // not representable in JSON + }); +}); diff --git a/typescript/test/roundtrip.test.ts b/typescript/test/roundtrip.test.ts new file mode 100644 index 0000000..7f22a59 --- /dev/null +++ b/typescript/test/roundtrip.test.ts @@ -0,0 +1,494 @@ +/** + * End-to-end TS-issue → TS-verify round trips using DEFAULT issuance options. + * + * Every other suite either byte-compares TS issuance against Python golden + * vectors (injecting fixed salts + clock) or replays Python-signed vectors + * through the TS verifier. None of them ever runs a credential the TypeScript + * SDK ISSUED (with real random salts and the real wall clock) back through + * `verifyChain`. This suite closes that gap: it builds full immediate and + * autonomous chains with the default `generateSalt` / `Date.now()` paths and + * asserts the TS verifier accepts its own issuance — plus negative controls and + * a constraint check against the actually-issued open mandate. + * + * Determinism note: no salts or clock are injected on purpose (the point is to + * exercise the default code paths). Outcomes stay deterministic anyway — a valid + * chain is valid regardless of which random salts were drawn — so no assertion + * depends on signature or disclosure bytes. + */ + +import { randomUUID } from 'node:crypto'; + +import { describe, expect, it } from 'vitest'; + +import { + AllowedMerchantConstraint, + AllowedPayeeConstraint, + buildSelectivePresentation, + checkConstraints, + CheckoutL3Mandate, + CheckoutLineItemsConstraint, + CheckoutMandate, + createLayer1, + createLayer2Autonomous, + createLayer2Immediate, + createLayer3Checkout, + createLayer3Payment, + decodeSdJwt, + FinalCheckoutMandate, + FinalPaymentMandate, + generateEs256Key, + hashAscii, + hashDisclosure, + IssuerCredential, + jwtEncode, + makeSigner, + MandateMode, + PaymentAmountConstraint, + PaymentL3Mandate, + PaymentMandate, + resolveDisclosures, + type SdJwt, + UserMandate, + verifyChain, + type VerifyChainOptions, +} from '../src/index.js'; + +// --------------------------------------------------------------------------- +// Scenario fixtures (mirrors python/examples/helpers.py) +// --------------------------------------------------------------------------- + +const TENNIS_WAREHOUSE = { id: 'merchant-uuid-1', name: 'Tennis Warehouse', website: 'https://tennis-warehouse.com' }; +const BABOLAT = { id: 'merchant-uuid-2', name: 'Babolat', website: 'https://babolat.com' }; +const MERCHANTS = [TENNIS_WAREHOUSE, BABOLAT]; + +const ACCEPTABLE_ITEMS = [ + { id: 'BAB86345', title: 'Babolat Pure Aero Tennis Racket' }, + { id: 'HEA23102', title: 'Head Graphene 360 Speed' }, +]; + +const PAYMENT_INSTRUMENT = { + type: 'mastercard.srcDigitalCard', + id: 'f199c3dd-7106-478b-9b5f-7af9ca725170', + description: 'Mastercard **** 1234', +}; + +const L2_AUD = 'https://agent.verifiable-intent.example'; +const L3_PAYMENT_AUD = 'https://www.mastercard.com'; +const L3_CHECKOUT_AUD = 'https://tennis-warehouse.com'; + +const isObj = (v: unknown): v is Record => + typeof v === 'object' && v !== null && !Array.isArray(v); + +/** Find a disclosure string in an SdJwt whose resolved value matches a predicate. */ +function findDisclosure(sj: SdJwt, predicate: (value: unknown) => boolean): string { + for (let i = 0; i < sj.disclosures.length; i++) { + const dv = sj.disclosureValues[i]; + const value = dv && dv.length ? dv[dv.length - 1] : null; + if (predicate(value)) { + const disc = sj.disclosures[i]; + if (disc !== undefined) return disc; + } + } + throw new Error('disclosure not found for predicate'); +} + +/** A merchant-signed checkout JWT. Its signature is never verified by the chain + * (only hashed into checkout_hash), so any real signed JWT works. */ +async function makeCheckoutJwt(merchantPrivateJwk: Parameters[0], now: number): Promise { + const signer = await makeSigner(merchantPrivateJwk); + const payload = { + iss: 'https://tennis-warehouse.com', + sub: 'cart_checkout', + iat: now, + exp: now + 3600, + cart: { items: [{ sku: 'BAB86345', quantity: 1 }], subTotal: { amount: 279.99, currencyCode: 'USD' } }, + }; + return jwtEncode({ alg: 'ES256', typ: 'JWT', kid: 'merchant-key-1' }, payload, signer); +} + +// --------------------------------------------------------------------------- +// Chain builders — DEFAULT issuance options (random salts, real clock) +// --------------------------------------------------------------------------- + +interface ImmediateChain { + issuer: Awaited>; + l1: SdJwt; + l2: SdJwt; + l2Nonce: string; +} + +async function buildImmediateChain(): Promise { + const now = Math.floor(Date.now() / 1000); + const issuer = await generateEs256Key(); + const user = await generateEs256Key(); + const merchant = await generateEs256Key(); + + const cred = new IssuerCredential({ + iss: 'https://www.mastercard.com', + sub: 'user-bob-001', + iat: now, + exp: now + 86400, + aud: 'https://wallet.example.com', + cnfJwk: user.publicKey, + email: 'bob@example.com', + panLastFour: '5678', + scheme: 'Mastercard', + }); + const l1 = await createLayer1(cred, issuer.privateKey); + + const checkoutJwt = await makeCheckoutJwt(merchant.privateKey, now); + const l2Nonce = randomUUID(); + const mandate = new UserMandate({ + nonce: l2Nonce, + aud: L2_AUD, + iat: now, + iss: 'https://wallet.example.com', + exp: now + 900, + mode: MandateMode.IMMEDIATE, + sdHash: await hashAscii(l1.serialize()), + promptSummary: 'Purchase Babolat Pure Aero racket', + // Final values; no cnf, no delegation. checkout_hash + transaction_id are + // auto-computed from checkout_jwt by createLayer2Immediate (default path). + checkoutMandate: new CheckoutMandate({ vct: 'mandate.checkout.1', checkoutJwt }), + paymentMandate: new PaymentMandate({ + vct: 'mandate.payment.1', + paymentInstrument: PAYMENT_INSTRUMENT, + payee: TENNIS_WAREHOUSE, + currency: 'USD', + amount: 27999, + }), + }); + const result = await createLayer2Immediate(mandate, user.privateKey); + return { issuer, l1, l2: result.sdJwt, l2Nonce }; +} + +interface AutonomousChain { + issuer: Awaited>; + l1: SdJwt; + l2: SdJwt; + l2Ser: string; + l3a: SdJwt; + l3b: SdJwt; + l2PaymentSer: string; + l2CheckoutSer: string; + l2Nonce: string; + l3Nonce: string; +} + +async function buildAutonomousChain(): Promise { + const now = Math.floor(Date.now() / 1000); + const issuer = await generateEs256Key(); + const user = await generateEs256Key(); + const agent = await generateEs256Key(); + const merchant = await generateEs256Key(); + + const cred = new IssuerCredential({ + iss: 'https://www.mastercard.com', + sub: 'user-alice-001', + iat: now, + exp: now + 86400, + aud: 'https://wallet.example.com', + cnfJwk: user.publicKey, + email: 'alice@example.com', + panLastFour: '1234', + scheme: 'Mastercard', + }); + const l1 = await createLayer1(cred, issuer.privateKey); + + const l2Nonce = randomUUID(); + const mandate = new UserMandate({ + nonce: l2Nonce, + aud: L2_AUD, + iat: now, + iss: 'https://wallet.example.com', + exp: now + 86400, + mode: MandateMode.AUTONOMOUS, + sdHash: await hashAscii(l1.serialize()), + promptSummary: 'Buy a Babolat tennis racket under $400', + checkoutMandate: new CheckoutMandate({ + vct: 'mandate.checkout.open.1', + cnfJwk: agent.publicKey, + cnfKid: 'agent-key-1', + constraints: [ + new AllowedMerchantConstraint({ allowed: MERCHANTS }), + new CheckoutLineItemsConstraint({ + items: [{ id: 'line-item-1', acceptable_items: ACCEPTABLE_ITEMS, quantity: 1 }], + }), + ], + }), + paymentMandate: new PaymentMandate({ + vct: 'mandate.payment.open.1', + cnfJwk: agent.publicKey, + cnfKid: 'agent-key-1', + paymentInstrument: PAYMENT_INSTRUMENT, + constraints: [ + new AllowedPayeeConstraint({ allowed: [TENNIS_WAREHOUSE] }), + new PaymentAmountConstraint({ currency: 'USD', min: 10000, max: 40000 }), + ], + }), + merchants: MERCHANTS, + acceptableItems: ACCEPTABLE_ITEMS, + }); + const l2 = await createLayer2Autonomous(mandate, user.privateKey); + const l2Ser = l2.serialize(); + const l2BaseJwt = l2Ser.split('~')[0] ?? ''; + + const paymentDisc = findDisclosure(l2, (v) => isObj(v) && v.vct === 'mandate.payment.open.1'); + const checkoutDisc = findDisclosure(l2, (v) => isObj(v) && v.vct === 'mandate.checkout.open.1'); + const merchantDisc = findDisclosure(l2, (v) => isObj(v) && v.name === 'Tennis Warehouse'); + const itemDisc = findDisclosure(l2, (v) => isObj(v) && v.id === 'BAB86345'); + + const checkoutJwt = await makeCheckoutJwt(merchant.privateKey, now); + const cHash = await hashAscii(checkoutJwt); + const l3Nonce = randomUUID(); + + // L3a: payment fulfillment for the network. + const l3a = await createLayer3Payment( + new PaymentL3Mandate({ + nonce: l3Nonce, + aud: L3_PAYMENT_AUD, + iat: now, + iss: 'https://agent.example.com', + exp: now + 300, + finalPayment: new FinalPaymentMandate({ + transactionId: cHash, + payee: TENNIS_WAREHOUSE, + paymentAmount: { currency: 'USD', amount: 27999 }, + paymentInstrument: PAYMENT_INSTRUMENT, + }), + finalMerchant: TENNIS_WAREHOUSE, + }), + agent.privateKey, + { l2BaseJwt, paymentDisclosure: paymentDisc, merchantDisclosure: merchantDisc }, + ); + + // L3b: checkout fulfillment for the merchant. + const l3b = await createLayer3Checkout( + new CheckoutL3Mandate({ + nonce: l3Nonce, + aud: L3_CHECKOUT_AUD, + iat: now, + iss: 'https://agent.example.com', + exp: now + 300, + finalCheckout: new FinalCheckoutMandate({ checkoutJwt, checkoutHash: cHash }), + }), + agent.privateKey, + { l2BaseJwt, checkoutDisclosure: checkoutDisc, itemDisclosure: itemDisc }, + ); + + // Role-specific L2 presentations each L3 binds its sd_hash to. + const l2PaymentSer = buildSelectivePresentation(l2BaseJwt, [paymentDisc, merchantDisc]); + const l2CheckoutSer = buildSelectivePresentation(l2BaseJwt, [checkoutDisc, itemDisc]); + + return { issuer, l1, l2, l2Ser, l3a, l3b, l2PaymentSer, l2CheckoutSer, l2Nonce, l3Nonce }; +} + +/** Full verify options for an autonomous chain, using the given (possibly + * re-decoded) L3 credentials in a single split-L3 pair. */ +function autonomousVerifyOpts(c: AutonomousChain, l3a: SdJwt, l3b: SdJwt): VerifyChainOptions { + return { + issuerPublicJwk: c.issuer.publicKey, + l1Serialized: c.l1.serialize(), + l2Serialized: c.l2Ser, + splitL3s: [ + { + l3Payment: l3a, + l3Checkout: l3b, + l2PaymentSerialized: c.l2PaymentSer, + l2CheckoutSerialized: c.l2CheckoutSer, + }, + ], + expectedL2Aud: L2_AUD, + expectedL2Nonce: c.l2Nonce, + expectedL3PaymentAud: L3_PAYMENT_AUD, + expectedL3PaymentNonce: c.l3Nonce, + expectedL3CheckoutAud: L3_CHECKOUT_AUD, + expectedL3CheckoutNonce: c.l3Nonce, + }; +} + +/** Flip one character in the middle of a serialized SD-JWT's payload segment, + * leaving header, signature and disclosures intact. */ +function tamperPayload(serialized: string): string { + const tildeIdx = serialized.indexOf('~'); + const jwt = tildeIdx === -1 ? serialized : serialized.slice(0, tildeIdx); + const rest = tildeIdx === -1 ? '' : serialized.slice(tildeIdx); + const [h, p, s] = jwt.split('.'); + if (h === undefined || p === undefined || s === undefined) throw new Error('not a compact JWT'); + const i = Math.floor(p.length / 2); + const flipped = p[i] === 'A' ? 'B' : 'A'; + const newP = p.slice(0, i) + flipped + p.slice(i + 1); + return `${h}.${newP}.${s}${rest}`; +} + +// --------------------------------------------------------------------------- +// Immediate mode +// --------------------------------------------------------------------------- + +describe('roundtrip: immediate mode (TS issue → TS verify, default options)', () => { + it('verifies a freshly issued immediate chain', async () => { + const c = await buildImmediateChain(); + // Two disclosures: the checkout mandate and the payment mandate. + expect(c.l2.disclosures.length).toBe(2); + + const res = await verifyChain(c.l1, c.l2, { + issuerPublicJwk: c.issuer.publicKey, + l1Serialized: c.l1.serialize(), + expectedL2Aud: L2_AUD, + expectedL2Nonce: c.l2Nonce, + }); + + expect(res.valid).toBe(true); + expect(res.errors).toEqual([]); + expect(res.mandatePairCount).toBe(1); + expect(res.pairResults.length).toBe(1); + expect(res.l2CheckoutDisclosed).toBe(true); + expect(res.l2PaymentDisclosed).toBe(true); + }); + + it('verifies again after a serialize → decodeSdJwt wire round-trip', async () => { + const c = await buildImmediateChain(); + const l1 = decodeSdJwt(c.l1.serialize()); + const l2 = decodeSdJwt(c.l2.serialize()); + + const res = await verifyChain(l1, l2, { + issuerPublicJwk: c.issuer.publicKey, + l1Serialized: c.l1.serialize(), + }); + expect(res.valid).toBe(true); + expect(res.errors).toEqual([]); + }); +}); + +// --------------------------------------------------------------------------- +// Autonomous mode +// --------------------------------------------------------------------------- + +describe('roundtrip: autonomous mode (TS issue → TS verify, default options)', () => { + it('verifies a freshly issued 3-layer split-L3 chain', async () => { + const c = await buildAutonomousChain(); + // Six disclosures: 2 merchants + 2 acceptable items + checkout + payment. + expect(c.l2.disclosures.length).toBe(6); + + const res = await verifyChain(c.l1, c.l2, autonomousVerifyOpts(c, c.l3a, c.l3b)); + + expect(res.valid).toBe(true); + expect(res.errors).toEqual([]); + expect(res.mandatePairCount).toBe(1); + expect(res.pairResults.length).toBe(1); + expect(res.l2CheckoutDisclosed).toBe(true); + expect(res.l2PaymentDisclosed).toBe(true); + // Both L3s were verified and cross-referenced. + expect(res.checksPerformed).toContain('l3_cross_reference'); + expect(res.pairResults[0]?.l3PaymentClaims).not.toEqual({}); + expect(res.pairResults[0]?.l3CheckoutClaims).not.toEqual({}); + }); + + it('verifies again after serialize → decodeSdJwt wire round-trip of every layer', async () => { + const c = await buildAutonomousChain(); + const l1 = decodeSdJwt(c.l1.serialize()); + const l2 = decodeSdJwt(c.l2Ser); + const l3a = decodeSdJwt(c.l3a.serialize()); + const l3b = decodeSdJwt(c.l3b.serialize()); + + const res = await verifyChain(l1, l2, autonomousVerifyOpts(c, l3a, l3b)); + expect(res.valid).toBe(true); + expect(res.errors).toEqual([]); + }); +}); + +// --------------------------------------------------------------------------- +// Negative round trips +// --------------------------------------------------------------------------- + +describe('roundtrip: negative controls', () => { + it('rejects a chain verified against the WRONG issuer public key', async () => { + const c = await buildImmediateChain(); + const attacker = await generateEs256Key(); + + const res = await verifyChain(c.l1, c.l2, { + issuerPublicJwk: attacker.publicKey, + l1Serialized: c.l1.serialize(), + }); + expect(res.valid).toBe(false); + expect(res.errors).toContain('L1 signature verification failed'); + }); + + it('rejects (or fails to decode) a tampered serialized L2', async () => { + const c = await buildImmediateChain(); + const tampered = tamperPayload(c.l2.serialize()); + + let decoded: SdJwt; + try { + decoded = decodeSdJwt(tampered); + } catch { + // A one-character flip that breaks base64url/JSON structure is rejected at + // decode time — an acceptable fail-closed outcome. + return; + } + // Otherwise it decoded to a different payload whose user signature no longer + // matches: verification must reject, never accept. + const res = await verifyChain(c.l1, decoded, { + issuerPublicJwk: c.issuer.publicKey, + l1Serialized: c.l1.serialize(), + }); + expect(res.valid).toBe(false); + }); +}); + +// --------------------------------------------------------------------------- +// Constraint checking against the actually-issued open mandate +// --------------------------------------------------------------------------- + +describe('roundtrip: checkConstraints against the issued open payment mandate', () => { + it('accepts a conforming fulfillment and rejects an over-budget one', async () => { + const c = await buildAutonomousChain(); + + // Extract the payment mandate's constraints straight from the issued L2. + const resolved = await resolveDisclosures(c.l2); + const delegates = Array.isArray(resolved.delegate_payload) ? resolved.delegate_payload : []; + const paymentMandate = delegates.find((d) => isObj(d) && d.vct === 'mandate.payment.open.1'); + if (!isObj(paymentMandate) || !Array.isArray(paymentMandate.constraints)) { + throw new Error('issued L2 payment mandate / constraints not found'); + } + const paymentConstraints = paymentMandate.constraints; + + // Resolve the allowed_payees SD refs back to merchant objects (as a network + // verifier would, per python/examples/autonomous_flow.py). + const valueByHash = new Map(); + for (let i = 0; i < c.l2.disclosures.length; i++) { + const disc = c.l2.disclosures[i]; + const dv = c.l2.disclosureValues[i]; + if (disc === undefined || dv === undefined) continue; + valueByHash.set(await hashDisclosure(disc), dv[dv.length - 1]); + } + const allowedMerchants: unknown[] = []; + for (const con of paymentConstraints) { + if (isObj(con) && con.type === 'mandate.payment.allowed_payees' && Array.isArray(con.allowed)) { + for (const ref of con.allowed) { + const h = isObj(ref) ? ref['...'] : undefined; + if (typeof h === 'string' && valueByHash.has(h)) allowedMerchants.push(valueByHash.get(h)); + } + } + } + + const okFulfillment = { + payment_amount: { currency: 'USD', amount: 27999 }, + payee: TENNIS_WAREHOUSE, + allowed_merchants: allowedMerchants, + }; + const okRes = checkConstraints(paymentConstraints, okFulfillment); + expect(okRes.satisfied).toBe(true); + expect(okRes.violations).toEqual([]); + + // $500 exceeds the issued $100–$400 (10000–40000 cents) amount range. + const overBudget = { + payment_amount: { currency: 'USD', amount: 50000 }, + payee: TENNIS_WAREHOUSE, + allowed_merchants: allowedMerchants, + }; + const badRes = checkConstraints(paymentConstraints, overBudget); + expect(badRes.satisfied).toBe(false); + expect(badRes.violations.length).toBeGreaterThan(0); + }); +}); diff --git a/typescript/test/time-claims-failclosed.test.ts b/typescript/test/time-claims-failclosed.test.ts new file mode 100644 index 0000000..885b442 --- /dev/null +++ b/typescript/test/time-claims-failclosed.test.ts @@ -0,0 +1,381 @@ +/** + * Fail-closed regression tests for the malformed time-claim guards + * (`isExpired` / `isFutureDated` in src/verification/chain.ts). + * + * These two helpers implement a security-critical rule: a malformed `exp` is + * treated as EXPIRED and a malformed `iat` is treated as FUTURE-DATED, so the + * credential is REJECTED. The dangerous refactor this file guards against is one + * that turns "malformed exp → reject" into "malformed exp → skip the check", + * which would fail OPEN. The golden conformance vectors never carry a malformed + * time claim, so without these tests that regression would ship silently. + * + * Two harnesses, mirroring the Python reference tests in + * python/tests/test_verification_hardening.py: + * + * L1: the `exp`/`iat` checks run BEFORE the L2 signature and sd_hash-binding + * steps, so we take the golden `immediate_valid` L1, mutate the decoded + * payload in memory, skip issuer verification, and pin `l1Serialized` to + * the original bytes so the (unmutated) L2 binding still holds. A reject + * returns at the time check; the accept path exercises the whole chain. + * + * L2/L3: their signatures are ALWAYS verified, so an in-memory mutation would + * fail the signature check first and never reach the time guard. Instead + * we RE-SIGN the layer (exactly as the Python tests do with create_sd_jwt) + * using the standard user/agent private keys the golden vectors were signed + * with — the golden L1's cnf.jwk is the standard user key, and the L2 + * mandate cnf.jwk is the standard agent key — so a re-signed layer still + * verifies against the rest of the golden chain. + * + * Where JS `String(value)` renders a mutated claim identically to Python's + * `str(value)` (plain strings, integers) we assert the FULL error byte-for-byte. + * For values whose textual form is language-specific (`true`→`True`, `NaN`→`nan`, + * `{}`→`[object Object]`, `Infinity`→a huge int) only the rendered value differs; + * the accept/reject DECISION is identical, so we assert the byte-identical error + * prefix plus `valid === false`. None of these is a behavioral divergence. + */ + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +import { createSdJwt, decodeSdJwt, type Es256Jwk, type SdJwt, type VerifyChainOptions, verifyChain } from '../src/index.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const V: any = JSON.parse(readFileSync(join(here, '../../test-vectors/vectors.json'), 'utf8')); +const C = V.verification_conformance; + +const IMM = C.chain_scenarios.find((s: any) => s.name === 'immediate_valid'); +const AUTO = C.chain_scenarios.find((s: any) => s.name === 'autonomous_valid'); + +/** The golden chains were signed with these standard keys (verified: L1 cnf.jwk == user pub). */ +const USER_PRIV = V.keys.user.private as Es256Jwk; +const AGENT_PRIV = V.keys.agent.private as Es256Jwk; + +/** Default clock skew used by verifyChain when `clockSkewSeconds` is unset. */ +const SKEW = 300; + +type Payload = Record; +type Mutate = (payload: Payload) => void; + +// --- L1 harness: in-memory mutation, issuer sig skipped, L1 serialization pinned. --- + +const freshImmL1 = () => decodeSdJwt(IMM.l1); +const freshImmL2 = () => decodeSdJwt(IMM.l2); + +/** Pinning `l1Serialized` keeps the L2→L1 sd_hash binding valid while we mutate L1's decoded payload. */ +const immOpts = (): VerifyChainOptions => ({ + skipIssuerVerification: true, + currentTime: IMM.current_time, + l1Serialized: IMM.l1, +}); + +/** Decode the golden immediate L2, mutate its payload, and re-sign with the user key. */ +async function resignImmL2(mutate: Mutate): Promise { + const l2 = decodeSdJwt(IMM.l2); + const payload: Payload = { ...l2.payload }; + mutate(payload); + return createSdJwt({ ...l2.header }, payload, [...l2.disclosures], USER_PRIV); +} + +// --- L3 harness: re-sign the autonomous L3a (payment) with the agent key. --- + +function autoOpts(l3Payment: SdJwt | null, l3Checkout: SdJwt | null): VerifyChainOptions { + return { + skipIssuerVerification: true, + currentTime: AUTO.current_time, + l1Serialized: AUTO.l1, + l2Serialized: AUTO.l2, + splitL3s: [ + { + l3Payment, + l3Checkout, + l2PaymentSerialized: AUTO.l2_payment_serialized ?? null, + l2CheckoutSerialized: AUTO.l2_checkout_serialized ?? null, + }, + ], + }; +} + +async function resignAutoL3Payment(mutate: Mutate): Promise { + const l3 = decodeSdJwt(AUTO.l3_payment); + const payload: Payload = { ...l3.payload }; + mutate(payload); + return createSdJwt({ ...l3.header }, payload, [...l3.disclosures], AGENT_PRIV); +} + +const freshAutoL3Checkout = () => decodeSdJwt(AUTO.l3_checkout); + +describe('fail-closed: malformed time claims (isExpired / isFutureDated)', () => { + it('baseline: unmutated immediate chain is valid (anchors the L1 harness)', async () => { + const res = await verifyChain(freshImmL1(), freshImmL2(), immOpts()); + expect(res.valid).toBe(true); + expect(res.errors).toEqual([]); + }); + + it('baseline: unmutated autonomous chain is valid (anchors the L3 harness)', async () => { + const res = await verifyChain( + decodeSdJwt(AUTO.l1), + decodeSdJwt(AUTO.l2), + autoOpts(decodeSdJwt(AUTO.l3_payment), freshAutoL3Checkout()), + ); + expect(res.valid).toBe(true); + expect(res.errors).toEqual([]); + }); + + // --- L1 exp: every malformed exp is treated as expired → reject. --- + + describe('L1 exp malformed → treated as expired', () => { + it('rejects a non-numeric string exp (byte-exact) [test_non_numeric_exp_rejected]', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).exp = 'never'; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors).toContain('L1 credential expired at never'); + }); + + it('rejects a numeric-looking string exp (byte-exact)', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).exp = '123'; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors).toContain('L1 credential expired at 123'); + }); + + it('rejects an object exp [test_dict_exp_rejected]', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).exp = { value: 9999999999 }; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + // JS renders the object as "[object Object]" vs Python "{'value': 9999999999}"; only the prefix is portable. + expect(res.errors.some((e) => e.startsWith('L1 credential expired at '))).toBe(true); + }); + + it('rejects an empty-object exp', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).exp = {}; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.startsWith('L1 credential expired at '))).toBe(true); + }); + + it('rejects a NaN exp [test_nan_exp_rejected]', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).exp = NaN; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.startsWith('L1 credential expired at '))).toBe(true); + }); + + it('rejects exp=true [test_l1_exp_bool_rejected]', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).exp = true; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.startsWith('L1 credential expired at '))).toBe(true); + }); + + it('rejects exp=false (a boolean is not a valid timestamp, regardless of value)', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).exp = false; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.startsWith('L1 credential expired at '))).toBe(true); + }); + + it('treats exp=0 as expired at the epoch, not as absent (byte-exact) [test_l1_exp_zero_treated_as_expired]', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).exp = 0; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors).toContain('L1 credential expired at 0'); + }); + }); + + // --- L1 iat: every malformed iat is treated as future-dated → reject. --- + + describe('L1 iat malformed → treated as future-dated', () => { + it('rejects a non-numeric string iat (byte-exact) [test_l1_iat_string_rejected]', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).iat = 'tomorrow'; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors).toContain('L1 credential iat is in the future: tomorrow'); + }); + + it('rejects a numeric-looking string iat (byte-exact)', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).iat = '123'; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors).toContain('L1 credential iat is in the future: 123'); + }); + + it('rejects iat=true [test_l1_iat_bool_rejected]', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).iat = true; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.startsWith('L1 credential iat is in the future: '))).toBe(true); + }); + + it('rejects iat=false', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).iat = false; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.startsWith('L1 credential iat is in the future: '))).toBe(true); + }); + + it('rejects a NaN iat', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).iat = NaN; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.startsWith('L1 credential iat is in the future: '))).toBe(true); + }); + + it('rejects an object iat', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).iat = {}; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.startsWith('L1 credential iat is in the future: '))).toBe(true); + }); + + it('rejects a huge iat without overflow (1e309 → Infinity) [test_l1_iat_huge_integer_rejected_without_overflow]', async () => { + const l1 = freshImmL1(); + // A JSON number like 1e309 parses to Infinity in JS (and to inf in Python); the + // non-finite guard rejects it. Python's synthetic 10**400 also rejects (> now). + (l1.payload as Payload).iat = Infinity; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.startsWith('L1 credential iat is in the future: '))).toBe(true); + }); + }); + + // --- L1 clock-skew boundary: at the boundary accept, one second past reject. --- + + describe('L1 iat clock-skew boundary', () => { + it('accepts iat exactly at now + skew [test_l1_iat_at_skew_boundary_accepted]', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).iat = IMM.current_time + SKEW; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(true); + expect(res.errors).toEqual([]); + }); + + it('rejects iat one second past the skew boundary (guards an off-by-one fail-open)', async () => { + const l1 = freshImmL1(); + (l1.payload as Payload).iat = IMM.current_time + SKEW + 1; + const res = await verifyChain(l1, freshImmL2(), immOpts()); + expect(res.valid).toBe(false); + expect(res.errors).toContain(`L1 credential iat is in the future: ${IMM.current_time + SKEW + 1}`); + }); + }); + + // --- L2 (top-level) exp/iat via re-signed immediate L2. --- + + describe('L2 exp malformed → treated as expired', () => { + it('rejects a string exp (byte-exact)', async () => { + const l2 = await resignImmL2((p) => { p.exp = 'never'; }); + const res = await verifyChain(freshImmL1(), l2, immOpts()); + expect(res.valid).toBe(false); + expect(res.errors).toContain('L2 expired at never'); + }); + + it('rejects exp=true', async () => { + const l2 = await resignImmL2((p) => { p.exp = true; }); + const res = await verifyChain(freshImmL1(), l2, immOpts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.startsWith('L2 expired at '))).toBe(true); + }); + + it('rejects an object exp', async () => { + const l2 = await resignImmL2((p) => { p.exp = {}; }); + const res = await verifyChain(freshImmL1(), l2, immOpts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.startsWith('L2 expired at '))).toBe(true); + }); + + it('treats exp=0 as expired (byte-exact)', async () => { + const l2 = await resignImmL2((p) => { p.exp = 0; }); + const res = await verifyChain(freshImmL1(), l2, immOpts()); + expect(res.valid).toBe(false); + expect(res.errors).toContain('L2 expired at 0'); + }); + }); + + describe('L2 iat malformed → treated as future-dated', () => { + it('rejects a string iat (byte-exact)', async () => { + const l2 = await resignImmL2((p) => { p.iat = 'later'; }); + const res = await verifyChain(freshImmL1(), l2, immOpts()); + expect(res.valid).toBe(false); + expect(res.errors).toContain('L2 iat is in the future: later'); + }); + + it('rejects iat=false', async () => { + const l2 = await resignImmL2((p) => { p.iat = false; }); + const res = await verifyChain(freshImmL1(), l2, immOpts()); + expect(res.valid).toBe(false); + expect(res.errors.some((e) => e.startsWith('L2 iat is in the future: '))).toBe(true); + }); + + // Note: NaN / Infinity are intentionally NOT tested at L2/L3. The signer + // (compactJson) refuses to serialize non-finite numbers, and JSON.parse has + // no NaN/Infinity literal, so a validly-signed L2/L3 carrying one is + // unconstructible — the credential fails closed at issuance/parse instead of + // at the time guard. The non-finite branch of the guard is exercised at L1. + }); + + describe('L2 clock-skew boundary and absent exp', () => { + it('accepts top-level iat exactly at now + skew [test_l2_top_level_iat_at_skew_boundary_accepted]', async () => { + const l2 = await resignImmL2((p) => { p.iat = IMM.current_time + SKEW; }); + const res = await verifyChain(freshImmL1(), l2, immOpts()); + expect(res.valid).toBe(true); + expect(res.errors).toEqual([]); + }); + + it('accepts an absent exp (backward compat) [test_l2_l3_exp_absent_passes]', async () => { + const l2 = await resignImmL2((p) => { delete p.exp; }); + const res = await verifyChain(freshImmL1(), l2, immOpts()); + expect(res.valid).toBe(true); + expect(res.errors.some((e) => e.toLowerCase().includes('expired'))).toBe(false); + }); + }); + + // --- L3a (payment) exp/iat via re-signed autonomous L3. --- + + describe('L3 exp/iat malformed → rejected', () => { + it('rejects a string exp (byte-exact)', async () => { + const l3 = await resignAutoL3Payment((p) => { p.exp = 'never'; }); + const res = await verifyChain(decodeSdJwt(AUTO.l1), decodeSdJwt(AUTO.l2), autoOpts(l3, freshAutoL3Checkout())); + expect(res.valid).toBe(false); + expect(res.errors).toContain('L3a (payment) expired at never'); + }); + + it('rejects a string iat (byte-exact)', async () => { + const l3 = await resignAutoL3Payment((p) => { p.iat = 'soon'; }); + const res = await verifyChain(decodeSdJwt(AUTO.l1), decodeSdJwt(AUTO.l2), autoOpts(l3, freshAutoL3Checkout())); + expect(res.valid).toBe(false); + expect(res.errors).toContain('L3a (payment) iat is in the future: soon'); + }); + }); + + describe('L3 clock-skew boundary and absent exp', () => { + it('accepts top-level iat exactly at now + skew [test_l3_top_level_iat_at_skew_boundary_accepted]', async () => { + const l3 = await resignAutoL3Payment((p) => { p.iat = AUTO.current_time + SKEW; }); + const res = await verifyChain(decodeSdJwt(AUTO.l1), decodeSdJwt(AUTO.l2), autoOpts(l3, freshAutoL3Checkout())); + expect(res.valid).toBe(true); + expect(res.errors).toEqual([]); + }); + + it('accepts an absent exp (backward compat) [test_l2_l3_exp_absent_passes]', async () => { + const l3 = await resignAutoL3Payment((p) => { delete p.exp; }); + const res = await verifyChain(decodeSdJwt(AUTO.l1), decodeSdJwt(AUTO.l2), autoOpts(l3, freshAutoL3Checkout())); + expect(res.valid).toBe(true); + expect(res.errors.some((e) => e.toLowerCase().includes('expired'))).toBe(false); + }); + }); +}); diff --git a/typescript/test/vectors.test.ts b/typescript/test/vectors.test.ts new file mode 100644 index 0000000..db3ce3e --- /dev/null +++ b/typescript/test/vectors.test.ts @@ -0,0 +1,364 @@ +/** + * Conformance tests: validate the TypeScript port against golden vectors + * generated from the Python reference implementation + * (../../test-vectors/vectors.json, produced by python/scripts/generate_vectors.py). + * + * ECDSA signatures are randomized, so we never compare signature bytes. We + * compare the deterministic artifacts (disclosures, hashes, the base64url + * header/payload segments, delegate_payload, _sd) and we cross-verify the + * Python-signed credentials with the TypeScript verifier. + */ + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +import { + AllowedMerchantConstraint, + AllowedPayeeConstraint, + b64urlDecode, + b64urlEncode, + CheckoutL3Mandate, + CheckoutLineItemsConstraint, + CheckoutMandate, + compactJson, + createDisclosure, + createLayer1, + createLayer2Autonomous, + createLayer2Immediate, + createLayer3Checkout, + createLayer3Payment, + decodeSdJwt, + FinalCheckoutMandate, + FinalPaymentMandate, + hashAscii, + hashDisclosure, + IssuerCredential, + MandateMode, + parseConstraint, + PaymentAmountConstraint, + PaymentBudgetConstraint, + PaymentL3Mandate, + PaymentMandate, + resolveDisclosures, + UserMandate, + verifySdJwtSignature, +} from '../src/index.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const V: any = JSON.parse(readFileSync(join(here, '../../test-vectors/vectors.json'), 'utf8')); + +/** Deterministic FIFO salt source that mirrors Python's disclosure-creation order. */ +function fifo(salts: string[]): () => string { + let i = 0; + return () => { + if (i >= salts.length) throw new Error(`salt source exhausted after ${i} salts`); + return salts[i++]; + }; +} + +const hexToBytes = (h: string): Uint8Array => new Uint8Array(Buffer.from(h, 'hex')); + +describe('primitives: base64url', () => { + for (const c of V.primitives.b64url) { + it(`encode/decode ${c.bytes_hex || '(empty)'}`, () => { + expect(b64urlEncode(hexToBytes(c.bytes_hex))).toBe(c.b64url); + expect(Buffer.from(b64urlDecode(c.b64url)).toString('hex')).toBe(c.bytes_hex); + }); + } +}); + +describe('primitives: disclosures', () => { + V.primitives.disclosures.forEach((c: any, idx: number) => { + it(`disclosure #${idx} (${c.claim_name ?? 'array-element'})`, async () => { + expect(await createDisclosure(c.claim_name, c.value, c.salt)).toBe(c.disclosure); + expect(await hashDisclosure(c.disclosure)).toBe(c.hash); + }); + }); +}); + +describe('primitives: hash_bytes (ascii)', () => { + V.primitives.hash_bytes.forEach((c: any, idx: number) => { + it(`hash #${idx}`, async () => { + expect(await hashAscii(c.input_ascii)).toBe(c.hash); + }); + }); +}); + +describe('models: constraints round-trip (parse → toJSON → json)', () => { + for (const c of V.models.constraints) { + it(c.name, () => { + const parsed = parseConstraint(c.dict); + expect(parsed.toJSON()).toEqual(c.dict); + expect(compactJson(parsed.toJSON())).toBe(c.json); + }); + } + + it('PaymentBudgetConstraint rejects non-positive max', () => { + expect(() => new PaymentBudgetConstraint({ currency: 'USD', max: 0 })).toThrow(); + expect(() => new PaymentBudgetConstraint({ currency: 'USD', max: 100, min: 0 })).toThrow(); + }); +}); + +describe('models: misc toJSON', () => { + it('IssuerCredential.toJSON', () => { + const c = V.shared.l1_credential; + const cred = new IssuerCredential({ + iss: c.iss, + sub: c.sub, + iat: c.iat, + exp: c.exp, + vct: c.vct, + cnfJwk: c.cnf_jwk, + panLastFour: c.pan_last_four, + scheme: c.scheme, + cardId: c.card_id, + email: c.email, + }); + expect(cred.toJSON()).toEqual(V.models.misc.issuer_credential_to_payload.dict); + expect(compactJson(cred.toJSON())).toBe(V.models.misc.issuer_credential_to_payload.json); + }); + + it('FinalPaymentMandate / FinalCheckoutMandate', () => { + const fp = V.models.misc.final_payment_mandate.dict; + const built = new FinalPaymentMandate({ + transactionId: fp.transaction_id, + payee: fp.payee, + paymentAmount: fp.payment_amount, + paymentInstrument: fp.payment_instrument, + }); + expect(built.toJSON()).toEqual(fp); + + const fc = V.models.misc.final_checkout_mandate.dict; + const builtC = new FinalCheckoutMandate({ checkoutJwt: fc.checkout_jwt, checkoutHash: fc.checkout_hash }); + expect(builtC.toJSON()).toEqual(fc); + }); +}); + +describe('crypto interop: TS verifies Python-signed credentials', () => { + it('binds checkout_hash and l1 sd_hash', async () => { + expect(await hashAscii(V.shared.checkout_jwt)).toBe(V.shared.checkout_hash); + expect(await hashAscii(V.shared.l1_serialized)).toBe(V.shared.l1_sd_hash); + }); + + it('verifies the Python L1 signature', async () => { + const sj = decodeSdJwt(V.shared.l1.serialized); + expect(await sjVerify(sj, V.keys.issuer.public)).toBe(true); + }); + + it('verifies L2 immediate / autonomous (user key)', async () => { + expect(await sjVerify(decodeSdJwt(V.immediate.l2.serialized), V.keys.user.public)).toBe(true); + expect(await sjVerify(decodeSdJwt(V.autonomous.l2.serialized), V.keys.user.public)).toBe(true); + }); + + it('verifies L3a / L3b (agent key)', async () => { + expect(await sjVerify(decodeSdJwt(V.autonomous.l3a.credential.serialized), V.keys.agent.public)).toBe(true); + expect(await sjVerify(decodeSdJwt(V.autonomous.l3b.credential.serialized), V.keys.agent.public)).toBe(true); + }); + + it('rejects a tampered Python L1 (mutated subject)', async () => { + const sj = decodeSdJwt(V.shared.l1.serialized); + sj.payload.sub = 'attacker'; + expect(await sjVerify(sj, V.keys.issuer.public)).toBe(false); + }); +}); + +describe('crypto: resolveDisclosures', () => { + it('resolves immediate delegate_payload to the mandate dicts', async () => { + const resolved = await resolveDisclosures(decodeSdJwt(V.immediate.l2.serialized)); + expect(resolved.delegate_payload).toEqual(V.immediate.l2.values); + }); + + it('resolves autonomous delegate_payload to checkout + payment mandates', async () => { + const resolved = await resolveDisclosures(decodeSdJwt(V.autonomous.l2.serialized)); + const vals = V.autonomous.l2.values; + expect(resolved.delegate_payload).toEqual([vals[vals.length - 2], vals[vals.length - 1]]); + }); +}); + +describe('disclosure reproduction from salt + value (array-element flows)', () => { + const cases: Array<[string, any]> = [ + ['L2-immediate', V.immediate.l2], + ['L2-autonomous', V.autonomous.l2], + ['L3a', V.autonomous.l3a.credential], + ['L3b', V.autonomous.l3b.credential], + ]; + for (const [name, rec] of cases) { + it(`${name}`, async () => { + for (let i = 0; i < rec.disclosures.length; i++) { + expect(await createDisclosure(null, rec.values[i], rec.salts[i])).toBe(rec.disclosures[i]); + expect(await hashDisclosure(rec.disclosures[i])).toBe(rec.sd_hashes_of_disclosures[i]); + } + }); + } +}); + +describe('issuance reproduction (byte-exact payload + disclosures)', () => { + it('L1', async () => { + const c = V.shared.l1_credential; + const cred = new IssuerCredential({ + iss: c.iss, + sub: c.sub, + iat: c.iat, + exp: c.exp, + vct: c.vct, + cnfJwk: c.cnf_jwk, + panLastFour: c.pan_last_four, + scheme: c.scheme, + cardId: c.card_id, + email: c.email, + }); + const sj = await createLayer1(cred, V.keys.issuer.private, { + kid: V.keys.issuer.kid, + saltSource: fifo(V.shared.l1.salts), + }); + expect(sj.disclosures).toEqual(V.shared.l1.disclosures); + expect(sj.rawHeaderB64).toBe(V.shared.l1.header_b64); + expect(sj.rawPayloadB64).toBe(V.shared.l1.payload_b64); + expect(sj.payload).toEqual(V.shared.l1.payload); + expect(await sjVerify(sj, V.keys.issuer.public)).toBe(true); + }); + + it('L2 immediate', async () => { + const inp = V.immediate.inputs; + const cm = new CheckoutMandate({ vct: inp.checkout_mandate.vct, checkoutJwt: inp.checkout_mandate.checkout_jwt }); + const pm = new PaymentMandate({ + vct: inp.payment_mandate.vct, + paymentInstrument: inp.payment_mandate.payment_instrument, + payee: inp.payment_mandate.payee, + currency: inp.payment_mandate.currency, + amount: inp.payment_mandate.amount, + }); + const mandate = new UserMandate({ + nonce: inp.nonce, + aud: inp.aud, + iat: inp.iat, + mode: MandateMode.IMMEDIATE, + iss: inp.iss, + exp: inp.exp, + sdHash: inp.sd_hash, + checkoutMandate: cm, + paymentMandate: pm, + }); + const res = await createLayer2Immediate(mandate, V.keys.user.private, { + kid: inp.kid, + saltSource: fifo(V.immediate.l2.salts), + }); + expect(res.sdJwt.disclosures).toEqual(V.immediate.l2.disclosures); + expect(res.sdJwt.rawPayloadB64).toBe(V.immediate.l2.payload_b64); + expect(res.sdJwt.payload).toEqual(V.immediate.l2.payload); + // Mandate dicts (post auto-compute of checkout_hash / transaction_id) + expect(cm.toJSON()).toEqual(V.immediate.checkout_mandate_dict); + expect(pm.toJSON()).toEqual(V.immediate.payment_mandate_dict); + expect(await sjVerify(res.sdJwt, V.keys.user.public)).toBe(true); + }); + + it('L2 autonomous', async () => { + const inp = V.autonomous.inputs; + const pi = V.immediate.inputs.payment_mandate.payment_instrument; // same PAYMENT_INSTRUMENT + const checkout = new CheckoutMandate({ + vct: 'mandate.checkout.open.1', + cnfJwk: inp.agent_cnf_jwk, + cnfKid: inp.agent_cnf_kid, + constraints: [ + new AllowedMerchantConstraint({ allowed: inp.merchants }), + new CheckoutLineItemsConstraint({ + items: [{ id: 'line-1', acceptable_items: inp.acceptable_items, quantity: 1 }], + }), + ], + }); + const payment = new PaymentMandate({ + vct: 'mandate.payment.open.1', + cnfJwk: inp.agent_cnf_jwk, + cnfKid: inp.agent_cnf_kid, + constraints: [ + new AllowedPayeeConstraint({ allowed: [inp.merchants[0]] }), + new PaymentAmountConstraint({ currency: 'USD', min: 10000, max: 40000 }), + ], + paymentInstrument: pi, + }); + const mandate = new UserMandate({ + nonce: inp.nonce, + aud: inp.aud, + iat: inp.iat, + mode: MandateMode.AUTONOMOUS, + iss: inp.iss, + exp: inp.exp, + sdHash: inp.sd_hash, + merchants: inp.merchants, + acceptableItems: inp.acceptable_items, + checkoutMandate: checkout, + paymentMandate: payment, + }); + const sj = await createLayer2Autonomous(mandate, V.keys.user.private, { + kid: inp.kid, + saltSource: fifo(V.autonomous.l2.salts), + }); + expect(sj.disclosures).toEqual(V.autonomous.l2.disclosures); + expect(sj.rawPayloadB64).toBe(V.autonomous.l2.payload_b64); + expect(sj.payload).toEqual(V.autonomous.l2.payload); + expect(await sjVerify(sj, V.keys.user.public)).toBe(true); + }); + + it('L3a payment', async () => { + const inp = V.autonomous.l3a.inputs; + const fp = new FinalPaymentMandate({ + transactionId: inp.final_payment.transaction_id, + payee: inp.final_payment.payee, + paymentAmount: inp.final_payment.payment_amount, + paymentInstrument: inp.final_payment.payment_instrument, + }); + const mandate = new PaymentL3Mandate({ + nonce: inp.nonce, + aud: inp.aud, + iat: inp.iat, + iss: inp.iss, + exp: inp.exp, + finalPayment: fp, + finalMerchant: inp.final_merchant, + }); + const sj = await createLayer3Payment( + mandate, + V.keys.agent.private, + { l2BaseJwt: inp.l2_base_jwt, paymentDisclosure: inp.payment_disclosure, merchantDisclosure: inp.merchant_disclosure }, + { kid: inp.kid, saltSource: fifo(V.autonomous.l3a.credential.salts) }, + ); + expect(sj.disclosures).toEqual(V.autonomous.l3a.credential.disclosures); + expect(sj.payload.sd_hash).toBe(V.autonomous.l3a.sd_hash); + expect(sj.rawPayloadB64).toBe(V.autonomous.l3a.credential.payload_b64); + expect(sj.payload).toEqual(V.autonomous.l3a.credential.payload); + expect(await sjVerify(sj, V.keys.agent.public)).toBe(true); + }); + + it('L3b checkout', async () => { + const inp = V.autonomous.l3b.inputs; + const fc = new FinalCheckoutMandate({ + checkoutJwt: inp.final_checkout.checkout_jwt, + checkoutHash: inp.final_checkout.checkout_hash, + }); + const mandate = new CheckoutL3Mandate({ + nonce: inp.nonce, + aud: inp.aud, + iat: inp.iat, + iss: inp.iss, + exp: inp.exp, + finalCheckout: fc, + }); + const sj = await createLayer3Checkout( + mandate, + V.keys.agent.private, + { l2BaseJwt: inp.l2_base_jwt, checkoutDisclosure: inp.checkout_disclosure, itemDisclosure: inp.item_disclosure }, + { kid: inp.kid, saltSource: fifo(V.autonomous.l3b.credential.salts) }, + ); + expect(sj.disclosures).toEqual(V.autonomous.l3b.credential.disclosures); + expect(sj.payload.sd_hash).toBe(V.autonomous.l3b.sd_hash); + expect(sj.rawPayloadB64).toBe(V.autonomous.l3b.credential.payload_b64); + expect(await sjVerify(sj, V.keys.agent.public)).toBe(true); + }); +}); + +function sjVerify(sj: ReturnType, jwk: any): Promise { + return verifySdJwtSignature(sj, jwk); +} diff --git a/typescript/test/verification.test.ts b/typescript/test/verification.test.ts new file mode 100644 index 0000000..260c709 --- /dev/null +++ b/typescript/test/verification.test.ts @@ -0,0 +1,112 @@ +/** + * Verification conformance: replays the scenarios in + * ../../test-vectors/vectors.json (verification_conformance) through the + * TypeScript verifier and asserts the result matches the Python reference. + * + * The clock is injected via `currentTime` so the fixed-timestamp vectors + * verify deterministically. + */ + +import { readFileSync } from 'node:fs'; +import { dirname, join } from 'node:path'; +import { fileURLToPath } from 'node:url'; + +import { describe, expect, it } from 'vitest'; + +import { + checkConstraints, + decodeSdJwt, + StrictnessMode, + type VerifyChainOptions, + verifyChain, + verifyCheckoutHashBinding, + verifyL2ReferenceBinding, + verifyL3CrossReference, +} from '../src/index.js'; + +const here = dirname(fileURLToPath(import.meta.url)); +const V: any = JSON.parse(readFileSync(join(here, '../../test-vectors/vectors.json'), 'utf8')); +const C = V.verification_conformance; + +function runScenario(s: any) { + const opts: VerifyChainOptions = { + issuerPublicJwk: s.issuer_public ?? undefined, + skipIssuerVerification: s.skip_issuer_verification, + l1Serialized: s.l1, + l2Serialized: s.l2, + currentTime: s.current_time, + expectedL2Aud: s.expected_l2_aud ?? undefined, + expectedL2Nonce: s.expected_l2_nonce ?? undefined, + expectedL3PaymentAud: s.expected_l3_payment_aud ?? undefined, + expectedL3PaymentNonce: s.expected_l3_payment_nonce ?? undefined, + expectedL3CheckoutAud: s.expected_l3_checkout_aud ?? undefined, + expectedL3CheckoutNonce: s.expected_l3_checkout_nonce ?? undefined, + }; + if (s.l3_payment || s.l3_checkout) { + opts.splitL3s = [ + { + l3Payment: s.l3_payment ? decodeSdJwt(s.l3_payment) : null, + l3Checkout: s.l3_checkout ? decodeSdJwt(s.l3_checkout) : null, + l2PaymentSerialized: s.l2_payment_serialized ?? null, + l2CheckoutSerialized: s.l2_checkout_serialized ?? null, + }, + ]; + } + return verifyChain(decodeSdJwt(s.l1), decodeSdJwt(s.l2), opts); +} + +describe('verification: chain scenarios (TS verdict matches Python)', () => { + for (const s of C.chain_scenarios) { + it(`${s.name} → valid=${s.expected_valid}`, async () => { + const res = await runScenario(s); + expect(res.valid).toBe(s.expected_valid); + // Exact, order-sensitive error parity with the Python reference. A + // rejection with the *wrong* error would mean the intended guard failed + // open and something else coincidentally rejected — that must fail here, + // not pass. If a string diverges, fix src to match Python; never weaken + // this assertion. + expect(res.errors).toEqual(s.expected_errors); + }); + } + + it('a valid autonomous chain exposes one verified mandate pair', async () => { + const s = C.chain_scenarios.find((x: any) => x.name === 'autonomous_valid'); + const res = await runScenario(s); + expect(res.valid).toBe(true); + expect(res.mandatePairCount).toBe(1); + expect(res.pairResults.length).toBe(1); + expect(res.l2PaymentDisclosed).toBe(true); + expect(res.l2CheckoutDisclosed).toBe(true); + }); +}); + +describe('verification: constraint checker (counts match Python)', () => { + for (const c of C.constraint_cases) { + it(c.name, () => { + const res = checkConstraints(c.constraints, c.fulfillment, { + mode: c.mode === 'strict' ? StrictnessMode.STRICT : StrictnessMode.PERMISSIVE, + isOpenMandate: c.is_open_mandate, + }); + expect(res.satisfied).toBe(c.expected.satisfied); + expect(res.violations.length).toBe(c.expected.violations); + expect(res.checked.length).toBe(c.expected.checked); + expect(res.skipped.length).toBe(c.expected.skipped); + }); + } +}); + +describe('verification: integrity', () => { + for (const c of C.integrity_cases) { + it(c.name, async () => { + let valid: boolean; + if (c.kind === 'checkout_hash') { + valid = (await verifyCheckoutHashBinding(c.checkout_mandate, c.payment_mandate)).valid; + } else if (c.kind === 'l2_ref') { + valid = (await verifyL2ReferenceBinding({}, c.payment_mandate, c.checkout_disclosure)).valid; + } else { + valid = verifyL3CrossReference(c.l3_payment_claims, c.l3_checkout_claims).valid; + } + expect(valid).toBe(c.expected_valid); + }); + } +}); diff --git a/typescript/tsconfig.json b/typescript/tsconfig.json new file mode 100644 index 0000000..07261a8 --- /dev/null +++ b/typescript/tsconfig.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022"], + "types": ["node"], + "strict": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + "verbatimModuleSyntax": true, + "exactOptionalPropertyTypes": true, + "declaration": true, + "declarationMap": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "skipLibCheck": true + }, + "include": ["src"] +} diff --git a/typescript/tsdown.config.mjs b/typescript/tsdown.config.mjs new file mode 100644 index 0000000..9ba93a5 --- /dev/null +++ b/typescript/tsdown.config.mjs @@ -0,0 +1,28 @@ +import { defineConfig } from 'tsdown'; + +/** + * Build config for the publishable npm package. Produces dual ESM (.mjs) + CJS + * (.cjs) output with bundled type declarations. The package has zero runtime + * dependencies and uses only Web-standard globals (WebCrypto, TextEncoder), so + * `platform: 'neutral'` keeps the bundle environment-agnostic — it runs on + * Node >= 20 and in modern browsers. Emits fixed .mjs/.cjs extensions. + * + * Authored as .mjs (not .ts) so tsdown loads it natively without a TS config loader. + */ +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm', 'cjs'], + platform: 'neutral', + // fixedExtension defaults to true only on platform 'node'; force it so the + // ESM/CJS outputs keep the fixed .mjs/.cjs extensions the exports map expects. + fixedExtension: true, + dts: true, + sourcemap: true, + clean: true, + // exports map is maintained by hand in package.json (nested per-condition + // types for clean ESM+CJS type resolution); don't let tsdown overwrite it. + exports: false, + // publint runs as a separate step (`npm run check:pkg`), not inside the build: + // running it during the build breaks the `npm publish` lifecycle here because + // tsdown's publint step shells out to `npm pack`, which collides with publish. +}); diff --git a/typescript/vitest.config.ts b/typescript/vitest.config.ts new file mode 100644 index 0000000..d5e8b71 --- /dev/null +++ b/typescript/vitest.config.ts @@ -0,0 +1,27 @@ +import { defineConfig } from 'vitest/config'; + +/** + * The library uses explicit `.js` extensions on relative imports (correct for + * the NodeNext ESM build). This pre-resolver maps `./foo.js` to `./foo.ts` so + * Vitest can run the TypeScript sources directly without a build step. + */ +export default defineConfig({ + plugins: [ + { + name: 'vi-resolve-js-to-ts', + enforce: 'pre', + async resolveId(source, importer) { + if (importer && source.startsWith('.') && source.endsWith('.js')) { + const resolved = await this.resolve(source.slice(0, -3) + '.ts', importer, { skipSelf: true }); + if (resolved) return resolved.id; + } + return null; + }, + }, + ], + test: { + globals: true, + environment: 'node', + include: ['test/**/*.test.ts'], + }, +});