diff --git a/public/ambient-verification.html b/public/ambient-verification.html index c358835..ff9ae8f 100644 --- a/public/ambient-verification.html +++ b/public/ambient-verification.html @@ -119,6 +119,6 @@

Careful wording

- + diff --git a/public/api.html b/public/api.html index ee3eb0a..7029674 100644 --- a/public/api.html +++ b/public/api.html @@ -222,6 +222,6 @@

Next steps

- + diff --git a/public/docs.html b/public/docs.html index 42b254a..d72738f 100644 --- a/public/docs.html +++ b/public/docs.html @@ -131,6 +131,6 @@

Reference links

- + diff --git a/public/stack-proof-demo.html b/public/stack-proof-demo.html index 01e98d8..f3cc695 100644 --- a/public/stack-proof-demo.html +++ b/public/stack-proof-demo.html @@ -156,6 +156,6 @@

Signed. Verified. Tamper-invalidated.

What this does not claim

- + diff --git a/public/verify.html b/public/verify.html index 9762566..6599bd3 100644 --- a/public/verify.html +++ b/public/verify.html @@ -72,7 +72,7 @@ } }

Verification result states

VALID / VERIFIED = hash and signature checks passed. INVALID = hash mismatch, signature failure, unsupported proof, missing proof, or wrong signer/key. TRANSPORT_ERROR = verifier/runtime unavailable or request failed.

Manual verifier = paste a receipt and inspect checks. Automatic verification = SDKs, APIs, proof URLs, embedded badges, webhooks, MCP tools, or agent-to-agent flows verify receipts without manual copy/paste. MCP is a bridge, not a signer. Runtime signs. Verifier validates.

- + diff --git a/public/webhook-auto-verify.html b/public/webhook-auto-verify.html new file mode 100644 index 0000000..07c62c8 --- /dev/null +++ b/public/webhook-auto-verify.html @@ -0,0 +1,166 @@ + + + + + + Webhook Auto-Verify | CommandLayer + + + + + + + + + + +
+
Webhook Auto-Verify

Automatic verification without manual paste.

This demo shows a webhook-style flow where a receipt is generated, verified automatically, then tampered and rejected.

+

Live demo panel

Idle.

NOT_RUN
{}

Tampered verification result

NOT_RUN
{}
+

How this maps to a webhook

External system → sends { event, receipt } → webhook server calls verifier → accepted or rejected

The browser demo proves the same verification logic. The backend example in examples/webhook-auto-verify performs the check server-side with POST /webhook.

+

Backend example commands

cd examples/webhook-auto-verify
+npm install
+npm run check
+npm run generate:samples
+npm start
+
+curl -X POST http://localhost:3000/webhook \\
+  -H "Content-Type: application/json" \\
+  --data @sample-valid-webhook.json
+
+curl -X POST http://localhost:3000/webhook \\
+  -H "Content-Type: application/json" \\
+  --data @sample-tampered-webhook.json
+
+Expected:
+valid -> 200 accepted
+tampered -> 400 rejected
+
Runtime signs the original receipt.
Verifier recomputes canonical hash.
Verifier checks Ed25519 signature.
Tampering changes payload and invalidates proof.
No manual paste is required.
+

What this does not replace

  • Receipt verification proves receipt integrity.
  • It does not replace webhook sender authentication.
  • Production webhooks still need sender auth, replay protection, timestamps, idempotency, and rate limiting.
  • Schema-valid alone is not verified.
  • MCP is a bridge, not a signer.
+
Runtime signs.
Verifier validates.
MCP bridges.
SDK wraps.
Schemas describe.
+
+
+ + + + diff --git a/tests/api-agents-verifyagent.test.js b/tests/api-agents-verifyagent.test.js index 5862bb6..5711a3b 100644 --- a/tests/api-agents-verifyagent.test.js +++ b/tests/api-agents-verifyagent.test.js @@ -19,10 +19,10 @@ function makeRes() { } const sampleReceipt = JSON.parse( - fs.readFileSync(path.join(__dirname, '..', 'examples', 'sample-receipt.json'), 'utf8') + fs.readFileSync(path.join(__dirname, 'fixtures', 'canonical-receipt.sample.json'), 'utf8') ); -test('POST /api/agents/verifyagent with legacy sample => INVALID', async () => { +test('POST /api/agents/verifyagent with canonical sample fixture => INVALID', async () => { const req = { method: 'POST', body: { task: 'verify this receipt', receipt: sampleReceipt } }; const res = makeRes(); diff --git a/tests/api-verify.test.js b/tests/api-verify.test.js index ae618aa..6221d11 100644 --- a/tests/api-verify.test.js +++ b/tests/api-verify.test.js @@ -19,10 +19,10 @@ function makeRes() { } const sampleReceipt = JSON.parse( - fs.readFileSync(path.join(__dirname, '..', 'examples', 'sample-receipt.json'), 'utf8') + fs.readFileSync(path.join(__dirname, 'fixtures', 'canonical-receipt.sample.json'), 'utf8') ); -test('POST /api/verify with legacy sample => INVALID', async () => { +test('POST /api/verify with canonical sample fixture => INVALID', async () => { const req = { method: 'POST', body: sampleReceipt }; const res = makeRes(); @@ -31,12 +31,11 @@ test('POST /api/verify with legacy sample => INVALID', async () => { assert.equal(res.statusCode, 200); assert.equal(res.body.ok, false); assert.equal(res.body.status, 'INVALID'); - assert.equal(res.body.debug.has_legacy_top_level_proof, true); -}); + }); -test('POST /api/verify with wrapped legacy receipt payload => INVALID', async () => { +test('POST /api/verify with wrapped canonical sample payload => INVALID', async () => { const req = { method: 'POST', body: { receipt: sampleReceipt } }; const res = makeRes(); diff --git a/tests/commons-flow.test.js b/tests/commons-flow.test.js index 9b27681..0f094b9 100644 --- a/tests/commons-flow.test.js +++ b/tests/commons-flow.test.js @@ -86,7 +86,7 @@ test('canonicalReceiptPayload: extracts only the six canonical fields', () => { }); test('sha256 of canonicalized payload matches known sample-receipt hash', () => { - // These values are taken from examples/sample-receipt.json + // These values are taken from tests/fixtures/canonical-receipt.sample.json const receipt = { signer: 'runtime.commandlayer.eth', verb: 'agent.execute', diff --git a/tests/fixtures/canonical-receipt.sample.json b/tests/fixtures/canonical-receipt.sample.json new file mode 100644 index 0000000..d5a5dfe --- /dev/null +++ b/tests/fixtures/canonical-receipt.sample.json @@ -0,0 +1,31 @@ +{ + "signer": "runtime.commandlayer.eth", + "verb": "agent.execute", + "ts": "2026-05-20T00:00:00.000Z", + "input": { + "task": "verify", + "content": "canonical" + }, + "output": { + "ok": true + }, + "execution": { + "runtime": "prod", + "run_id": "run_1" + }, + "metadata": { + "proof": { + "canonicalization": "json.sorted_keys.v1", + "hash": { + "alg": "SHA-256", + "value": "8035378e3f1f4d2ae3f5c8f4a8767e8fc99695f5f3d17d5d49f75f53dceb0f73" + }, + "signature": { + "alg": "Ed25519", + "kid": "vC4WbcNoq2znSCiQ", + "value": "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==" + }, + "signer_id": "runtime.commandlayer.eth" + } + } +}