Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
# Changelog

All notable changes to this runtime repository will be documented in this file.

## Unreleased

- Adds production-surface tests for receipt signing and `POST /verify` behavior in `server.mjs`.
- Removes the in-repo `sdk/` subtree so this repository stays scoped to the runtime service layer.

## v1.0.0

- Establishes the Node.js CommandLayer runtime service for versioned verb execution and signed receipts.
- Exposes the runtime health surface (`GET /health` and `GET /healthz`) and receipt verification via `POST /verify`.
- Includes repo-local configuration, smoke coverage, and operational documentation for running the runtime as a service.
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -170,11 +170,11 @@ This repository's GitHub Actions workflows currently run:

`npm test` runs Node unit tests plus `tests/smoke.mjs`.

## Legacy verification code
## Verification coverage

This repository still contains `runtime/src/receipt-verification.js` and related tests under `runtime/tests/` and `sdk/typescript-sdk/tests/`.
The production verification path is `server.mjs`, which signs receipts and verifies them via `@commandlayer/runtime-core`.

That file is explicitly legacy compatibility code for older fixture-based verification coverage in this repository. It is not the production verification path used by `server.mjs`, which imports signing and verification from `@commandlayer/runtime-core`.
Repo-local test coverage now includes runtime service tests that exercise the receipt production path and `POST /verify` behavior directly, alongside the remaining legacy helper coverage under `runtime/tests/`.

