From 11e5962479e5a7d6d11085365cca9b1f76992322 Mon Sep 17 00:00:00 2001 From: Greg Soucy Date: Sat, 21 Mar 2026 23:40:39 -0400 Subject: [PATCH] Align SDK receipt parsing with runtime contract --- test_vectors/README.md | 2 +- test_vectors/expected_hash.txt | 2 +- test_vectors/public_key_base64.txt | 2 +- test_vectors/receipt_invalid_sig.json | 10 +++++----- test_vectors/receipt_malformed_pubkey.json | 10 +++++----- test_vectors/receipt_valid.json | 10 +++++----- test_vectors/receipt_valid_v1.json | 10 +++++----- test_vectors/receipt_wrong_kid.json | 10 +++++----- typescript-sdk/README.md | 13 ++++++++++-- typescript-sdk/scripts/unit-tests.mjs | 23 +++++++++++++++++++--- typescript-sdk/src/index.ts | 21 +++++++++++++++++--- 11 files changed, 77 insertions(+), 36 deletions(-) diff --git a/test_vectors/README.md b/test_vectors/README.md index 91cd403..2cfe9f6 100644 --- a/test_vectors/README.md +++ b/test_vectors/README.md @@ -1,6 +1,6 @@ # Test vectors -This directory contains shared receipt fixtures used by both SDKs and the parity check. The fixtures model the current Commons v1.1.0 receipt contract: `receipt` is canonical, `runtime_metadata` is unsigned, and the `x402` object is retained only as protocol metadata. +This directory contains shared receipt fixtures used by both SDKs and the parity check. The fixtures model the current Commons v1.1.0 runtime-aligned receipt contract: `receipt.verb` is canonical, `metadata.receipt_id` is a distinct receipt identifier, `metadata.proof.hash_sha256` is the integrity hash, `runtime_metadata` is unsigned, and the `x402` object is retained only as protocol metadata or legacy compatibility metadata. ## Files diff --git a/test_vectors/expected_hash.txt b/test_vectors/expected_hash.txt index 34034be..4e5da03 100644 --- a/test_vectors/expected_hash.txt +++ b/test_vectors/expected_hash.txt @@ -1 +1 @@ -509b71764d0158fab6bb073ed0bf28182bc1fa0227c00dc4ef79a0067e968196 +9a2bad8df55ceff4944485c83577741b777f3f474dc58e43fa9a6fd8f610afcf diff --git a/test_vectors/public_key_base64.txt b/test_vectors/public_key_base64.txt index ef2dcde..047b197 100644 --- a/test_vectors/public_key_base64.txt +++ b/test_vectors/public_key_base64.txt @@ -1 +1 @@ -A6EHv/POEL4dcN0Y50vAmWfk1jCbpQ1fHdyGZBJVMbg= +ebVWLo/mVPlAeLES6KmLp5AfhTrmlb7X4OORC60ElmQ= diff --git a/test_vectors/receipt_invalid_sig.json b/test_vectors/receipt_invalid_sig.json index c7ced1e..d6f8d9d 100644 --- a/test_vectors/receipt_invalid_sig.json +++ b/test_vectors/receipt_invalid_sig.json @@ -1,21 +1,21 @@ { "kid": "v1", "status": "success", + "verb": "summarize", "x402": { - "verb": "summarize", "version": "1.1.0" }, "result": { "summary": "fixture" }, "metadata": { + "receipt_id": "rcpt_20260322_fixture_invalid_sig_001", "proof": { "alg": "ed25519-sha256", "canonical": "cl-stable-json-v1", "signer_id": "runtime.commandlayer.eth", - "hash_sha256": "509b71764d0158fab6bb073ed0bf28182bc1fa0227c00dc4ef79a0067e968196", - "signature_b64": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" - }, - "receipt_id": "509b71764d0158fab6bb073ed0bf28182bc1fa0227c00dc4ef79a0067e968196" + "hash_sha256": "9a2bad8df55ceff4944485c83577741b777f3f474dc58e43fa9a6fd8f610afcf", + "signature_b64": "Azupi+yFGOtusVUP5WsI7MdKqedE/Dl8BftuuOfsYQkTeAzArNLtR3+Uq1BKbaGDzIvSEgocerx72L44AYdpCg=A" + } } } diff --git a/test_vectors/receipt_malformed_pubkey.json b/test_vectors/receipt_malformed_pubkey.json index bc6464d..084e53d 100644 --- a/test_vectors/receipt_malformed_pubkey.json +++ b/test_vectors/receipt_malformed_pubkey.json @@ -1,21 +1,21 @@ { "kid": "v1", "status": "success", + "verb": "summarize", "x402": { - "verb": "summarize", "version": "1.1.0" }, "result": { "summary": "fixture" }, "metadata": { + "receipt_id": "rcpt_20260322_fixture_malformed_pubkey_001", "proof": { "alg": "ed25519-sha256", "canonical": "cl-stable-json-v1", "signer_id": "runtime.commandlayer.eth", - "hash_sha256": "509b71764d0158fab6bb073ed0bf28182bc1fa0227c00dc4ef79a0067e968196", - "signature_b64": "RXtxKNIiU4uRSQoTK2wjByZnj3wJsA8g4+Ofuv+y3pUf/d57p1V9dL0vSoYUXLLMgPa7CpxKMsOl50AFhojcCw==" - }, - "receipt_id": "509b71764d0158fab6bb073ed0bf28182bc1fa0227c00dc4ef79a0067e968196" + "hash_sha256": "9a2bad8df55ceff4944485c83577741b777f3f474dc58e43fa9a6fd8f610afcf", + "signature_b64": "ozupi+yFGOtusVUP5WsI7MdKqedE/Dl8BftuuOfsYQkTeAzArNLtR3+Uq1BKbaGDzIvSEgocerx72L44AYdpCg==" + } } } diff --git a/test_vectors/receipt_valid.json b/test_vectors/receipt_valid.json index bc6464d..566ee91 100644 --- a/test_vectors/receipt_valid.json +++ b/test_vectors/receipt_valid.json @@ -1,21 +1,21 @@ { "kid": "v1", "status": "success", + "verb": "summarize", "x402": { - "verb": "summarize", "version": "1.1.0" }, "result": { "summary": "fixture" }, "metadata": { + "receipt_id": "rcpt_20260322_fixture_valid_001", "proof": { "alg": "ed25519-sha256", "canonical": "cl-stable-json-v1", "signer_id": "runtime.commandlayer.eth", - "hash_sha256": "509b71764d0158fab6bb073ed0bf28182bc1fa0227c00dc4ef79a0067e968196", - "signature_b64": "RXtxKNIiU4uRSQoTK2wjByZnj3wJsA8g4+Ofuv+y3pUf/d57p1V9dL0vSoYUXLLMgPa7CpxKMsOl50AFhojcCw==" - }, - "receipt_id": "509b71764d0158fab6bb073ed0bf28182bc1fa0227c00dc4ef79a0067e968196" + "hash_sha256": "9a2bad8df55ceff4944485c83577741b777f3f474dc58e43fa9a6fd8f610afcf", + "signature_b64": "ozupi+yFGOtusVUP5WsI7MdKqedE/Dl8BftuuOfsYQkTeAzArNLtR3+Uq1BKbaGDzIvSEgocerx72L44AYdpCg==" + } } } diff --git a/test_vectors/receipt_valid_v1.json b/test_vectors/receipt_valid_v1.json index bc6464d..430486b 100644 --- a/test_vectors/receipt_valid_v1.json +++ b/test_vectors/receipt_valid_v1.json @@ -1,21 +1,21 @@ { "kid": "v1", "status": "success", + "verb": "summarize", "x402": { - "verb": "summarize", "version": "1.1.0" }, "result": { "summary": "fixture" }, "metadata": { + "receipt_id": "rcpt_20260322_fixture_valid_v1_001", "proof": { "alg": "ed25519-sha256", "canonical": "cl-stable-json-v1", "signer_id": "runtime.commandlayer.eth", - "hash_sha256": "509b71764d0158fab6bb073ed0bf28182bc1fa0227c00dc4ef79a0067e968196", - "signature_b64": "RXtxKNIiU4uRSQoTK2wjByZnj3wJsA8g4+Ofuv+y3pUf/d57p1V9dL0vSoYUXLLMgPa7CpxKMsOl50AFhojcCw==" - }, - "receipt_id": "509b71764d0158fab6bb073ed0bf28182bc1fa0227c00dc4ef79a0067e968196" + "hash_sha256": "9a2bad8df55ceff4944485c83577741b777f3f474dc58e43fa9a6fd8f610afcf", + "signature_b64": "ozupi+yFGOtusVUP5WsI7MdKqedE/Dl8BftuuOfsYQkTeAzArNLtR3+Uq1BKbaGDzIvSEgocerx72L44AYdpCg==" + } } } diff --git a/test_vectors/receipt_wrong_kid.json b/test_vectors/receipt_wrong_kid.json index 282c795..e77b822 100644 --- a/test_vectors/receipt_wrong_kid.json +++ b/test_vectors/receipt_wrong_kid.json @@ -1,21 +1,21 @@ { "kid": "v2", "status": "success", + "verb": "summarize", "x402": { - "verb": "summarize", "version": "1.1.0" }, "result": { "summary": "fixture" }, "metadata": { + "receipt_id": "rcpt_20260322_fixture_wrong_kid_001", "proof": { "alg": "ed25519-sha256", "canonical": "cl-stable-json-v1", "signer_id": "runtime.commandlayer.eth", - "hash_sha256": "509b71764d0158fab6bb073ed0bf28182bc1fa0227c00dc4ef79a0067e968196", - "signature_b64": "RXtxKNIiU4uRSQoTK2wjByZnj3wJsA8g4+Ofuv+y3pUf/d57p1V9dL0vSoYUXLLMgPa7CpxKMsOl50AFhojcCw==" - }, - "receipt_id": "509b71764d0158fab6bb073ed0bf28182bc1fa0227c00dc4ef79a0067e968196" + "hash_sha256": "9a2bad8df55ceff4944485c83577741b777f3f474dc58e43fa9a6fd8f610afcf", + "signature_b64": "ozupi+yFGOtusVUP5WsI7MdKqedE/Dl8BftuuOfsYQkTeAzArNLtR3+Uq1BKbaGDzIvSEgocerx72L44AYdpCg==" + } } } diff --git a/typescript-sdk/README.md b/typescript-sdk/README.md index 1005e99..00b85d5 100644 --- a/typescript-sdk/README.md +++ b/typescript-sdk/README.md @@ -30,6 +30,7 @@ const response = await client.summarize({ }); console.log(response.receipt.result?.summary); +console.log(response.receipt.verb); console.log(response.receipt.metadata?.receipt_id); console.log(response.runtime_metadata?.duration_ms); @@ -48,8 +49,8 @@ Client methods return: { "receipt": { "status": "success", + "verb": "summarize", "x402": { - "verb": "summarize", "version": "1.1.0" }, "result": {}, @@ -72,7 +73,7 @@ Client methods return: } ``` -`verifyReceipt()` accepts the canonical `receipt` object. The retained `receipt.x402` block is Commons protocol metadata, not a commercial SDK surface. The SDK also accepts a whole response envelope for legacy compatibility, but new integrations should pass `response.receipt` explicitly. +`verifyReceipt()` accepts the canonical `receipt` object. The runtime-aligned receipt verb lives at `receipt.verb`; `receipt.x402.verb` is accepted only as a legacy fallback. `metadata.receipt_id` is a distinct receipt identifier, while `metadata.proof.hash_sha256` is the integrity hash that is recomputed and signature-verified. The retained `receipt.x402` block is Commons protocol metadata, not a primary SDK surface. The SDK also accepts a whole response envelope for legacy compatibility, but new integrations should pass `response.receipt` explicitly. ## Verification modes @@ -120,3 +121,11 @@ npm run typecheck npm test npm run test:integration ``` + +## Receipt verification semantics + +- `receipt.verb` is the canonical verb field returned by the runtime. +- `receipt.metadata.receipt_id` is an identifier for the receipt instance. +- `receipt.metadata.proof.hash_sha256` is the SHA-256 hash over the unsigned canonical receipt payload. +- `verifyReceipt()` succeeds when the declared algorithm/canonicalization match, the recomputed payload hash matches `hash_sha256`, and the Ed25519 signature validates over that hash. +- Legacy receipts that still place the verb under `receipt.x402.verb` continue to parse, but that path is fallback-only. diff --git a/typescript-sdk/scripts/unit-tests.mjs b/typescript-sdk/scripts/unit-tests.mjs index c1e349d..eeb3f52 100644 --- a/typescript-sdk/scripts/unit-tests.mjs +++ b/typescript-sdk/scripts/unit-tests.mjs @@ -196,7 +196,8 @@ await assertRejects( const receipt = { status: "success", - x402: { verb: "summarize", version: "1.1.0" }, + verb: "summarize", + x402: { version: "1.1.0" }, result: { summary: "test" }, metadata: { proof: { @@ -213,13 +214,15 @@ const receiptSig = nacl.sign.detached(new Uint8Array(receiptMsg), kp.secretKey); receipt.metadata.proof.hash_sha256 = hash_sha256; receipt.metadata.proof.signature_b64 = Buffer.from(receiptSig).toString("base64"); -receipt.metadata.receipt_id = hash_sha256; +receipt.metadata.receipt_id = "rcpt_unit_test_001"; const vr = await verifyReceipt(receipt, { publicKey: `ed25519:${b64Key}` }); assert(vr.ok === true, "verifyReceipt ok for valid receipt (explicit key)"); assert(vr.checks.hash_matches === true, "verifyReceipt hash matches"); assert(vr.checks.signature_valid === true, "verifyReceipt signature valid"); -assert(vr.checks.receipt_id_matches === true, "verifyReceipt receipt_id matches"); +assert(vr.checks.receipt_id_present === true, "verifyReceipt receipt_id present"); +assert(vr.checks.receipt_id_matches === false, "verifyReceipt does not require receipt_id to equal hash"); +assert(vr.values.verb === "summarize", "verifyReceipt reads top-level receipt verb"); const vrEns = await verifyReceipt(receipt, { ens: { @@ -248,6 +251,20 @@ try { assert(err instanceof CommandLayerError, "client.call rejects unknown verb with CommandLayerError"); } + + +const legacyVerbReceipt = JSON.parse(JSON.stringify(receipt)); +delete legacyVerbReceipt.verb; +legacyVerbReceipt.x402.verb = "summarize"; +const legacyVerbHash = recomputeReceiptHashSha256(legacyVerbReceipt).hash_sha256; +const legacyVerbSig = nacl.sign.detached(new Uint8Array(Buffer.from(legacyVerbHash, "utf8")), kp.secretKey); +legacyVerbReceipt.metadata.proof.hash_sha256 = legacyVerbHash; +legacyVerbReceipt.metadata.proof.signature_b64 = Buffer.from(legacyVerbSig).toString("base64"); +legacyVerbReceipt.metadata.receipt_id = "rcpt_unit_test_legacy_verb_001"; +const legacyVerbResult = await verifyReceipt(legacyVerbReceipt, { publicKey: `ed25519:${b64Key}` }); +assert(legacyVerbResult.ok === true, "verifyReceipt supports legacy x402.verb fallback"); +assert(legacyVerbResult.values.verb === "summarize", "verifyReceipt reports fallback verb value"); + // ---- Summary ---- console.log(`\n${passed} passed, ${failed} failed`); diff --git a/typescript-sdk/src/index.ts b/typescript-sdk/src/index.ts index 31249f8..ba711ed 100644 --- a/typescript-sdk/src/index.ts +++ b/typescript-sdk/src/index.ts @@ -26,7 +26,10 @@ export type ReceiptMetadata = { export type CanonicalReceipt = { status: "success" | "error" | string; + /** Canonical runtime receipt verb. */ + verb?: string; x402?: { + /** @deprecated Legacy fallback only. Prefer the top-level receipt.verb field. */ verb?: string; version?: string; entry?: string; @@ -60,6 +63,8 @@ export type CommandResponse = { export type VerifyChecks = { hash_matches: boolean; signature_valid: boolean; + receipt_id_present: boolean; + /** @deprecated Legacy compatibility signal only. New receipts do not require receipt_id === hash_sha256. */ receipt_id_matches: boolean; alg_matches: boolean; canonical_matches: boolean; @@ -253,6 +258,13 @@ export async function resolveSignerKey(name: string, rpcUrl: string): Promise