diff --git a/README.md b/README.md index d34c0b9..9469613 100644 --- a/README.md +++ b/README.md @@ -1,209 +1,72 @@ # @commandlayer/runtime-core -Core signing, verification, and canonicalization primitives for the CommandLayer v1.1.0 protocol. +Canonical crypto and receipt verification primitives for CommandLayer CLAS. -This package is the single canonical implementation of: +## Canonical proof envelope (CLAS) -- **Canonicalization** — `json.sorted_keys.v1` deterministic JSON (keys sorted at every level) -- **Ed25519 signing and verification** — real `node:crypto` Ed25519, no mocks -- **Signed layered receipts** — v1.1.0 `SignedLayeredReceipt` with structured proof envelope -- **ENS signer resolution** — live TXT record lookup for `cl.sig.pub` -- **Legacy compat shims** — `metadata.proof` envelope bridge for runtime/server.mjs +`signCommandLayerReceipt()` writes the canonical proof envelope: -All other CommandLayer repos import from here. Nothing is reimplemented downstream. - -## Install - -```bash -npm install @commandlayer/runtime-core -``` - -Requires Node.js >= 20. - -## Usage - -### Canonicalization - -```ts -import { canonicalize } from '@commandlayer/runtime-core'; - -const canonical = canonicalize({ - verb: 'chat.completions', - version: '1.1.0', - agent: 'runtime.commandlayer.eth', - timestamp: '2026-05-12T00:00:00.000Z', -}); -// Keys are sorted at every level, no trailing whitespace, no undefined values -``` - -### Sign and verify a receipt - -```ts -import { - generateEd25519KeyPair, - signReceipt, - verifyReceipt, -} from '@commandlayer/runtime-core'; - -const { privateKeyPem, rawPublicKey } = generateEd25519KeyPair(); - -const signed = signReceipt( - { - verb: 'chat.completions', - version: '1.1.0', - agent: 'runtime.commandlayer.eth', - timestamp: new Date().toISOString(), - payload: { prompt: 'hello' }, - result: { output: 'world' }, - }, - { - privateKeyPem, - kid: process.env.KEY_ID!, - signerEns: 'runtime.commandlayer.eth', - } -); - -const result = verifyReceipt(signed, { - rawPublicKey, - expectedSigner: 'runtime.commandlayer.eth', -}); - -console.assert(result.valid === true); -``` - -### Resolve signer from ENS - -```ts -import { JsonRpcProvider } from 'ethers'; -import { resolveSignerFromENS } from '@commandlayer/runtime-core'; - -// Positional form -const provider = new JsonRpcProvider(process.env.RPC_URL); -const signer = await resolveSignerFromENS('signer.commandlayer.eth', provider); - -// Options-object form (equivalent) -const signer2 = await resolveSignerFromENS({ - ensName: 'signer.commandlayer.eth', - provider, -}); -``` - -Supported TXT records: - -| Key | Required | Description | -|-----|----------|-------------| -| `cl.sig.pub` | Yes | `ed25519:` | -| `cl.sig.kid` | No | Short key identifier | -| `cl.sig.canonical` | No | Defaults to `json.sorted_keys.v1` | - -### Key encoding +- `metadata.proof.canonicalization = "json.sorted_keys.v1"` +- `metadata.proof.hash.alg = "SHA-256"` +- `metadata.proof.hash.value = ` +- `metadata.proof.signature.alg = "ed25519"` +- `metadata.proof.signature.value = ` +- `metadata.proof.signature.kid = ` ```ts -import { encodePublicKey, parsePublicKey } from '@commandlayer/runtime-core'; - -// Encode raw 32-byte key for ENS TXT record -const ensValue = encodePublicKey(rawPublicKey); -// => "ed25519:hhyCuPNoMk4JtEvGEV8F6nMZ4uDO1EcyizPufmnJTOY=" - -// Parse ENS TXT record back to raw bytes -const raw = parsePublicKey(ensValue); -// => Uint8Array(32) +import { signCommandLayerReceipt, verifyCommandLayerReceipt } from "@commandlayer/runtime-core"; + +const signed = signCommandLayerReceipt(receipt, { privateKeyPem, kid: "vC4WbcNoq2znSCiQ" }); +const result = verifyCommandLayerReceipt(signed, { publicKeyPemOrDer: publicKeyPem }); + +// result shape +// { +// ok: boolean, +// status: "VERIFIED" | "INVALID", +// checks: { schema, canonical_hash, signature, signer }, +// errors: string[] +// } ``` -### Low-level crypto +## ENS signer records -```ts -import { - canonicalize, - signCanonical, - verifyCanonical, - verifyCanonicalWithRawKey, -} from '@commandlayer/runtime-core'; - -const canonical = canonicalize(payload); -const signature = signCanonical(canonical, privateKeyPem); -const valid = verifyCanonical(canonical, signature, publicKeyPem); -const validFromRaw = verifyCanonicalWithRawKey(canonical, signature, rawPublicKey); -``` +Supported signer TXT records: -### Canonical CLAS proof envelope +- `cl.sig.pub = ed25519:` +- `cl.sig.kid = ` +- `cl.sig.canonical = json.sorted_keys.v1` +- `cl.receipt.signer = ` -Use the canonical metadata proof API: +Example fixture: -```ts -import { - signCommandLayerReceipt, - verifyCommandLayerReceipt, - buildCanonicalProof, - isSignedCommandLayerReceipt, -} from '@commandlayer/runtime-core'; -``` +- `cl.sig.kid = vC4WbcNoq2znSCiQ` +- `cl.sig.pub = ed25519:hhyCuPNoMk4JtEvGEV8F6nMZ4uDO1EcyizPufmnJTOY=` +- `cl.sig.canonical = json.sorted_keys.v1` +- `cl.receipt.signer = runtime.commandlayer.eth` -Canonical envelope fields: -- `metadata.proof.canonicalization` -- `metadata.proof.hash.alg` -- `metadata.proof.hash.value` -- `metadata.proof.signature.alg` -- `metadata.proof.signature.value` -- `metadata.proof.signature.kid` +When `ensRecord` is provided to `verifyCommandLayerReceipt`, verifier compares: -### Cross-repo canonicalization alignment +- `signature.kid` ↔ `cl.sig.kid` +- `metadata.proof.canonicalization` ↔ `cl.sig.canonical` +- `receipt.agent` ↔ `cl.receipt.signer` -Every repo that imports `@commandlayer/runtime-core` should run the shared test vectors: +## Endpoint discovery metadata (optional) -```ts -import { canonicalize, CANONICAL_TEST_VECTORS } from '@commandlayer/runtime-core'; - -for (const { description, input, expected } of CANONICAL_TEST_VECTORS) { - const actual = canonicalize(input); - if (actual !== expected) throw new Error(`Vector failed: ${description}`); -} -``` +ENS resolver also parses optional discovery TXT records: -## Protocol constants +- `cl.endpoint.runtime` +- `cl.endpoint.verify` +- `cl.endpoint.mcp` +- `cl.endpoint.docs` +- `cl.endpoint.registry` -```ts -import { - PROTOCOL_VERSION, // "1.1.0" - CANONICAL_METHOD, // "json.sorted_keys.v1" - SIGNATURE_ALG, // "ed25519" - ENS_KEY_PUB, // "cl.sig.pub" - ENS_KEY_KID, // "cl.sig.kid" - ENS_KEY_CANONICAL, // "cl.sig.canonical" - ENS_KEY_SIGNER, // "cl.receipt.signer" -} from '@commandlayer/runtime-core'; -``` - -## Environment variables - -See `.env.example` for the full list. Key variables: - -| Variable | Used by | Description | -|----------|---------|-------------| -| `RPC_URL` | `resolveSignerFromENS` | Ethereum JSON-RPC endpoint | -| `SIGNING_PRIVATE_KEY_PEM` | `signReceipt`, `signCanonical` | Ed25519 private key (PEM) | -| `SIGNING_PUBLIC_KEY_PEM` | `verifyReceipt`, `verifyCanonical` | Ed25519 public key (PEM) | -| `SIGNER_ENS_NAME` | ENS | ENS name with `cl.sig.pub` set | +These endpoint records are **optional discovery metadata only** and are **not verification-critical proof**. ## Development ```bash -npm run build # compile TypeScript -npm test # run all tests -npm run typecheck # type-check without emitting -``` - -## Signing protocol - -The signing message is **raw UTF-8 bytes** of `canonicalize(receipt)`. This is NOT `sha256(canonical)` — signatures are over the data directly. Any change to this contract requires a protocol version bump. - -``` -signature = Ed25519.sign( - privateKey, - UTF8(canonicalize(receiptPayload)) -) +npm install +npm run build +npm test +npm run typecheck ``` - -## License - -Apache-2.0 diff --git a/src/canonicalize.ts b/src/canonicalize.ts index a7d7fc3..9a488dd 100644 --- a/src/canonicalize.ts +++ b/src/canonicalize.ts @@ -180,7 +180,7 @@ export const CANONICAL_TEST_VECTORS = [ description: "audit protocol vector: verb/family/version", input: { verb: "verify", family: "trust", version: "1.0.0" }, expected: '{"family":"trust","verb":"verify","version":"1.0.0"}', - // SHA-256 of the canonical string (UTF-8 encoded): + // SHA-256 of the canonical string (UTF-8 encoded), independently verified: sha256: "7f84cc113290c283fe97e3beb9bd3f65e5de0022e278cad25ef7619c398b1bab", }, ] as const; diff --git a/src/compat.ts b/src/compat.ts index 60f1d97..b3ceae3 100644 --- a/src/compat.ts +++ b/src/compat.ts @@ -8,6 +8,18 @@ export interface EnsVerificationRecord { canonical: string; } +export interface VerifyCommandLayerReceiptResult { + ok: boolean; + status: "VERIFIED" | "INVALID"; + checks: { + schema: boolean; + canonical_hash: boolean; + signature: boolean; + signer: boolean; + }; + errors: string[]; +} + export interface CommandLayerReceipt { verb: string; version?: string; @@ -22,7 +34,7 @@ export interface CommandLayerReceipt { export interface CommandLayerProof { canonicalization: string; - hash: { alg: "sha256"; value: string }; + hash: { alg: "SHA-256"; value: string }; signature: { alg: typeof SIGNATURE_ALG; value: string; kid: string }; } @@ -39,7 +51,7 @@ export function signCommandLayerReceipt( opts: { privateKeyPem: string; kid: string } ): CommandLayerReceipt { if (!opts.privateKeyPem) throw new Error("privateKeyPem is required"); - if (!opts.kid) throw new Error("kid is required"); + if (!opts.kid || typeof opts.kid !== "string") throw new Error("kid is required"); const canonical = buildCanonicalProof(receipt); const hash = createHash("sha256").update(canonical, "utf8").digest("hex"); @@ -54,7 +66,7 @@ export function signCommandLayerReceipt( ...metaWithoutProof, proof: { canonicalization: CANONICAL_METHOD, - hash: { alg: "sha256", value: hash }, + hash: { alg: "SHA-256", value: hash }, signature: { alg: SIGNATURE_ALG, value: sig, kid: opts.kid }, }, }, @@ -68,38 +80,94 @@ export function verifyCommandLayerReceipt( allowedCanonicals?: string[]; ensRecord?: EnsVerificationRecord; } -): { ok: boolean; reason?: string } { +): VerifyCommandLayerReceiptResult { + const checks: VerifyCommandLayerReceiptResult["checks"] = { + schema: false, + canonical_hash: false, + signature: false, + signer: opts.ensRecord ? false : true, + }; + const errors: string[] = []; + const proof = receipt?.metadata?.proof; - if (!proof) return { ok: false, reason: "Missing metadata.proof" }; - if (!proof.hash?.alg) return { ok: false, reason: "Missing metadata.proof.hash.alg" }; - if (!proof.hash?.value) return { ok: false, reason: "Missing metadata.proof.hash.value" }; - if (!proof.signature?.alg) return { ok: false, reason: "Missing metadata.proof.signature.alg" }; - if (!proof.signature?.value) return { ok: false, reason: "Missing metadata.proof.signature.value" }; - if (!proof.signature?.kid) return { ok: false, reason: "Missing metadata.proof.signature.kid" }; + if (!proof || typeof proof !== "object") { + errors.push("ERR_MISSING_PROOF"); + return { ok: false, status: "INVALID", checks, errors }; + } + + if (typeof proof.canonicalization !== "string" || proof.canonicalization.length === 0) { + errors.push("ERR_MISSING_CANONICALIZATION"); + } + if (typeof proof.hash?.alg !== "string" || proof.hash.alg.length === 0) { + errors.push("ERR_MISSING_HASH_ALG"); + } + if (typeof proof.hash?.value !== "string" || proof.hash.value.length === 0) { + errors.push("ERR_MISSING_HASH_VALUE"); + } + if (typeof proof.signature?.alg !== "string" || proof.signature.alg.length === 0) { + errors.push("ERR_MISSING_SIGNATURE_ALG"); + } + if (typeof proof.signature?.value !== "string" || proof.signature.value.length === 0) { + errors.push("ERR_MISSING_SIGNATURE_VALUE"); + } + if (typeof proof.signature?.kid !== "string" || proof.signature.kid.trim().length === 0) { + errors.push("ERR_MISSING_SIGNATURE_KID"); + } + + const allowed = opts.allowedCanonicals ?? [CANONICAL_METHOD]; + if (typeof proof.canonicalization === "string" && !allowed.includes(proof.canonicalization)) { + errors.push("ERR_UNSUPPORTED_CANONICALIZATION"); + } + if (proof.hash?.alg && proof.hash.alg !== "SHA-256") { + errors.push("ERR_UNSUPPORTED_HASH_ALG"); + } + if (proof.signature?.alg && proof.signature.alg !== SIGNATURE_ALG) { + errors.push("ERR_UNSUPPORTED_SIGNATURE_ALG"); + } if (opts.ensRecord) { - if (proof.signature.kid !== opts.ensRecord.kid) { - return { ok: false, reason: "metadata.proof.signature.kid does not match ENS cl.sig.kid" }; + if (proof.signature?.kid !== opts.ensRecord.kid) { + errors.push("ERR_ENS_KID_MISMATCH"); } if (proof.canonicalization !== opts.ensRecord.canonical) { - return { ok: false, reason: "metadata.proof.canonicalization does not match ENS cl.sig.canonical" }; + errors.push("ERR_ENS_CANONICAL_MISMATCH"); } if (receipt.agent !== opts.ensRecord.signer) { - return { ok: false, reason: "receipt signer identity does not match ENS cl.receipt.signer" }; + errors.push("ERR_ENS_SIGNER_MISMATCH"); } } - const allowed = opts.allowedCanonicals ?? [CANONICAL_METHOD]; - if (!allowed.includes(proof.canonicalization)) return { ok: false, reason: "Unsupported canonicalization" }; - if (proof.hash.alg !== "sha256") return { ok: false, reason: "Unsupported hash algorithm" }; - if (proof.signature.alg !== SIGNATURE_ALG) return { ok: false, reason: "Unsupported signature algorithm" }; + checks.schema = errors.length === 0; - const canonical = buildCanonicalProof(receipt); - const recomputed = createHash("sha256").update(canonical, "utf8").digest("hex"); - if (recomputed !== proof.hash.value) return { ok: false, reason: "Hash mismatch" }; + let canonical = ""; + if (checks.schema) { + canonical = buildCanonicalProof(receipt); + const recomputed = createHash("sha256").update(canonical, "utf8").digest("hex"); + if (recomputed === proof.hash.value) { + checks.canonical_hash = true; + } else { + errors.push("ERR_HASH_MISMATCH"); + } + + const sigOk = verifyCanonical(canonical, proof.signature.value, opts.publicKeyPemOrDer); + if (sigOk) { + checks.signature = true; + } else { + errors.push("ERR_SIGNATURE_INVALID"); + } + } - const ok = verifyCanonical(canonical, proof.signature.value, opts.publicKeyPemOrDer); - return ok ? { ok: true } : { ok: false, reason: "signature invalid" }; + if (opts.ensRecord) { + checks.signer = !errors.some((e) => e.startsWith("ERR_ENS_")); + } + + const ok = checks.schema && checks.canonical_hash && checks.signature && checks.signer; + return { + ok, + status: ok ? "VERIFIED" : "INVALID", + checks, + errors, + }; } export function isSignedCommandLayerReceipt(value: unknown): value is CommandLayerReceipt { @@ -107,5 +175,6 @@ export function isSignedCommandLayerReceipt(value: unknown): value is CommandLay return !!v?.metadata?.proof?.hash?.alg && !!v?.metadata?.proof?.hash?.value && !!v?.metadata?.proof?.signature?.alg - && !!v?.metadata?.proof?.signature?.value; + && !!v?.metadata?.proof?.signature?.value + && !!v?.metadata?.proof?.signature?.kid; } diff --git a/src/ens.ts b/src/ens.ts index 0f3e4e0..1e94b4a 100644 --- a/src/ens.ts +++ b/src/ens.ts @@ -26,6 +26,14 @@ export { ENS_KEY_SIGNER } from "./crypto.js"; // ── Types ───────────────────────────────────────────────────────────────────── +export interface EnsEndpointMetadata { + runtime?: string; + verify?: string; + mcp?: string; + docs?: string; + registry?: string; +} + export interface EnsSignerRecord { /** ENS name, e.g. runtime.commandlayer.eth */ name: string; @@ -35,6 +43,10 @@ export interface EnsSignerRecord { kid: string; /** Canonicalization method from cl.sig.canonical */ canonical: string; + /** Receipt signer identity from cl.receipt.signer */ + signer: string; + /** Optional endpoint discovery metadata */ + endpoints: EnsEndpointMetadata; } /** @@ -127,12 +139,24 @@ export async function resolveSignerFromENS( let pubValue: string | null; let kidValue: string | null; let canonicalValue: string | null; + let signerValue: string | null; + let endpointRuntime: string | null; + let endpointVerify: string | null; + let endpointMcp: string | null; + let endpointDocs: string | null; + let endpointRegistry: string | null; try { - [pubValue, kidValue, canonicalValue] = await Promise.all([ + [pubValue, kidValue, canonicalValue, signerValue, endpointRuntime, endpointVerify, endpointMcp, endpointDocs, endpointRegistry] = await Promise.all([ resolver.getText(ENS_KEY_PUB), resolver.getText(ENS_KEY_KID), resolver.getText(ENS_KEY_CANONICAL), + resolver.getText("cl.receipt.signer"), + resolver.getText("cl.endpoint.runtime"), + resolver.getText("cl.endpoint.verify"), + resolver.getText("cl.endpoint.mcp"), + resolver.getText("cl.endpoint.docs"), + resolver.getText("cl.endpoint.registry"), ]); } catch (err) { throw new Error( @@ -165,6 +189,14 @@ export async function resolveSignerFromENS( rawPublicKey, kid: kidValue ?? "", canonical: canonicalValue ?? CANONICAL_METHOD, + signer: signerValue ?? ensName, + endpoints: { + ...(endpointRuntime ? { runtime: endpointRuntime } : {}), + ...(endpointVerify ? { verify: endpointVerify } : {}), + ...(endpointMcp ? { mcp: endpointMcp } : {}), + ...(endpointDocs ? { docs: endpointDocs } : {}), + ...(endpointRegistry ? { registry: endpointRegistry } : {}), + }, }; } diff --git a/src/index.ts b/src/index.ts index 22631c5..75a3227 100644 --- a/src/index.ts +++ b/src/index.ts @@ -49,19 +49,6 @@ export { type ResolveSignerFromENSOptions, } from "./ens.js"; -// Receipt v1.1.0 -export { - signReceipt, - verifyReceipt, - isSignedLayeredReceipt, - type ReceiptPayload, - type ReceiptProof, - type SignedLayeredReceipt, - type SignReceiptOptions, - type VerifyReceiptResult, - type VerifyReceiptOptions, -} from "./receipt.js"; - // CommandLayer canonical proof envelope APIs export { buildCanonicalProof, diff --git a/test/compat.test.ts b/test/compat.test.ts index 25648e0..6b7e291 100644 --- a/test/compat.test.ts +++ b/test/compat.test.ts @@ -21,25 +21,51 @@ describe("canonical CLAS proof envelope", () => { const signed = signCommandLayerReceipt(baseReceipt, { privateKeyPem: kp.privateKeyPem, kid: "testKid" }); const proof = signed.metadata!.proof!; assert.equal(proof.canonicalization, "json.sorted_keys.v1"); - assert.equal(proof.hash.alg, "sha256"); + assert.equal(proof.hash.alg, "SHA-256"); assert.ok(proof.hash.value); assert.equal(proof.signature.alg, "ed25519"); assert.ok(proof.signature.value); assert.equal(proof.signature.kid, "testKid"); const result = verifyCommandLayerReceipt(signed, { publicKeyPemOrDer: kp.publicKeyPem }); - assert.ok(result.ok, result.reason); + assert.deepEqual(result.status, "VERIFIED"); + assert.deepEqual(result.checks, { schema: true, canonical_hash: true, signature: true, signer: true }); + assert.deepEqual(result.errors, []); assert.equal(isSignedCommandLayerReceipt(signed), true); }); - test("rejects required canonical fields when missing", () => { + test("requires signature.kid to be a non-empty string", () => { const signed = signCommandLayerReceipt(baseReceipt, { privateKeyPem: kp.privateKeyPem, kid: "testKid" }); const p = signed.metadata!.proof!; - assert.equal(verifyCommandLayerReceipt({ ...signed, metadata: { ...signed.metadata!, proof: { ...p, hash: { ...p.hash, alg: "" as never } } } }, { publicKeyPemOrDer: kp.publicKeyPem }).ok, false); - assert.equal(verifyCommandLayerReceipt({ ...signed, metadata: { ...signed.metadata!, proof: { ...p, hash: { ...p.hash, value: "" } } } }, { publicKeyPemOrDer: kp.publicKeyPem }).ok, false); - assert.equal(verifyCommandLayerReceipt({ ...signed, metadata: { ...signed.metadata!, proof: { ...p, signature: { ...p.signature, alg: "" as never } } } }, { publicKeyPemOrDer: kp.publicKeyPem }).ok, false); - assert.equal(verifyCommandLayerReceipt({ ...signed, metadata: { ...signed.metadata!, proof: { ...p, signature: { ...p.signature, value: "" } } } }, { publicKeyPemOrDer: kp.publicKeyPem }).ok, false); - assert.equal(verifyCommandLayerReceipt({ ...signed, metadata: { ...signed.metadata!, proof: { ...p, signature: { ...p.signature, kid: "" } } } }, { publicKeyPemOrDer: kp.publicKeyPem }).ok, false); + const missingKid = verifyCommandLayerReceipt( + { ...signed, metadata: { ...signed.metadata!, proof: { ...p, signature: { ...p.signature, kid: "" } } } }, + { publicKeyPemOrDer: kp.publicKeyPem } + ); + assert.equal(missingKid.status, "INVALID"); + assert.equal(missingKid.checks.schema, false); + assert.ok(missingKid.errors.includes("ERR_MISSING_SIGNATURE_KID")); + }); + + test("returns INVALID and canonical_hash=false on invalid hash", () => { + const signed = signCommandLayerReceipt(baseReceipt, { privateKeyPem: kp.privateKeyPem, kid: "testKid" }); + const bad = verifyCommandLayerReceipt( + { ...signed, metadata: { ...signed.metadata!, proof: { ...signed.metadata!.proof!, hash: { ...signed.metadata!.proof!.hash, value: "00" } } } }, + { publicKeyPemOrDer: kp.publicKeyPem } + ); + assert.equal(bad.status, "INVALID"); + assert.equal(bad.checks.canonical_hash, false); + assert.ok(bad.errors.includes("ERR_HASH_MISMATCH")); + }); + + test("returns INVALID and signature=false on invalid signature", () => { + const signed = signCommandLayerReceipt(baseReceipt, { privateKeyPem: kp.privateKeyPem, kid: "testKid" }); + const bad = verifyCommandLayerReceipt( + { ...signed, metadata: { ...signed.metadata!, proof: { ...signed.metadata!.proof!, signature: { ...signed.metadata!.proof!.signature, value: Buffer.alloc(64).toString("base64") } } } }, + { publicKeyPemOrDer: kp.publicKeyPem } + ); + assert.equal(bad.status, "INVALID"); + assert.equal(bad.checks.signature, false); + assert.ok(bad.errors.includes("ERR_SIGNATURE_INVALID")); }); test("validates ENS-compatible signer constraints when ensRecord is supplied", () => { @@ -48,43 +74,9 @@ describe("canonical CLAS proof envelope", () => { kid: "vC4WbcNoq2znSCiQ", canonical: "json.sorted_keys.v1", }; - - const signed = signCommandLayerReceipt( - { ...baseReceipt, agent: "runtime.commandlayer.eth" }, - { privateKeyPem: kp.privateKeyPem, kid: "vC4WbcNoq2znSCiQ" } - ); - - const ok = verifyCommandLayerReceipt(signed, { - publicKeyPemOrDer: kp.publicKeyPem, - ensRecord: runtimeEnsFixture, - }); + const signed = signCommandLayerReceipt({ ...baseReceipt, agent: "runtime.commandlayer.eth" }, { privateKeyPem: kp.privateKeyPem, kid: "vC4WbcNoq2znSCiQ" }); + const ok = verifyCommandLayerReceipt(signed, { publicKeyPemOrDer: kp.publicKeyPem, ensRecord: runtimeEnsFixture }); assert.equal(ok.ok, true); - - const badKid = verifyCommandLayerReceipt( - { ...signed, metadata: { ...signed.metadata!, proof: { ...signed.metadata!.proof!, signature: { ...signed.metadata!.proof!.signature, kid: "wrong" } } } }, - { publicKeyPemOrDer: kp.publicKeyPem, ensRecord: runtimeEnsFixture } - ); - assert.equal(badKid.ok, false); - - const badCanonical = verifyCommandLayerReceipt( - { ...signed, metadata: { ...signed.metadata!, proof: { ...signed.metadata!.proof!, canonicalization: "json.unsorted.v1" } } }, - { publicKeyPemOrDer: kp.publicKeyPem, ensRecord: runtimeEnsFixture } - ); - assert.equal(badCanonical.ok, false); - - const badSigner = verifyCommandLayerReceipt( - { ...signed, agent: "other.commandlayer.eth" }, - { publicKeyPemOrDer: kp.publicKeyPem, ensRecord: runtimeEnsFixture } - ); - assert.equal(badSigner.ok, false); - }); - - test("rejects legacy fields as canonical", () => { - const legacyLike = { - ...baseReceipt, - metadata: { proof: { signature_b64: "abc", hash_sha256: "def", canonicalization: "json.sorted_keys.v1" } }, - }; - const result = verifyCommandLayerReceipt(legacyLike as never, { publicKeyPemOrDer: kp.publicKeyPem }); - assert.equal(result.ok, false); + assert.equal(ok.checks.signer, true); }); }); diff --git a/test/ens.test.ts b/test/ens.test.ts index 7c9edc0..38d3d1d 100644 --- a/test/ens.test.ts +++ b/test/ens.test.ts @@ -63,6 +63,12 @@ const FULL_RECORDS: Record = { "cl.sig.pub": PUB_VALUE, "cl.sig.kid": "testKid001", "cl.sig.canonical": "json.sorted_keys.v1", + "cl.receipt.signer": "test.commandlayer.eth", + "cl.endpoint.runtime": "https://runtime.commandlayer.io", + "cl.endpoint.verify": "https://verify.commandlayer.io", + "cl.endpoint.mcp": "https://mcp.commandlayer.io", + "cl.endpoint.docs": "https://docs.commandlayer.io", + "cl.endpoint.registry": "https://registry.commandlayer.io", }; // ── Tests ───────────────────────────────────────────────────────────────────── @@ -75,6 +81,7 @@ describe("resolveSignerFromENS — positional args", () => { assert.strictEqual(record.kid, "vC4WbcNoq2znSCiQ"); assert.strictEqual(record.canonical, "json.sorted_keys.v1"); assert.strictEqual(record.rawPublicKey.length, 32); + assert.strictEqual(record.signer, "runtime.commandlayer.eth"); }); it("resolves a full signer record", async () => { @@ -85,18 +92,27 @@ describe("resolveSignerFromENS — positional args", () => { assert.deepStrictEqual(record.rawPublicKey, RAW_KEY); assert.strictEqual(record.kid, "testKid001"); assert.strictEqual(record.canonical, "json.sorted_keys.v1"); + assert.strictEqual(record.signer, "test.commandlayer.eth"); + assert.strictEqual(record.endpoints.runtime, "https://runtime.commandlayer.io"); + assert.strictEqual(record.endpoints.verify, "https://verify.commandlayer.io"); + assert.strictEqual(record.endpoints.mcp, "https://mcp.commandlayer.io"); + assert.strictEqual(record.endpoints.docs, "https://docs.commandlayer.io"); + assert.strictEqual(record.endpoints.registry, "https://registry.commandlayer.io"); }); it("defaults kid to empty string when cl.sig.kid is absent", async () => { const provider = makeMockProvider({ "cl.sig.pub": PUB_VALUE }); const record = await resolveSignerFromENS("test.commandlayer.eth", provider); assert.strictEqual(record.kid, ""); + assert.strictEqual(record.signer, "test.commandlayer.eth"); + assert.deepStrictEqual(record.endpoints, {}); }); it("defaults canonical to json.sorted_keys.v1 when absent", async () => { const provider = makeMockProvider({ "cl.sig.pub": PUB_VALUE }); const record = await resolveSignerFromENS("test.commandlayer.eth", provider); assert.strictEqual(record.canonical, "json.sorted_keys.v1"); + assert.strictEqual(record.signer, "test.commandlayer.eth"); }); it("throws when no resolver found", async () => {