## Configuration and operations

Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
"check": "node --check server.mjs",
"test": "npm run test:unit && node tests/smoke.mjs",
"ci": "npm run check && npm test",
"test:unit": "node --test runtime/tests/*.test.mjs sdk/typescript-sdk/tests/*.test.mjs"
"test:unit": "node --test runtime/tests/*.test.mjs"
},
"dependencies": {
"@commandlayer/runtime-core": "github:commandlayer/runtime-core#main",
Expand Down
2 changes: 1 addition & 1 deletion runtime/src/receipt-verification.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import crypto from "node:crypto";

/**
* Legacy compatibility verifier used only by repo-local fixtures and SDK parity tests.
* Legacy compatibility verifier used only by repo-local fixtures and compatibility tests.
*
* Production runtime verification flows through @commandlayer/runtime-core via server.mjs.
* Keep this file only for older compatibility material in this repository; do not treat it
Expand Down
164 changes: 118 additions & 46 deletions runtime/tests/runtime-signing.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -49,12 +49,24 @@ async function startServer(extraEnv) {
throw new Error(`server did not boot: ${stderr}`);
}


function unwrapReceiptResponse(payload) {
return { receipt: payload?.receipt || payload, runtimeMetadata: payload?.runtime_metadata || null };
}


async function createDescribeReceipt(base) {
const body = {
x402: { verb: "describe", version: "1.1.0", entry: "x402://describeagent.eth/describe/v1.1.0" },
input: { subject: "t", detail_level: "short" },
trace: { provider: "test" },
};
const receiptResp = await fetch(`${base}/describe/v1.1.0`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
assert.equal(receiptResp.status, 200);
return receiptResp.json();
}

async function stop(proc) {
if (proc.exitCode !== null) return;
Expand All @@ -74,7 +86,7 @@ test("boot fails fast without keys unless DEV_AUTO_KEYS=1", async () => {
assert.match(`${res.stderr}${res.stdout}`, /fatal signer misconfiguration|Missing required env var/);
});

test("private PEM_B64 + public raw32 b64 path signs and /verify roundtrip works", async () => {
test("makeReceipt production path emits signed receipts with runtime kid and canonical fields", async () => {
const keys = makeKeys();
const srv = await startServer({
RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64,
Expand All @@ -87,55 +99,91 @@ test("private PEM_B64 + public raw32 b64 path signs and /verify roundtrip works"
assert.equal(h.signer_ok, true);
assert.equal(h.kid, keys.kid);

const body = {
x402: { verb: "describe", version: "1.1.0", entry: "x402://describeagent.eth/describe/v1.1.0" },
input: { subject: "t", detail_level: "short" },
trace: { provider: "test" },
};
const receiptResp = await fetch(`${srv.base}/describe/v1.1.0`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
});
assert.equal(receiptResp.status, 200);
const response = await receiptResp.json();
const response = await createDescribeReceipt(srv.base);
const { receipt, runtimeMetadata } = unwrapReceiptResponse(response);

assert.equal(runtimeMetadata?.trace?.provider, "runtime");
assert.ok(receipt.metadata?.proof?.signature_b64);
assert.ok(receipt.metadata?.proof?.hash_sha256);
assert.equal(receipt.metadata?.proof?.alg, "ed25519-sha256");
assert.equal(receipt.metadata?.proof?.signer_id, "runtime.commandlayer.eth");
assert.equal(receipt.metadata?.proof?.kid, keys.kid);
assert.equal(receipt.metadata?.proof?.canonical, "json.sorted_keys.v1");
assert.equal(receipt.metadata?.proof?.canonical_id, "json.sorted_keys.v1");
assert.match(receipt.metadata?.proof?.hash_sha256, /^[a-f0-9]{64}$/);
assert.ok(receipt.metadata?.proof?.signature_b64);
} finally {
await stop(srv.proc);
}
});

test("/verify accepts both wrapped and bare receipts from the production signing path", async () => {
const keys = makeKeys();
const srv = await startServer({
RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64,
RECEIPT_SIGNING_PUBLIC_KEY_B64: keys.publicRaw32B64,
RECEIPT_SIGNER_ID: "runtime.commandlayer.eth",
});

try {
const response = await createDescribeReceipt(srv.base);
const { receipt } = unwrapReceiptResponse(response);

for (const payload of [response, receipt]) {
const verifyResp = await fetch(`${srv.base}/verify`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(payload),
});
const verifyJson = await verifyResp.json();
assert.equal(verifyResp.status, 200);
assert.equal(verifyJson.ok, true);
assert.equal(verifyJson.verified_with, "env");
assert.equal(verifyJson.checks?.hash_matches, true);
assert.equal(verifyJson.checks?.signature_valid, true);
assert.equal(verifyJson.values?.kid, keys.kid);
assert.equal(verifyJson.values?.canonical_id, "json.sorted_keys.v1");
}
} finally {
await stop(srv.proc);
}
});

test("/verify reports a hash/signature failure when a signed production receipt is tampered", async () => {
const keys = makeKeys();
const srv = await startServer({
RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64,
RECEIPT_SIGNING_PUBLIC_KEY_B64: keys.publicRaw32B64,
RECEIPT_SIGNER_ID: "runtime.commandlayer.eth",
});

try {
const response = await createDescribeReceipt(srv.base);
const { receipt } = unwrapReceiptResponse(response);
const tampered = structuredClone(receipt);
tampered.result.summary = "tampered after signing";

const verifyResp = await fetch(`${srv.base}/verify`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(response),
body: JSON.stringify(tampered),
});
const verifyJson = await verifyResp.json();
assert.equal(verifyResp.status, 200);
assert.equal(verifyJson.ok, true);
assert.equal(verifyJson.verified_with, "env");

const verifyBareResp = await fetch(`${srv.base}/verify`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(receipt),
});
const verifyBareJson = await verifyBareResp.json();
assert.equal(verifyBareResp.status, 200);
assert.equal(verifyBareJson.ok, true);
assert.equal(verifyBareJson.verified_with, "env");
assert.equal(verifyResp.status, 200);
assert.equal(verifyJson.ok, false);
assert.equal(verifyJson.checks?.hash_matches, false);
assert.equal(verifyJson.checks?.signature_valid, false);
assert.match(String(verifyJson.errors?.signature_error), /hash_mismatch|verify failed/);
} finally {
await stop(srv.proc);
}
});

test("/verify?ens=1 passes with mocked ENS TXT response", async () => {
test("/verify?ens=1 passes with mocked ENS TXT response and preserves current kid behavior", async () => {
const keys = makeKeys();
const ensMock = JSON.stringify({
"cl.sig.pub": `ed25519:${keys.publicRaw32B64}`,
"cl.sig.canonical": "json.sorted_keys.v1",
"cl.sig.kid": "v1",
"cl.sig.kid": keys.kid,
});
const srv = await startServer({
RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64,
Expand All @@ -145,21 +193,9 @@ test("/verify?ens=1 passes with mocked ENS TXT response", async () => {
});

try {
const body = {
x402: { verb: "describe", version: "1.1.0", entry: "x402://describeagent.eth/describe/v1.1.0" },
input: { subject: "t", detail_level: "short" },
trace: { provider: "test" },
};
const response = await (
await fetch(`${srv.base}/describe/v1.1.0`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(body),
})
).json();
const { receipt } = unwrapReceiptResponse(response);
const response = await createDescribeReceipt(srv.base);

const verifyResp = await fetch(`${srv.base}/verify?ens=1`, {
const verifyResp = await fetch(`${srv.base}/verify?ens=1&strict_kid=1`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(response),
Expand All @@ -168,11 +204,47 @@ test("/verify?ens=1 passes with mocked ENS TXT response", async () => {
assert.equal(verifyResp.status, 200);
assert.equal(verifyJson.ok, true);
assert.equal(verifyJson.verified_with, "ens");
assert.equal(verifyJson.checks?.ens_match, true);
assert.equal(verifyJson.values?.ens?.kid, keys.kid);
} finally {
await stop(srv.proc);
}
});

test("/verify?ens=1&strict_kid=1 rejects a receipt when ENS publishes a different kid", async () => {
const keys = makeKeys();
const ensMock = JSON.stringify({
"cl.sig.pub": `ed25519:${keys.publicRaw32B64}`,
"cl.sig.canonical": "json.sorted_keys.v1",
"cl.sig.kid": "rotated-kid",
});
const srv = await startServer({
RECEIPT_SIGNING_PRIVATE_KEY_PEM_B64: keys.privatePemB64,
RECEIPT_SIGNING_PUBLIC_KEY_B64: keys.publicRaw32B64,
RECEIPT_SIGNER_ID: "runtime.commandlayer.eth",
ENS_MOCK_TXT_JSON: ensMock,
});

try {
const response = await createDescribeReceipt(srv.base);

const verifyResp = await fetch(`${srv.base}/verify?ens=1&strict_kid=1`, {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify(response),
});
const verifyJson = await verifyResp.json();

assert.equal(verifyResp.status, 400);
assert.equal(verifyJson.ok, false);
assert.equal(verifyJson.checks?.ens_match, false);
assert.match(String(verifyJson.error), /does not match ENS signer records/);
assert.equal(verifyJson.values?.proof?.kid, keys.kid);
assert.equal(verifyJson.values?.ens?.kid, "rotated-kid");
} finally {
await stop(srv.proc);
}
});

test("fabricated x402 defaults use v1.1.0", async () => {
const keys = makeKeys();
Expand Down
11 changes: 0 additions & 11 deletions sdk/typescript-sdk/tests/canonicalization.test.mjs

This file was deleted.

11 changes: 0 additions & 11 deletions sdk/typescript-sdk/tests/ens-delegation.test.mjs

This file was deleted.

17 changes: 0 additions & 17 deletions sdk/typescript-sdk/tests/security-cases.test.mjs

This file was deleted.

Loading