From 2fe22eb83daf215db692aebfa14c1626c38ce65f Mon Sep 17 00:00:00 2001 From: Mustafa Zaidi <201567061+hedge-mustafa@users.noreply.github.com> Date: Fri, 23 Oct 2026 13:47:52 +0500 Subject: [PATCH] Align the spec with the SDKs: free-text list, x402 ids, rail_ref, conformance - docs/FIELDS.md lists the free-text fields checked for card numbers, adding payee.website, payee.resource_url and label keys, and says identifiers and evidence values are never checked. - rail_ref keys are closed per rail, as the schema already says; the docs no longer say receivers ignore unknown rail_ref keys. - docs/MAPPINGS.md defines the x402 payment_id for every stage, including payment.requested, which was unspecified. - conformance/: 40 events with expected verdicts, the signing vector and canonical-JSON vectors, checked in CI by npm test. Co-Authored-By: Claude Opus 5.5 --- .github/workflows/conformance.yml | 18 + .gitignore | 2 + CHANGELOG.md | 9 + README.md | 6 +- conformance/README.md | 21 + conformance/events.json | 1839 +++++++++++++++++++++++++++++ conformance/jcs-vectors.json | 154 +++ conformance/sign-vector.json | 109 ++ docs/FIELDS.md | 13 +- docs/MAPPINGS.md | 3 +- package-lock.json | 101 ++ package.json | 14 + scripts/check-conformance.mjs | 114 ++ 13 files changed, 2397 insertions(+), 6 deletions(-) create mode 100644 .github/workflows/conformance.yml create mode 100644 .gitignore create mode 100644 conformance/README.md create mode 100644 conformance/events.json create mode 100644 conformance/jcs-vectors.json create mode 100644 conformance/sign-vector.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 scripts/check-conformance.mjs diff --git a/.github/workflows/conformance.yml b/.github/workflows/conformance.yml new file mode 100644 index 0000000..4031b91 --- /dev/null +++ b/.github/workflows/conformance.yml @@ -0,0 +1,18 @@ +name: Conformance +on: + push: + branches: [main] + pull_request: +permissions: + contents: read +jobs: + check: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + - run: npm ci + - run: npm test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2752eb9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +node_modules/ +.DS_Store diff --git a/CHANGELOG.md b/CHANGELOG.md index ff7729d..dd7cded 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,15 @@ All notable changes to the Agent Payment Event (APE) schema. Versions follow `MAJOR.MINOR`; minor versions only add optional fields. Until v1.0, a minor version may still make breaking changes, and the changelog will say so. +## Unreleased + +Clarifications to v0.1, plus one tightening. The JSON Schema is unchanged. + +- **Tightening:** the free-text fields checked for card numbers are now listed exactly in `docs/FIELDS.md`, and include `payee.website`, `payee.resource_url` and `labels` keys. Receivers adopting this reject events with a card number in those fields (`pan_detected`). Identifiers and `evidence[].value` are never checked. +- `rail_ref` keys are closed per rail, as the schema already says. The README and `docs/FIELDS.md` no longer say receivers ignore unknown `rail_ref` keys. `labels` and `evidence[].value` stay open. +- x402 mapping: `payment_id` is `x402::` on every stage after the challenge, never the transaction hash. `payment.requested` uses `x402::requested:`, and the answering `payment.authorized` links back with `related.previous_event_id`. +- New `conformance/` folder: events with their expected verdicts, a signing vector and canonical-JSON vectors, checked in CI by `npm test`. + ## 0.1.0 First public draft. diff --git a/README.md b/README.md index d059947..507fe33 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,9 @@ const validate = ajv.compile(schema); if (!validate(event)) console.error(validate.errors); ``` -JSON Schema cannot express every rule. Receivers should also reject free-text fields that contain a Luhn-valid 13 to 19 digit sequence (a possible card number). +JSON Schema cannot express every rule. Receivers must also reject an event whose free-text fields contain a possible card number (a Luhn-valid run of 13 to 19 digits). [`docs/FIELDS.md`](docs/FIELDS.md#cross-field-rules) lists exactly which fields are free text. + +[`conformance/`](conformance/) holds events every implementation must accept or reject, plus signing and canonical-JSON test vectors. SDKs and receivers should run them in their own tests. ## Signing @@ -38,7 +40,7 @@ An envelope is `{ "event": { ... }, "sig": "" }`. The JWS header i ## Versioning -`ape_version` is `MAJOR.MINOR`. Minor versions only add optional fields. Receivers must ignore unknown keys in `labels`, `rail_ref` and `evidence[].value`, and reject unknown top-level fields. v0.x may change between minors until v1.0. +`ape_version` is `MAJOR.MINOR`. Minor versions only add optional fields, and receivers pick the schema by `ape_version`. Within a version, unknown top-level fields and unknown `rail_ref` keys are rejected; keys inside `labels` and `evidence[].value` are open, and receivers ignore the ones they do not use. v0.x may change between minors until v1.0. The `sha256` values in the examples are placeholders; real events carry the digest of their evidence value. diff --git a/conformance/README.md b/conformance/README.md new file mode 100644 index 0000000..ea594b8 --- /dev/null +++ b/conformance/README.md @@ -0,0 +1,21 @@ +# Conformance + +Test data every APE implementation (SDKs and receivers) should run in its own tests, so they all accept and reject the same events. `npm test` at the repository root checks these files against [`ape/v0.1.json`](../ape/v0.1.json) and the card-number rule in [`docs/FIELDS.md`](../docs/FIELDS.md#cross-field-rules). + +| File | What it holds | +| --- | --- | +| [`events.json`](events.json) | Events with the verdict an implementation must give. `expect` is `valid` or `invalid`; for an invalid event, `rule` says why: `schema` (the JSON Schema rejects it) or `pan` (it passes the schema, but a free-text field holds a possible card number). | +| [`sign-vector.json`](sign-vector.json) | One event, its RFC 8785 canonical form, a key pair and the detached JWS. Re-signing the event with `private_jwk` must give exactly `sig`; Ed25519 is deterministic. | +| [`jcs-vectors.json`](jcs-vectors.json) | Canonical JSON for numbers (as IEEE 754 bit patterns, including RFC 8785 appendix B) and for objects with tricky keys and strings. Generated from the TypeScript SDK's `canonicalize`. | + +A producer SDK that redacts before validating should run `events.json` through its validator directly, not through its emit path: redaction would remove the card numbers that the `pan` cases test for. + +## Status of the SDKs + +Run on 2026-10-23 against this revision. + +| Implementation | `events.json` | Differences | +| --- | --- | --- | +| `halvorai_ingest` (Dart) | 40 / 40 | none | +| `@halvorai/ingest` (Node.js, zod model) | 37 / 40 | Not yet checking `payee.website`, `payee.resource_url` and label keys for card numbers | +| `halvorai-ingest` (Python) | 36 / 40 | The same three, and it accepts `null` for optional fields (`null-optional-field`) | diff --git a/conformance/events.json b/conformance/events.json new file mode 100644 index 0000000..ceee133 --- /dev/null +++ b/conformance/events.json @@ -0,0 +1,1839 @@ +{ + "description": "APE v0.1 conformance cases. Every implementation must give each event the expected verdict. rule says why an invalid event fails: schema (the JSON Schema rejects it) or pan (schema-valid, but a free-text field holds a possible card number).", + "cases": [ + { + "name": "stripe-settled-minimal", + "expect": "valid", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "x402-authorized-with-evidence", + "expect": "valid", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.authorized", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "x402-client" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "x402:eip155:8453:0x7d3c5e", + "rail": "x402", + "amount": { + "value": "10000", + "unit": "atomic", + "asset": { + "network": "eip155:8453", + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "symbol": "USDC", + "decimals": 6 + } + }, + "status": "authorized", + "rail_ref": { + "scheme": "exact", + "network": "eip155:8453", + "nonce": "0x7d3c5e" + } + }, + "payee": { + "type": "api_provider", + "name": "api.example", + "resource_url": "https://api.example/prices", + "wallet": { + "network": "eip155:8453", + "address": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C" + } + }, + "instrument": { + "type": "wallet", + "wallet": { + "network": "eip155:8453", + "address": "0x857b06519E91e3A54538791bDbb0E22373e36b66" + } + }, + "evidence": [ + { + "kind": "x402.payment_payload", + "format": "json", + "value": { + "x402Version": 2 + }, + "sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } + ], + "related": { + "previous_event_id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1V" + } + } + }, + { + "name": "failed-with-reason", + "expect": "valid", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.failed", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "failed", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + }, + "failure_reason": "card_declined" + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "evidence-by-uri", + "expect": "valid", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + }, + "evidence": [ + { + "kind": "stripe.object", + "format": "json", + "uri": "https://files.example/pi.json", + "sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } + ] + } + }, + { + "name": "identifier-passing-luhn-is-not-free-text", + "expect": "valid", + "note": "Card networks issue numeric ids; 123456789012347 passes the Luhn check and must not be rejected or redacted.", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "card", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "network_transaction_id": "123456789012347" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "rail-ref-integer-value", + "expect": "valid", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stablecoin", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "network": "eip155:1", + "tx_hash": "0xabc", + "log_index": 3 + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "redacted-placeholder-in-free-text", + "expect": "valid", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Airways card [redacted:pan]" + } + } + }, + { + "name": "short-digit-run-in-free-text", + "expect": "valid", + "note": "10 digits is too short to be a card number.", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + }, + "description": "Order 1234567890 for 2 seats" + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "labels-open-keys", + "expect": "valid", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + }, + "labels": { + "team": "travel", + "cost_center": "cc-41" + } + } + }, + { + "name": "evidence-value-open-keys", + "expect": "valid", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + }, + "evidence": [ + { + "kind": "stripe.object", + "format": "json", + "value": { + "anything": { + "nested": [ + 1, + 2 + ] + } + }, + "sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } + ] + } + }, + { + "name": "unknown-top-level-field", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + }, + "extra": 1 + } + }, + { + "name": "unknown-rail-ref-key", + "expect": "invalid", + "rule": "schema", + "note": "rail_ref keys are closed per rail.", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_1", + "nonce": "n" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "failed-without-reason", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "failed", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "missing-rail-ref", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled" + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "payee-without-name-wallet-or-id", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant" + } + } + }, + { + "name": "decimal-amount", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41.20", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "amount-as-number", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": 41200, + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "lower-case-currency", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "usd" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "atomic-without-asset", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "1", + "unit": "atomic" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "unknown-event-type", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.approved", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "unknown-rail", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "paypal", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "date-time-without-offset", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "impossible-date", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-02-30T00:00:00Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "short-event-id", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_1", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "null-optional-field", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + }, + "instruction": null + } + }, + { + "name": "evidence-with-value-and-uri", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + }, + "evidence": [ + { + "kind": "stripe.object", + "format": "json", + "value": {}, + "uri": "https://files.example/a", + "sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } + ] + } + }, + { + "name": "evidence-bad-digest", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + }, + "evidence": [ + { + "kind": "stripe.object", + "format": "json", + "value": {}, + "sha256": "sha256:ABC" + } + ] + } + }, + { + "name": "too-many-labels", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + }, + "labels": { + "k0": "v", + "k1": "v", + "k2": "v", + "k3": "v", + "k4": "v", + "k5": "v", + "k6": "v", + "k7": "v", + "k8": "v", + "k9": "v", + "k10": "v", + "k11": "v", + "k12": "v", + "k13": "v", + "k14": "v", + "k15": "v", + "k16": "v", + "k17": "v", + "k18": "v", + "k19": "v", + "k20": "v" + } + } + }, + { + "name": "bad-country-code", + "expect": "invalid", + "rule": "schema", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "usa" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "pan-in-payee-name", + "expect": "invalid", + "rule": "pan", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Airways 4242424242424242" + } + } + }, + { + "name": "pan-with-spaces-in-description", + "expect": "invalid", + "rule": "pan", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + }, + "description": "Paid with 4242 4242 4242 4242" + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "pan-with-dashes-in-failure-reason", + "expect": "invalid", + "rule": "pan", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.failed", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "failed", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + }, + "failure_reason": "declined 4242-4242-4242-4242" + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "pan-in-category", + "expect": "invalid", + "rule": "pan", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + }, + "category": "4242424242424242" + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "pan-in-instruction-summary", + "expect": "invalid", + "rule": "pan", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + }, + "instruction": { + "id": "ins_1", + "hash": "sha256:0000000000000000000000000000000000000000000000000000000000000000", + "summary": "Use card 4242424242424242" + } + } + }, + { + "name": "pan-in-agent-name", + "expect": "invalid", + "rule": "pan", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "agent 4242424242424242" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "pan-in-owner-display-name", + "expect": "invalid", + "rule": "pan", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US", + "display_name": "Jo 4242424242424242" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + } + } + }, + { + "name": "pan-in-payee-website", + "expect": "invalid", + "rule": "pan", + "note": "New in this revision: URLs are free text.", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways", + "website": "https://shop.example/pay?card=4242424242424242" + } + } + }, + { + "name": "pan-in-payee-resource-url", + "expect": "invalid", + "rule": "pan", + "note": "New in this revision: URLs are free text.", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways", + "resource_url": "https://api.example/r/4242424242424242" + } + } + }, + { + "name": "pan-in-label-value", + "expect": "invalid", + "rule": "pan", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + }, + "labels": { + "note": "card 4242424242424242" + } + } + }, + { + "name": "pan-in-label-key", + "expect": "invalid", + "rule": "pan", + "note": "New in this revision: label keys are free text.", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "conformance", + "adapter": "custom" + }, + "agent": { + "id": "agt_travel_12", + "name": "Travel booking agent" + }, + "owner": { + "id": "own_u_4410", + "type": "person", + "country": "US" + }, + "payment": { + "payment_id": "pi_3QxAbCdEfGhIjKlM", + "rail": "stripe", + "amount": { + "value": "41200", + "unit": "minor", + "currency": "USD" + }, + "status": "settled", + "rail_ref": { + "payment_intent": "pi_3QxAbCdEfGhIjKlM" + } + }, + "payee": { + "type": "merchant", + "name": "Example Airways" + }, + "labels": { + "4242424242424242": "note" + } + } + } + ] +} diff --git a/conformance/jcs-vectors.json b/conformance/jcs-vectors.json new file mode 100644 index 0000000..9614bb1 --- /dev/null +++ b/conformance/jcs-vectors.json @@ -0,0 +1,154 @@ +{ + "numbers": [ + { + "bits": "0000000000000000", + "expected": "0" + }, + { + "bits": "8000000000000000", + "expected": "0" + }, + { + "bits": "0000000000000001", + "expected": "5e-324" + }, + { + "bits": "8000000000000001", + "expected": "-5e-324" + }, + { + "bits": "7fefffffffffffff", + "expected": "1.7976931348623157e+308" + }, + { + "bits": "ffefffffffffffff", + "expected": "-1.7976931348623157e+308" + }, + { + "bits": "4340000000000000", + "expected": "9007199254740992" + }, + { + "bits": "c340000000000000", + "expected": "-9007199254740992" + }, + { + "bits": "4430000000000000", + "expected": "295147905179352830000" + }, + { + "bits": "44b52d02c7e14af5", + "expected": "9.999999999999997e+22" + }, + { + "bits": "44b52d02c7e14af6", + "expected": "1e+23" + }, + { + "bits": "44b52d02c7e14af7", + "expected": "1.0000000000000001e+23" + }, + { + "bits": "444b1ae4d6e2ef4e", + "expected": "999999999999999700000" + }, + { + "bits": "444b1ae4d6e2ef4f", + "expected": "999999999999999900000" + }, + { + "bits": "444b1ae4d6e2ef50", + "expected": "1e+21" + }, + { + "bits": "3eb0c6f7a0b5ed8c", + "expected": "9.999999999999997e-7" + }, + { + "bits": "3eb0c6f7a0b5ed8d", + "expected": "0.000001" + }, + { + "bits": "41b3de4355555553", + "expected": "333333333.3333332" + }, + { + "bits": "41b3de4355555554", + "expected": "333333333.33333325" + }, + { + "bits": "41b3de4355555555", + "expected": "333333333.3333333" + }, + { + "bits": "41b3de4355555556", + "expected": "333333333.3333334" + }, + { + "bits": "41b3de4355555557", + "expected": "333333333.33333343" + }, + { + "bits": "becbf647612f3696", + "expected": "-0.0000033333333333333333" + }, + { + "bits": "43143ff3c1cb0959", + "expected": "1424953923781206.2" + }, + { + "bits": "3ff0000000000000", + "expected": "1" + }, + { + "bits": "4059000000000000", + "expected": "100" + }, + { + "bits": "3fb999999999999a", + "expected": "0.1" + }, + { + "bits": "3e7ad7f29abcaf48", + "expected": "1e-7" + }, + { + "bits": "3eb0c6f7a0b5ed8e", + "expected": "0.0000010000000000000002" + }, + { + "bits": "4415af1d78b58c40", + "expected": "100000000000000000000" + }, + { + "bits": "7fe0000000000000", + "expected": "8.98846567431158e+307" + } + ], + "objects": [ + { + "json": "{\"numbers\":[333333333.33333329,1E30,4.50,2e-3,0.000000000000000000000000001],\"string\":\"\\u20ac$\\u000F\\u000aA'\\u0042\\u0022\\u005c\\\\\\\"\\/\",\"literals\":[null,true,false]}", + "expected": "{\"literals\":[null,true,false],\"numbers\":[333333333.3333333,1e+30,4.5,0.002,1e-27],\"string\":\"€$\\u000f\\nA'B\\\"\\\\\\\\\\\"/\"}" + }, + { + "json": "{\"\\u20ac\":\"Euro Sign\",\"\\r\":\"Carriage Return\",\"\\ufb33\":\"Hebrew Letter Dalet With Dagesh\",\"1\":\"One\",\"\\ud83d\\ude00\":\"Emoji: Grinning Face\",\"\\u0080\":\"Control\",\"\\u00f6\":\"Latin Small Letter O With Diaeresis\"}", + "expected": "{\"\\r\":\"Carriage Return\",\"1\":\"One\",\"€\":\"Control\",\"ö\":\"Latin Small Letter O With Diaeresis\",\"€\":\"Euro Sign\",\"😀\":\"Emoji: Grinning Face\",\"דּ\":\"Hebrew Letter Dalet With Dagesh\"}" + }, + { + "json": "{\"b\":[3,{\"z\":1,\"a\":[]}],\"a\":{},\"\":\"empty key\",\"A\":\"upper\"}", + "expected": "{\"\":\"empty key\",\"A\":\"upper\",\"a\":{},\"b\":[3,{\"a\":[],\"z\":1}]}" + }, + { + "json": "{\"controls\":\"\\u0000\\u0001\\b\\t\\n\\u000b\\f\\r\\u001f\\u007f\",\"lone\":\"\\ud800x\\udfff\",\"quote\":\"say \\\"hi\\\"\"}", + "expected": "{\"controls\":\"\\u0000\\u0001\\b\\t\\n\\u000b\\f\\r\\u001f\",\"lone\":\"\\ud800x\\udfff\",\"quote\":\"say \\\"hi\\\"\"}" + }, + { + "json": "[56,{\"d\":true,\"10\":null,\"1\":[]}]", + "expected": "[56,{\"1\":[],\"10\":null,\"d\":true}]" + }, + { + "json": "{\"ints\":[0,-1,9007199254740991,-9007199254740991,100,1e2,-0]}", + "expected": "{\"ints\":[0,-1,9007199254740991,-9007199254740991,100,100,0]}" + } + ] +} diff --git a/conformance/sign-vector.json b/conformance/sign-vector.json new file mode 100644 index 0000000..60ee905 --- /dev/null +++ b/conformance/sign-vector.json @@ -0,0 +1,109 @@ +{ + "description": "Cross-language APE detached JWS vector (P1.3 / P3.7). Sign JCS bytes with Ed25519; compact JWS with empty payload.", + "event": { + "ape_version": "0.1", + "id": "evt_01JA8Q4K3T9V2C7M5N6P8R0S1W", + "type": "payment.settled", + "occurred_at": "2026-10-17T09:14:03.218Z", + "environment": "test", + "source": { + "id": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F", + "sdk": "@halvorai/ingest@0.1.0", + "adapter": "x402-client" + }, + "agent": { + "id": "agt_research_07", + "name": "Market research agent", + "framework": "openai-agents", + "version": "2026.10.2", + "key_thumbprint": "NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs" + }, + "owner": { + "id": "own_acme_ltd", + "type": "business", + "display_name": "Acme Analytics Ltd", + "country": "US", + "kyc_ref": "kyb_88231" + }, + "instruction": { + "id": "ins_5521", + "hash": "sha256:f755e5bdab5721af297a47e30692f0b50c4abd68e52f711e1fe8b970aa4e5890", + "summary": "Pull the latest EU power prices and summarise.", + "channel": "chat", + "issued_at": "2026-10-17T09:13:41.002Z", + "tool_call_id": "call_x2b9" + }, + "payment": { + "payment_id": "x402:eip155:8453:0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480", + "rail": "x402", + "amount": { + "value": "10000", + "unit": "atomic", + "asset": { + "network": "eip155:8453", + "address": "0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913", + "symbol": "USDC", + "decimals": 6 + } + }, + "status": "settled", + "description": "Premium market data, 1 request", + "rail_ref": { + "scheme": "exact", + "network": "eip155:8453", + "nonce": "0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480", + "tx_hash": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "facilitator": "https://x402.org/facilitator", + "payment_flow": "authorization" + } + }, + "payee": { + "type": "api_provider", + "name": "Example Market Data", + "wallet": { + "network": "eip155:8453", + "address": "0x209693Bc6afc0C5328bA36FaF03C514EF312287C" + }, + "resource_url": "https://api.example.com/premium-data" + }, + "instrument": { + "type": "wallet", + "wallet": { + "network": "eip155:8453", + "address": "0x857b06519E91e3A54538791bDbb0E22373e36b66" + } + }, + "evidence": [ + { + "kind": "x402.settlement", + "format": "json", + "value": { + "success": true, + "transaction": "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef", + "network": "eip155:8453", + "payer": "0x857b06519E91e3A54538791bDbb0E22373e36b66" + }, + "sha256": "sha256:0000000000000000000000000000000000000000000000000000000000000000" + } + ], + "related": { + "previous_event_id": "evt_01JA8Q4JZ0B1C2D3E4F5G6H7J8", + "session_id": "ses_9f2" + } + }, + "private_jwk": { + "crv": "Ed25519", + "d": "ZQ_YAuffmJz4_nqhgfpBGTxT7xOvtskmSW6MkRJm3dw", + "x": "Kz2Ad8JG2VeShi3vCFgk0F-p48AeKqUqbn09wKSXedc", + "kty": "OKP", + "kid": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F" + }, + "public_jwk": { + "crv": "Ed25519", + "x": "Kz2Ad8JG2VeShi3vCFgk0F-p48AeKqUqbn09wKSXedc", + "kty": "OKP", + "kid": "src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F" + }, + "canonical_jcs": "{\"agent\":{\"framework\":\"openai-agents\",\"id\":\"agt_research_07\",\"key_thumbprint\":\"NzbLsXh8uDCcd-6MNwXF4W_7noWXFZAfHkxZsRGC9Xs\",\"name\":\"Market research agent\",\"version\":\"2026.10.2\"},\"ape_version\":\"0.1\",\"environment\":\"test\",\"evidence\":[{\"format\":\"json\",\"kind\":\"x402.settlement\",\"sha256\":\"sha256:0000000000000000000000000000000000000000000000000000000000000000\",\"value\":{\"network\":\"eip155:8453\",\"payer\":\"0x857b06519E91e3A54538791bDbb0E22373e36b66\",\"success\":true,\"transaction\":\"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\"}}],\"id\":\"evt_01JA8Q4K3T9V2C7M5N6P8R0S1W\",\"instruction\":{\"channel\":\"chat\",\"hash\":\"sha256:f755e5bdab5721af297a47e30692f0b50c4abd68e52f711e1fe8b970aa4e5890\",\"id\":\"ins_5521\",\"issued_at\":\"2026-10-17T09:13:41.002Z\",\"summary\":\"Pull the latest EU power prices and summarise.\",\"tool_call_id\":\"call_x2b9\"},\"instrument\":{\"type\":\"wallet\",\"wallet\":{\"address\":\"0x857b06519E91e3A54538791bDbb0E22373e36b66\",\"network\":\"eip155:8453\"}},\"occurred_at\":\"2026-10-17T09:14:03.218Z\",\"owner\":{\"country\":\"US\",\"display_name\":\"Acme Analytics Ltd\",\"id\":\"own_acme_ltd\",\"kyc_ref\":\"kyb_88231\",\"type\":\"business\"},\"payee\":{\"name\":\"Example Market Data\",\"resource_url\":\"https://api.example.com/premium-data\",\"type\":\"api_provider\",\"wallet\":{\"address\":\"0x209693Bc6afc0C5328bA36FaF03C514EF312287C\",\"network\":\"eip155:8453\"}},\"payment\":{\"amount\":{\"asset\":{\"address\":\"0x833589fCD6eDb6E08f4c7C32D4f71b54bdA02913\",\"decimals\":6,\"network\":\"eip155:8453\",\"symbol\":\"USDC\"},\"unit\":\"atomic\",\"value\":\"10000\"},\"description\":\"Premium market data, 1 request\",\"payment_id\":\"x402:eip155:8453:0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480\",\"rail\":\"x402\",\"rail_ref\":{\"facilitator\":\"https://x402.org/facilitator\",\"network\":\"eip155:8453\",\"nonce\":\"0xf3746613c2d920b5fdabc0856f2aeb2d4f88ee6037b8cc5d04a71a4462f13480\",\"payment_flow\":\"authorization\",\"scheme\":\"exact\",\"tx_hash\":\"0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef\"},\"status\":\"settled\"},\"related\":{\"previous_event_id\":\"evt_01JA8Q4JZ0B1C2D3E4F5G6H7J8\",\"session_id\":\"ses_9f2\"},\"source\":{\"adapter\":\"x402-client\",\"id\":\"src_01JA8P0Z6X1Y2Z3A4B5C6D7E8F\",\"sdk\":\"@halvorai/ingest@0.1.0\"},\"type\":\"payment.settled\"}", + "sig": "eyJhbGciOiJFZERTQSIsImtpZCI6InNyY18wMUpBOFAwWjZYMVkyWjNBNEI1QzZEN0U4RiIsInR5cCI6ImFwZStqd3MiLCJhcGUiOiIwLjEifQ..-low0vAGf3Dk40LOM6JTW2eG1RFbSmQcTzV7vdnItiouUdI920fz_vpP3LP9EXEKYzUaWi-bf5ffsTCkinUrAA" +} diff --git a/docs/FIELDS.md b/docs/FIELDS.md index 007a919..a3e201f 100644 --- a/docs/FIELDS.md +++ b/docs/FIELDS.md @@ -107,6 +107,13 @@ Generated from [`ape/v0.1.json`](../ape/v0.1.json). - If `payment.status` is `failed`, `payment.failure_reason` is required. - `payee` needs at least one of `name`, `wallet` or `id`. - Each `evidence[]` item has exactly one of `value` or `uri`. -- `payment.rail_ref` accepts only the keys listed for the event's rail. -- Unknown top-level fields are rejected. Unknown keys in `labels`, `rail_ref` and `evidence[].value` are ignored by receivers. -- Receivers also reject free-text fields containing a Luhn-valid 13 to 19 digit sequence (a possible card number). JSON Schema cannot express this check. +- `payment.rail_ref` accepts only the keys listed for the event's rail. An unknown `rail_ref` key is rejected, like an unknown top-level field. A new key arrives with a new `ape_version`, and receivers pick the schema by that version. +- `labels` and `evidence[].value` are open: any keys are allowed, and receivers ignore the ones they do not use. +- Free-text fields must not contain a possible card number: a run of 13 to 19 digits, with single spaces or dashes allowed between digits, that passes the Luhn check. Producers redact such runs before signing (as `[redacted:pan]`); receivers reject an event that still has one, with `pan_detected`. JSON Schema cannot express this check. The free-text fields are exactly: + - `payment.description`, `payment.category`, `payment.failure_reason` + - `instruction.summary` + - `agent.name`, `owner.display_name` + - `payee.name`, `payee.website`, `payee.resource_url` + - every key and every value of `labels` + + Identifiers are not free text and are never checked or redacted: ids, `payment.payment_id`, every `rail_ref` value, `instrument.*` and `related.*`. Card networks issue numeric transaction ids that pass the Luhn check about one time in ten, so checking them would corrupt or reject real events. `evidence[].value` is not checked either: it is a third party's proof, carried verbatim, and changing it would break its signature. diff --git a/docs/MAPPINGS.md b/docs/MAPPINGS.md index bd1d72c..573bc19 100644 --- a/docs/MAPPINGS.md +++ b/docs/MAPPINGS.md @@ -46,7 +46,8 @@ Notation: `src.path → ape.path`. "verbatim" means the whole object also goes i | `SettlementResponse.transaction` | `payment.rail_ref.tx_hash` | | `SettlementResponse.amount` | overrides `payment.amount.value` (actual amount for `upto`) | | `SettlementResponse.errorReason` | event `payment.failed`, `payment.failure_reason` | -| `payment_id` | `x402::` | +| `payment_id` on `payment.authorized`, `payment.settled`, `payment.failed` | `x402::`, the same on every stage so the events of one payment join. Never the transaction hash, which only exists once it settles. | +| `payment_id` on `payment.requested` | `x402::requested:`. No nonce exists before the client signs, and a fixed `x402::requested` would give every challenge on a network the same id. The `payment.authorized` event that answers the challenge sets `related.previous_event_id` to the requested event's `id`. | ### 5.2 Stripe (PaymentIntents and Issuing) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..499ce63 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,101 @@ +{ + "name": "ape-schema-conformance", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "ape-schema-conformance", + "devDependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "jose": "^6.0.0" + } + }, + "node_modules/ajv": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.20.0.tgz", + "integrity": "sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "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-uri": { + "version": "3.1.9", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.9.tgz", + "integrity": "sha512-hS/0Wt3Ya043oKXs0sIxrMpKQITXhMLIrVAKKXi4gozGC9v7YRWVnXIZzjNPTO02M9BdxLCWqK1yRXgNziVyJQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/jose": { + "version": "6.2.13", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.13.tgz", + "integrity": "sha512-7TIg10ZE1x82YediN+lcrPNvGMKge9yFHQD+Qho/b8y2adfugU3AsvvHfxV0fzyFllT6l9xCjf4RWY9g/Z9PJQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..7b7ff6c --- /dev/null +++ b/package.json @@ -0,0 +1,14 @@ +{ + "name": "ape-schema-conformance", + "private": true, + "type": "module", + "description": "Checks APE conformance cases and test vectors against ape/v0.1.json.", + "scripts": { + "test": "node scripts/check-conformance.mjs" + }, + "devDependencies": { + "ajv": "^8.17.1", + "ajv-formats": "^3.0.1", + "jose": "^6.0.0" + } +} diff --git a/scripts/check-conformance.mjs b/scripts/check-conformance.mjs new file mode 100644 index 0000000..4d38843 --- /dev/null +++ b/scripts/check-conformance.mjs @@ -0,0 +1,114 @@ +// Checks the conformance cases, the examples and the test vectors against ape/v0.1.json and +// the card-number rule in docs/FIELDS.md. Run with `npm test`. +import { readFileSync, readdirSync } from 'node:fs'; +import Ajv2020 from 'ajv/dist/2020.js'; +import addFormats from 'ajv-formats'; +import { compactVerify, importJWK } from 'jose'; + +const read = (path) => JSON.parse(readFileSync(new URL(`../${path}`, import.meta.url), 'utf8')); + +const ajv = new Ajv2020({ strict: false, allErrors: true }); +addFormats(ajv); +const validate = ajv.compile(read('ape/v0.1.json')); + +// The free-text fields, exactly as docs/FIELDS.md lists them. +function freeText(event) { + const at = (a, b) => (typeof event[a]?.[b] === 'string' ? [[`${a}.${b}`, event[a][b]]] : []); + const labels = event.labels && typeof event.labels === 'object' ? event.labels : {}; + return [ + ...at('payment', 'description'), + ...at('payment', 'category'), + ...at('payment', 'failure_reason'), + ...at('instruction', 'summary'), + ...at('agent', 'name'), + ...at('owner', 'display_name'), + ...at('payee', 'name'), + ...at('payee', 'website'), + ...at('payee', 'resource_url'), + ...Object.keys(labels).map((k) => ['labels (key)', k]), + ...Object.entries(labels).filter(([, v]) => typeof v === 'string').map(([k, v]) => [`labels.${k}`, v]), + ]; +} + +function luhn(digits) { + let sum = 0; + for (let i = 0; i < digits.length; i++) { + let d = digits.charCodeAt(digits.length - 1 - i) - 48; + if (i % 2 === 1) { + d *= 2; + if (d > 9) d -= 9; + } + sum += d; + } + return sum % 10 === 0; +} + +function containsPan(text) { + return (text.match(/\d(?:[ -]?\d)*/g) ?? []).some((run) => { + const digits = run.replace(/[ -]/g, ''); + return digits.length >= 13 && digits.length <= 19 && luhn(digits); + }); +} + +/** 'valid', or the first rule an event breaks: 'schema' or 'pan'. */ +function verdict(event) { + if (!validate(event)) return { result: 'schema', why: ajv.errorsText(validate.errors) }; + const hit = freeText(event).find(([, text]) => containsPan(text)); + if (hit) return { result: 'pan', why: `card number in ${hit[0]}` }; + return { result: 'valid' }; +} + +// RFC 8785, as the TypeScript SDK's canonicalize. +function canonicalize(value) { + if (value === null || typeof value !== 'object') return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(canonicalize).join(',')}]`; + const keys = Object.keys(value).sort(); + return `{${keys.map((k) => `${JSON.stringify(k)}:${canonicalize(value[k])}`).join(',')}}`; +} + +const failures = []; +let passed = 0; +const check = (ok, label) => (ok ? passed++ : failures.push(label)); + +for (const c of read('conformance/events.json').cases) { + const v = verdict(c.event); + const expected = c.expect === 'valid' ? 'valid' : c.rule; + check(v.result === expected, `events.json ${c.name}: expected ${expected}, got ${v.result}${v.why ? ` (${v.why})` : ''}`); +} + +for (const file of readdirSync(new URL('../examples', import.meta.url)).filter((f) => f.endsWith('.json'))) { + const v = verdict(read(`examples/${file}`)); + check(v.result === 'valid', `examples/${file}: ${v.result} (${v.why})`); +} + +const sign = read('conformance/sign-vector.json'); +check(canonicalize(sign.event) === sign.canonical_jcs, 'sign-vector.json: canonical form differs'); +try { + const [header, , signature] = sign.sig.split('.'); + const payload = Buffer.from(sign.canonical_jcs, 'utf8').toString('base64url'); + const { protectedHeader } = await compactVerify( + `${header}.${payload}.${signature}`, + await importJWK(sign.public_jwk, 'EdDSA'), + { algorithms: ['EdDSA'] }, + ); + check( + protectedHeader.typ === 'ape+jws' && protectedHeader.ape === '0.1' && protectedHeader.kid === sign.event.source.id, + 'sign-vector.json: header does not match the spec', + ); +} catch (err) { + check(false, `sign-vector.json: signature does not verify (${err.message})`); +} + +const jcs = read('conformance/jcs-vectors.json'); +const view = new DataView(new ArrayBuffer(8)); +for (const { bits, expected } of jcs.numbers) { + view.setBigUint64(0, BigInt(`0x${bits}`)); + check(canonicalize(view.getFloat64(0)) === expected, `jcs-vectors.json number ${bits}`); +} +jcs.objects.forEach(({ json, expected }, i) => check(canonicalize(JSON.parse(json)) === expected, `jcs-vectors.json object ${i}`)); + +if (failures.length > 0) { + console.error(`${failures.length} failed, ${passed} passed:\n ${failures.join('\n ')}`); + process.exit(1); +} +console.log(`All ${passed} conformance checks passed.`);