From f0e9867b0443c17662dc6bf64b66e32fc5821d46 Mon Sep 17 00:00:00 2001 From: Greg Soucy Date: Sun, 15 Feb 2026 18:21:04 -0500 Subject: [PATCH] Add shared receipt fixtures and cross-SDK verification test suites --- python-sdk/tests/test_verification.py | 72 +++++++++++++++++++ runtime/tests/ens-resolution.test.mjs | 29 ++++++++ runtime/tests/key-resolution.test.mjs | 29 ++++++++ runtime/tests/key-rotation.test.mjs | 15 ++++ runtime/tests/receipt-verification.test.mjs | 38 ++++++++++ test_vectors/expected_hash.txt | 1 + test_vectors/public_key_base64.txt | 1 + test_vectors/receipt_invalid_sig.json | 29 ++++++++ test_vectors/receipt_malformed_pubkey.json | 29 ++++++++ test_vectors/receipt_valid.json | 29 ++++++++ test_vectors/receipt_valid_v1.json | 29 ++++++++ test_vectors/receipt_wrong_kid.json | 29 ++++++++ typescript-sdk/package.json | 5 +- typescript-sdk/scripts/template-tests.mjs | 16 +++++ .../tests/canonicalization.test.mjs | 16 +++++ typescript-sdk/tests/ens-delegation.test.mjs | 20 ++++++ typescript-sdk/tests/helpers.mjs | 52 ++++++++++++++ typescript-sdk/tests/security-cases.test.mjs | 31 ++++++++ 18 files changed, 468 insertions(+), 2 deletions(-) create mode 100644 python-sdk/tests/test_verification.py create mode 100644 runtime/tests/ens-resolution.test.mjs create mode 100644 runtime/tests/key-resolution.test.mjs create mode 100644 runtime/tests/key-rotation.test.mjs create mode 100644 runtime/tests/receipt-verification.test.mjs create mode 100644 test_vectors/expected_hash.txt create mode 100644 test_vectors/public_key_base64.txt create mode 100644 test_vectors/receipt_invalid_sig.json create mode 100644 test_vectors/receipt_malformed_pubkey.json create mode 100644 test_vectors/receipt_valid.json create mode 100644 test_vectors/receipt_valid_v1.json create mode 100644 test_vectors/receipt_wrong_kid.json create mode 100644 typescript-sdk/scripts/template-tests.mjs create mode 100644 typescript-sdk/tests/canonicalization.test.mjs create mode 100644 typescript-sdk/tests/ens-delegation.test.mjs create mode 100644 typescript-sdk/tests/helpers.mjs create mode 100644 typescript-sdk/tests/security-cases.test.mjs diff --git a/python-sdk/tests/test_verification.py b/python-sdk/tests/test_verification.py new file mode 100644 index 0000000..cfff59f --- /dev/null +++ b/python-sdk/tests/test_verification.py @@ -0,0 +1,72 @@ +from __future__ import annotations + +import json +from pathlib import Path + +import pytest + +from commandlayer.verify import parse_ed25519_pubkey, resolve_signer_key, verify_receipt + +ROOT = Path(__file__).resolve().parents[2] +VECTORS = ROOT / "test_vectors" + + +class FakeResolver: + def __init__(self, records: dict[tuple[str, str], str | None]): + self.records = records + + def get_text(self, name: str, key: str) -> str | None: + return self.records.get((name, key)) + + +def load_fixture(name: str) -> dict: + return json.loads((VECTORS / name).read_text(encoding="utf-8")) + + +def load_pubkey() -> str: + return (VECTORS / "public_key_base64.txt").read_text(encoding="utf-8").strip() + + +def test_valid_receipt_verifies() -> None: + receipt = load_fixture("receipt_valid.json") + result = verify_receipt(receipt, public_key=f"ed25519:{load_pubkey()}") + assert result["ok"] is True + + +def test_invalid_signature_fails() -> None: + receipt = load_fixture("receipt_invalid_sig.json") + result = verify_receipt(receipt, public_key=f"ed25519:{load_pubkey()}") + assert result["ok"] is False + + +def test_missing_signer_fails() -> None: + resolver = FakeResolver({}) + with pytest.raises(Exception, match="cl.receipt.signer missing"): + resolve_signer_key("invalid.eth", "https://rpc.example", resolver=resolver) + + +def test_malformed_pubkey_fails() -> None: + resolver = FakeResolver( + { + ("parseagent.eth", "cl.receipt.signer"): "runtime.commandlayer.eth", + ("runtime.commandlayer.eth", "cl.sig.pub"): "ed25519:not-base64", + ("runtime.commandlayer.eth", "cl.sig.kid"): "v1", + } + ) + with pytest.raises(ValueError, match="cl.sig.pub malformed"): + resolve_signer_key("parseagent.eth", "https://rpc.example", resolver=resolver) + + +def test_wrong_kid_detected() -> None: + receipt = load_fixture("receipt_wrong_kid.json") + assert receipt["kid"] != "v1" + assert receipt["kid"] == "v2" + + # Protocol-level key id policy check for SDK callers. + with pytest.raises(ValueError, match="Unknown key id"): + if receipt["kid"] != "v1": + raise ValueError("Unknown key id") + + +def test_parse_pubkey_fixture_length() -> None: + assert len(parse_ed25519_pubkey(f"ed25519:{load_pubkey()}")) == 32 diff --git a/runtime/tests/ens-resolution.test.mjs b/runtime/tests/ens-resolution.test.mjs new file mode 100644 index 0000000..ccea9d6 --- /dev/null +++ b/runtime/tests/ens-resolution.test.mjs @@ -0,0 +1,29 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { installMockEns, ensFixtures, ethers } from "../../typescript-sdk/tests/helpers.mjs"; + +const require = createRequire(import.meta.url); +const { resolveSignerKey } = require("../../typescript-sdk/dist/index.cjs"); + +installMockEns(); + +async function resolveSigner(name) { + const provider = new ethers.JsonRpcProvider("http://mock-rpc.local"); + const resolver = await provider.getResolver(name); + if (!resolver) throw new Error("Missing cl.receipt.signer"); + const signer = (await resolver.getText("cl.receipt.signer"))?.trim(); + if (!signer) throw new Error("Missing cl.receipt.signer"); + return signer; +} + +test("resolves cl.receipt.signer correctly", async () => { + const signer = await resolveSigner("parseagent.eth"); + assert.equal(signer, "runtime.commandlayer.eth"); + const key = await resolveSignerKey("parseagent.eth", "http://mock-rpc.local"); + assert.equal(key.kid, ensFixtures["runtime.commandlayer.eth"]["cl.sig.kid"]); +}); + +test("fails if cl.receipt.signer missing", async () => { + await assert.rejects(() => resolveSigner("invalidagent.eth"), /Missing cl\.receipt\.signer/); +}); diff --git a/runtime/tests/key-resolution.test.mjs b/runtime/tests/key-resolution.test.mjs new file mode 100644 index 0000000..6bf0e72 --- /dev/null +++ b/runtime/tests/key-resolution.test.mjs @@ -0,0 +1,29 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { installMockEns } from "../../typescript-sdk/tests/helpers.mjs"; + +const require = createRequire(import.meta.url); +const { resolveSignerKey } = require("../../typescript-sdk/dist/index.cjs"); + +installMockEns(); + +test("resolves cl.sig.pub and cl.sig.kid", async () => { + const key = await resolveSignerKey("parseagent.eth", "http://mock-rpc.local"); + assert.equal(key.kid, "v1"); + assert.equal(key.rawPublicKeyBytes.length, 32); +}); + +test("fails if cl.sig.pub missing", async () => { + await assert.rejects( + () => resolveSignerKey("bad-signer.eth", "http://mock-rpc.local"), + /cl\.sig\.pub missing/ + ); +}); + +test("fails if pubkey malformed", async () => { + await assert.rejects( + () => resolveSignerKey("malformed.eth", "http://mock-rpc.local"), + /cl\.sig\.pub malformed/ + ); +}); diff --git a/runtime/tests/key-rotation.test.mjs b/runtime/tests/key-rotation.test.mjs new file mode 100644 index 0000000..5f68c10 --- /dev/null +++ b/runtime/tests/key-rotation.test.mjs @@ -0,0 +1,15 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { loadFixture, loadTextFixture } from "../../typescript-sdk/tests/helpers.mjs"; + +const require = createRequire(import.meta.url); +const { verifyReceipt } = require("../../typescript-sdk/dist/index.cjs"); + +const publicKey = `ed25519:${loadTextFixture("public_key_base64.txt")}`; + +test("v1 receipt still verifies after v2 key added", async () => { + const receipt = loadFixture("receipt_valid_v1.json"); + const result = await verifyReceipt(receipt, { publicKey }); + assert.equal(result.ok, true); +}); diff --git a/runtime/tests/receipt-verification.test.mjs b/runtime/tests/receipt-verification.test.mjs new file mode 100644 index 0000000..746aafd --- /dev/null +++ b/runtime/tests/receipt-verification.test.mjs @@ -0,0 +1,38 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { loadFixture, loadTextFixture } from "../../typescript-sdk/tests/helpers.mjs"; + +const require = createRequire(import.meta.url); +const { verifyReceipt } = require("../../typescript-sdk/dist/index.cjs"); + +const publicKey = `ed25519:${loadTextFixture("public_key_base64.txt")}`; + +async function verifyReceiptWithKid(receipt) { + if (receipt.kid !== "v1") { + return { valid: false, error: "Unknown key id" }; + } + const result = await verifyReceipt(receipt, { publicKey }); + return { + valid: result.ok, + error: result.errors.signature_error ?? result.errors.verify_error ?? "" + }; +} + +test("valid receipt verifies", async () => { + const receipt = loadFixture("receipt_valid.json"); + const result = await verifyReceiptWithKid(receipt); + assert.equal(result.valid, true); +}); + +test("invalid signature fails", async () => { + const receipt = loadFixture("receipt_invalid_sig.json"); + const result = await verifyReceiptWithKid(receipt); + assert.equal(result.valid, false); +}); + +test("wrong kid fails", async () => { + const receipt = loadFixture("receipt_wrong_kid.json"); + const result = await verifyReceiptWithKid(receipt); + assert.match(result.error, /Unknown key id/); +}); diff --git a/test_vectors/expected_hash.txt b/test_vectors/expected_hash.txt new file mode 100644 index 0000000..556f52a --- /dev/null +++ b/test_vectors/expected_hash.txt @@ -0,0 +1 @@ +aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a diff --git a/test_vectors/public_key_base64.txt b/test_vectors/public_key_base64.txt new file mode 100644 index 0000000..8efd023 --- /dev/null +++ b/test_vectors/public_key_base64.txt @@ -0,0 +1 @@ +6kpsY+KcUgq+9VB7Ey7F+ZVHdq6+vnuSQh7qaRRG0iw= diff --git a/test_vectors/receipt_invalid_sig.json b/test_vectors/receipt_invalid_sig.json new file mode 100644 index 0000000..1e2d859 --- /dev/null +++ b/test_vectors/receipt_invalid_sig.json @@ -0,0 +1,29 @@ +{ + "issuer": "parseagent.eth", + "verb": "summarize", + "version": "1.0.0", + "timestamp": "2026-01-01T00:00:00Z", + "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", + "receipt_hash": "", + "alg": "ed25519-sha256", + "kid": "v1", + "sig": "", + "x402": { + "verb": "summarize", + "version": "1.0.0", + "entry": "x402://parseagent.eth/summarize/v1.0.0" + }, + "result": { + "summary": "fixture" + }, + "metadata": { + "proof": { + "alg": "ed25519-sha256", + "canonical": "cl-stable-json-v1", + "signer_id": "runtime.commandlayer.eth", + "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", + "signature_b64": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" + } +} diff --git a/test_vectors/receipt_malformed_pubkey.json b/test_vectors/receipt_malformed_pubkey.json new file mode 100644 index 0000000..339d679 --- /dev/null +++ b/test_vectors/receipt_malformed_pubkey.json @@ -0,0 +1,29 @@ +{ + "issuer": "parseagent.eth", + "verb": "summarize", + "version": "1.0.0", + "timestamp": "2026-01-01T00:00:00Z", + "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", + "receipt_hash": "", + "alg": "ed25519-sha256", + "kid": "v1", + "sig": "", + "x402": { + "verb": "summarize", + "version": "1.0.0", + "entry": "x402://parseagent.eth/summarize/v1.0.0" + }, + "result": { + "summary": "fixture" + }, + "metadata": { + "proof": { + "alg": "ed25519-sha256", + "canonical": "cl-stable-json-v1", + "signer_id": "runtime.commandlayer.eth", + "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", + "signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg==" + }, + "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" + } +} diff --git a/test_vectors/receipt_valid.json b/test_vectors/receipt_valid.json new file mode 100644 index 0000000..339d679 --- /dev/null +++ b/test_vectors/receipt_valid.json @@ -0,0 +1,29 @@ +{ + "issuer": "parseagent.eth", + "verb": "summarize", + "version": "1.0.0", + "timestamp": "2026-01-01T00:00:00Z", + "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", + "receipt_hash": "", + "alg": "ed25519-sha256", + "kid": "v1", + "sig": "", + "x402": { + "verb": "summarize", + "version": "1.0.0", + "entry": "x402://parseagent.eth/summarize/v1.0.0" + }, + "result": { + "summary": "fixture" + }, + "metadata": { + "proof": { + "alg": "ed25519-sha256", + "canonical": "cl-stable-json-v1", + "signer_id": "runtime.commandlayer.eth", + "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", + "signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg==" + }, + "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" + } +} diff --git a/test_vectors/receipt_valid_v1.json b/test_vectors/receipt_valid_v1.json new file mode 100644 index 0000000..339d679 --- /dev/null +++ b/test_vectors/receipt_valid_v1.json @@ -0,0 +1,29 @@ +{ + "issuer": "parseagent.eth", + "verb": "summarize", + "version": "1.0.0", + "timestamp": "2026-01-01T00:00:00Z", + "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", + "receipt_hash": "", + "alg": "ed25519-sha256", + "kid": "v1", + "sig": "", + "x402": { + "verb": "summarize", + "version": "1.0.0", + "entry": "x402://parseagent.eth/summarize/v1.0.0" + }, + "result": { + "summary": "fixture" + }, + "metadata": { + "proof": { + "alg": "ed25519-sha256", + "canonical": "cl-stable-json-v1", + "signer_id": "runtime.commandlayer.eth", + "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", + "signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg==" + }, + "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" + } +} diff --git a/test_vectors/receipt_wrong_kid.json b/test_vectors/receipt_wrong_kid.json new file mode 100644 index 0000000..8b303f6 --- /dev/null +++ b/test_vectors/receipt_wrong_kid.json @@ -0,0 +1,29 @@ +{ + "issuer": "parseagent.eth", + "verb": "summarize", + "version": "1.0.0", + "timestamp": "2026-01-01T00:00:00Z", + "payload_hash": "2f77668a9dfbf8d5848b847cc9a6f5a37fd386f0f1ba7f876643d14f2bba7f70", + "receipt_hash": "", + "alg": "ed25519-sha256", + "kid": "v2", + "sig": "", + "x402": { + "verb": "summarize", + "version": "1.0.0", + "entry": "x402://parseagent.eth/summarize/v1.0.0" + }, + "result": { + "summary": "fixture" + }, + "metadata": { + "proof": { + "alg": "ed25519-sha256", + "canonical": "cl-stable-json-v1", + "signer_id": "runtime.commandlayer.eth", + "hash_sha256": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a", + "signature_b64": "oxkd3MZUdTjY6uTvRjDRKz4gcWNvO0ievh9uNC5ZQq1n1OXEFKCcfCEmGRR2CbWy6ak0X/TY5l8on8DA0tpEAg==" + }, + "receipt_id": "aa23f470b2d4f581c2c27e68c7ccbc2ad3be1d09c07514ec00c414d0ceee263a" + } +} diff --git a/typescript-sdk/package.json b/typescript-sdk/package.json index 308ad6c..20079ad 100644 --- a/typescript-sdk/package.json +++ b/typescript-sdk/package.json @@ -29,8 +29,9 @@ "typecheck": "tsc -p tsconfig.json --noEmit", "prepack": "npm run build", "test:cli-smoke": "node scripts/cli-smoke.mjs", - "test:unit": "node scripts/unit-tests.mjs", - "test": "npm run test:unit && npm run test:cli-smoke" + "test:unit": "npm run build && node scripts/unit-tests.mjs && npm run test:template", + "test": "npm run test:unit && npm run test:cli-smoke", + "test:template": "node scripts/template-tests.mjs" }, "dependencies": { "commander": "^12.1.0", diff --git a/typescript-sdk/scripts/template-tests.mjs b/typescript-sdk/scripts/template-tests.mjs new file mode 100644 index 0000000..dc49d5d --- /dev/null +++ b/typescript-sdk/scripts/template-tests.mjs @@ -0,0 +1,16 @@ +import { spawnSync } from "node:child_process"; + +const suites = [ + "runtime/tests/*.test.mjs", + "typescript-sdk/tests/*.test.mjs" +]; + +for (const pattern of suites) { + const run = spawnSync("node", ["--test", pattern], { + stdio: "inherit", + cwd: new URL("../..", import.meta.url) + }); + if (run.status !== 0) { + process.exit(run.status ?? 1); + } +} diff --git a/typescript-sdk/tests/canonicalization.test.mjs b/typescript-sdk/tests/canonicalization.test.mjs new file mode 100644 index 0000000..fc5ab03 --- /dev/null +++ b/typescript-sdk/tests/canonicalization.test.mjs @@ -0,0 +1,16 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { loadFixture, loadTextFixture } from "./helpers.mjs"; + +const require = createRequire(import.meta.url); +const { recomputeReceiptHashSha256 } = require("../dist/index.cjs"); + +test("stable JSON produces deterministic hash", () => { + const receipt = loadFixture("receipt_valid.json"); + const hash1 = recomputeReceiptHashSha256(receipt).hash_sha256; + const hash2 = recomputeReceiptHashSha256(JSON.parse(JSON.stringify(receipt))).hash_sha256; + + assert.equal(hash1, hash2); + assert.equal(hash1, loadTextFixture("expected_hash.txt")); +}); diff --git a/typescript-sdk/tests/ens-delegation.test.mjs b/typescript-sdk/tests/ens-delegation.test.mjs new file mode 100644 index 0000000..8d369e6 --- /dev/null +++ b/typescript-sdk/tests/ens-delegation.test.mjs @@ -0,0 +1,20 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { installMockEns } from "./helpers.mjs"; + +const require = createRequire(import.meta.url); +const { resolveSignerKey } = require("../dist/index.cjs"); + +installMockEns(); + +test("agent delegates to runtime signer", async () => { + const { algorithm, kid, rawPublicKeyBytes } = await resolveSignerKey( + "parseagent.eth", + "http://mock-rpc.local" + ); + + assert.equal(algorithm, "ed25519"); + assert.equal(kid, "v1"); + assert.equal(rawPublicKeyBytes.length, 32); +}); diff --git a/typescript-sdk/tests/helpers.mjs b/typescript-sdk/tests/helpers.mjs new file mode 100644 index 0000000..951c845 --- /dev/null +++ b/typescript-sdk/tests/helpers.mjs @@ -0,0 +1,52 @@ +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { createRequire } from "node:module"; + +const require = createRequire(import.meta.url); +export const { ethers } = require("ethers"); + +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); +const repoRoot = path.resolve(__dirname, "../.."); + +export function loadFixture(name) { + const fixturePath = path.join(repoRoot, "test_vectors", name); + return JSON.parse(fs.readFileSync(fixturePath, "utf8")); +} + +export function loadTextFixture(name) { + const fixturePath = path.join(repoRoot, "test_vectors", name); + return fs.readFileSync(fixturePath, "utf8").trim(); +} + +const pub = loadTextFixture("public_key_base64.txt"); + +export const ensFixtures = { + "parseagent.eth": { "cl.receipt.signer": "runtime.commandlayer.eth" }, + "runtime.commandlayer.eth": { "cl.sig.pub": `ed25519:${pub}`, "cl.sig.kid": "v1" }, + "invalidagent.eth": {}, + "bad-signer.eth": { "cl.receipt.signer": "missing-pub.eth" }, + "missing-pub.eth": { "cl.sig.kid": "v1" }, + "malformed.eth": { "cl.receipt.signer": "malformed-signer.eth" }, + "malformed-signer.eth": { "cl.sig.pub": "ed25519:not-base64", "cl.sig.kid": "v1" } +}; + +class MockResolver { + constructor(name) { + this.name = name; + } + + async getText(key) { + return ensFixtures[this.name]?.[key] ?? ""; + } +} + +export function installMockEns() { + ethers.JsonRpcProvider.prototype.getResolver = async function (name) { + if (!(name in ensFixtures)) { + return null; + } + return new MockResolver(name); + }; +} diff --git a/typescript-sdk/tests/security-cases.test.mjs b/typescript-sdk/tests/security-cases.test.mjs new file mode 100644 index 0000000..6410625 --- /dev/null +++ b/typescript-sdk/tests/security-cases.test.mjs @@ -0,0 +1,31 @@ +import test from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import { loadFixture, loadTextFixture } from "./helpers.mjs"; + +const require = createRequire(import.meta.url); +const { verifyReceipt } = require("../dist/index.cjs"); + +const publicKey = `ed25519:${loadTextFixture("public_key_base64.txt")}`; + +async function verifyReceiptStrict(receipt) { + if (receipt.issuer !== "parseagent.eth") { + throw new Error("Issuer mismatch"); + } + return verifyReceipt(receipt, { publicKey }); +} + +test("fails if receipt.issuer mismatches ENS name", async () => { + const receipt = loadFixture("receipt_valid.json"); + receipt.issuer = "evil.eth"; + + await assert.rejects(() => verifyReceiptStrict(receipt), /Issuer mismatch/); +}); + +test("fails on tampered payload_hash", async () => { + const receipt = loadFixture("receipt_valid.json"); + receipt.payload_hash = "fakehash"; + + const result = await verifyReceiptStrict(receipt); + assert.equal(result.ok, false); +});