diff --git a/.env.example b/.env.example index 3750b86..4841faf 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,4 @@ CL_AGENT=runtime.commandlayer.eth CL_KEY_ID=vC4WbcNoq2znSCiQ CL_PRIVATE_KEY_PEM="-----BEGIN PRIVATE KEY-----..." +CL_VERIFIER_URL=https://www.commandlayer.org/api/verify diff --git a/README.md b/README.md index ebbe5cd..e9b0c45 100644 --- a/README.md +++ b/README.md @@ -10,14 +10,6 @@ Wrap your agent. Emit a signed receipt. Verify through CommandLayer. npm install @commandlayer/agent-sdk ``` -> Temporary caveat (as of May 9, 2026): package availability on npm may vary by registry policy/account permissions. -> If `npm install @commandlayer/agent-sdk` fails in your environment, use local development install: - -```bash -npm install -npm run build -``` - ## Quickstart ```ts @@ -38,7 +30,7 @@ const local = validateTrustReceipt(receipt); // schema only if (!local.ok) throw new Error(local.errors.join("; ")); const remote = await cl.verify(receipt); // cryptographic verification -console.log({ output, receipt, remote }); +process.stdout.write(JSON.stringify({ output, receipt, remote }) + "\n"); ``` ## Wrap your agent @@ -73,10 +65,10 @@ import { } from "@commandlayer/agent-sdk"; const requestResult = validateTrustRequest(requestPayload); -if (!requestResult.ok) console.error(requestResult.errors); +if (!requestResult.ok) process.stderr.write(requestResult.errors.join("\n") + "\n"); const receiptResult = validateTrustReceipt(receiptPayload); -if (!receiptResult.ok) console.error(receiptResult.errors); +if (!receiptResult.ok) process.stderr.write(receiptResult.errors.join("\n") + "\n"); assertValidTrustRequest(requestPayload); assertValidTrustReceipt(receiptPayload); diff --git a/docs/examples.md b/docs/examples.md index 5dea970..3f684f1 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -13,6 +13,7 @@ All examples are dependency-free and intentionally use mocked execution (no real - `CL_PRIVATE_KEY_PEM` - `CL_KEY_ID` - `CL_AGENT` (defaults to `runtime.commandlayer.eth`) +- `CL_VERIFIER_URL` (optional, defaults to `https://www.commandlayer.org/api/verify`) The demo signer/key id match the public VerifyAgent demo. For your own agent, replace these with your ENS signer and key id. @@ -78,12 +79,11 @@ After building: "completed_at": "2026-04-29T14:22:00.012Z" }, "proof": { - "canonicalization": "json.sorted_keys.v1", - "hash": "...", - "signature_alg": "ed25519", - "signature": "...", - "key_id": "vC4WbcNoq2znSCiQ", - "signer": "runtime.commandlayer.eth" + "canonical": "json.sorted_keys.v1", + "alg": "ed25519", + "signature": "", + "kid": "vC4WbcNoq2znSCiQ", + "signer_id": "runtime.commandlayer.eth" } } ``` diff --git a/examples/agent-to-agent-verify.ts b/examples/agent-to-agent-verify.ts index 58e1bdb..189b586 100644 --- a/examples/agent-to-agent-verify.ts +++ b/examples/agent-to-agent-verify.ts @@ -1,3 +1,4 @@ +import "dotenv/config"; import { CommandLayer } from "../src/index.js"; const privateKeyPem = process.env.CL_PRIVATE_KEY_PEM; @@ -15,8 +16,8 @@ const result = await cl.wrap("agent.execute", async () => ({ executed_by: agent, })); -console.log("output", result.output); -console.log("receipt", JSON.stringify(result.receipt, null, 2)); +process.stdout.write(`output: ${JSON.stringify(result.output)}\n`); +process.stdout.write(`receipt: ${JSON.stringify(result.receipt, null, 2)}\n`); const verification = await cl.verify(result.receipt); -console.log("verification_status", verification); +process.stdout.write(`verification_status: ${JSON.stringify(verification)}\n`); diff --git a/examples/basic-agent.ts b/examples/basic-agent.ts index 9c75a4a..2fed354 100644 --- a/examples/basic-agent.ts +++ b/examples/basic-agent.ts @@ -14,13 +14,13 @@ const cl = new CommandLayer({ agent: process.env.CL_AGENT ?? "runtime.commandlayer.eth", keyId: process.env.CL_KEY_ID ?? "vC4WbcNoq2znSCiQ", privateKeyPem: process.env.CL_PRIVATE_KEY_PEM, - verifierUrl: "https://www.commandlayer.org/api/verify", + verifierUrl: process.env.CL_VERIFIER_URL ?? "https://www.commandlayer.org/api/verify", }); const result = await cl.wrap("summarize", async () => fakeSummarizeAgent("hello world")); -console.log("output", result.output); -console.log("receipt", JSON.stringify(result.receipt, null, 2)); +process.stdout.write(`output: ${JSON.stringify(result.output)}\n`); +process.stdout.write(`receipt: ${JSON.stringify(result.receipt, null, 2)}\n`); const verified = await cl.verify(result.receipt); -console.log("verified", verified); +process.stdout.write(`verified: ${JSON.stringify(verified)}\n`); diff --git a/examples/existing-agent-integration.ts b/examples/existing-agent-integration.ts index b6eee22..eef48a3 100644 --- a/examples/existing-agent-integration.ts +++ b/examples/existing-agent-integration.ts @@ -1,3 +1,4 @@ +import "dotenv/config"; import { CommandLayer } from "../src/index.js"; const privateKeyPem = process.env.CL_PRIVATE_KEY_PEM; @@ -24,12 +25,12 @@ const input = { context: "release build green", }; -console.log("Already have an agent? Wrap the action, don't rewrite the agent."); +process.stdout.write("Already have an agent? Wrap the action, don't rewrite the agent.\n"); const result = await cl.wrap("agent.execute", async () => existingAgent.run(input)); const verification = await cl.verify(result.receipt); -console.log("output", result.output); -console.log("receipt", JSON.stringify(result.receipt, null, 2)); -console.log("verification_status", verification); +process.stdout.write(`output: ${JSON.stringify(result.output)}\n`); +process.stdout.write(`receipt: ${JSON.stringify(result.receipt, null, 2)}\n`); +process.stdout.write(`verification_status: ${JSON.stringify(verification)}\n`); diff --git a/examples/full-demo.ts b/examples/full-demo.ts index b2f655b..faf57a2 100644 --- a/examples/full-demo.ts +++ b/examples/full-demo.ts @@ -1,3 +1,4 @@ +import "dotenv/config"; import { CommandLayer } from "../src/index.js"; const privateKeyPem = process.env.CL_PRIVATE_KEY_PEM; @@ -15,20 +16,15 @@ const result = await cl.wrap("summarize", async () => { return { summary: "hello world" }; }); -console.log("Agent output"); -console.log(JSON.stringify(result.output, null, 2)); -console.log(""); - -console.log("Signed receipt"); -console.log(JSON.stringify(result.receipt, null, 2)); -console.log("receipt.signer:", result.receipt.signer); -console.log("receipt.verb:", result.receipt.verb); -console.log( - "receipt.proof.hash:", - result.receipt.proof.hash, -); -console.log("receipt.proof.key_id:", result.receipt.proof.key_id); -console.log(""); +process.stdout.write("Agent output\n"); +process.stdout.write(`${JSON.stringify(result.output, null, 2)}\n\n`); + +process.stdout.write("Signed receipt\n"); +process.stdout.write(`${JSON.stringify(result.receipt, null, 2)}\n`); +process.stdout.write(`receipt.signer: ${result.receipt.signer}\n`); +process.stdout.write(`receipt.verb: ${result.receipt.verb}\n`); +process.stdout.write(`receipt.proof.kid: ${result.receipt.proof.kid}\n`); +process.stdout.write(`receipt.proof.signer_id: ${result.receipt.proof.signer_id}\n\n`); const statusOf = (value: unknown): string => { if (!value || typeof value !== "object") { @@ -41,7 +37,7 @@ const statusOf = (value: unknown): string => { const verified = await cl.verify(result.receipt); const verifiedStatus = statusOf(verified); -console.log(`Original receipt verification: ${verifiedStatus === "VERIFIED" ? "VERIFIED" : verifiedStatus}`); +process.stdout.write(`Original receipt verification: ${verifiedStatus === "VERIFIED" ? "VERIFIED" : verifiedStatus}\n`); const tamperedReceipt = structuredClone(result.receipt); @@ -53,6 +49,6 @@ if (!tamperedReceipt.output || typeof tamperedReceipt.output !== "object" || Arr const tampered = await cl.verify(tamperedReceipt); const tamperedStatus = statusOf(tampered); -console.log(`Tampered receipt verification: ${tamperedStatus === "INVALID" ? "INVALID" : tamperedStatus}`); +process.stdout.write(`Tampered receipt verification: ${tamperedStatus === "INVALID" ? "INVALID" : tamperedStatus}\n`); -console.log("\nAgents don’t make claims — they produce proof."); +process.stdout.write("\nAgents don't make claims — they produce proof.\n"); diff --git a/examples/openai-tool-wrapper.ts b/examples/openai-tool-wrapper.ts index 507dc7f..cb35e83 100644 --- a/examples/openai-tool-wrapper.ts +++ b/examples/openai-tool-wrapper.ts @@ -1,3 +1,4 @@ +import "dotenv/config"; import { CommandLayer } from "../src/index.js"; const privateKeyPem = process.env.CL_PRIVATE_KEY_PEM; @@ -20,9 +21,9 @@ const result = await cl.wrap("tool.get_weather", async () => ({ forecast: "sunny", })); -console.log("tool_call", toolCall); -console.log("output", result.output); -console.log("receipt", JSON.stringify(result.receipt, null, 2)); +process.stdout.write(`tool_call: ${JSON.stringify(toolCall)}\n`); +process.stdout.write(`output: ${JSON.stringify(result.output)}\n`); +process.stdout.write(`receipt: ${JSON.stringify(result.receipt, null, 2)}\n`); const verification = await cl.verify(result.receipt); -console.log("verification_status", verification); +process.stdout.write(`verification_status: ${JSON.stringify(verification)}\n`); diff --git a/examples/workflow-job-runner.ts b/examples/workflow-job-runner.ts index bc48c42..ad72714 100644 --- a/examples/workflow-job-runner.ts +++ b/examples/workflow-job-runner.ts @@ -1,3 +1,4 @@ +import "dotenv/config"; import { CommandLayer } from "../src/index.js"; const privateKeyPem = process.env.CL_PRIVATE_KEY_PEM; @@ -14,8 +15,8 @@ const result = await cl.wrap("workflow.run", async () => ({ steps_completed: ["lead_received", "send_followup", "update_crm"], })); -console.log("output", result.output); -console.log("receipt", JSON.stringify(result.receipt, null, 2)); +process.stdout.write(`output: ${JSON.stringify(result.output)}\n`); +process.stdout.write(`receipt: ${JSON.stringify(result.receipt, null, 2)}\n`); const verification = await cl.verify(result.receipt); -console.log("verification_status", verification); +process.stdout.write(`verification_status: ${JSON.stringify(verification)}\n`); diff --git a/examples/wrapped-agent-demo.ts b/examples/wrapped-agent-demo.ts index 2d47f6f..f9e1d9b 100644 --- a/examples/wrapped-agent-demo.ts +++ b/examples/wrapped-agent-demo.ts @@ -1,5 +1,11 @@ +import "dotenv/config"; import { CommandLayer } from "../src/index.js"; +if (!process.env.CL_PRIVATE_KEY_PEM) { + console.error("Missing CL_PRIVATE_KEY_PEM. Copy .env.example to .env and add a PKCS8 Ed25519 private key."); + process.exit(1); +} + const cl = new CommandLayer({ agent: process.env.CL_AGENT ?? "runtime.commandlayer.eth", privateKeyPem: process.env.CL_PRIVATE_KEY_PEM, @@ -10,8 +16,8 @@ const result = await cl.wrap("summarize", async () => { return "hello world"; }); -console.log(result.output); -console.log(result.receipt); +process.stdout.write(`${JSON.stringify(result.output)}\n`); +process.stdout.write(`${JSON.stringify(result.receipt, null, 2)}\n`); const verified = await cl.verify(result.receipt); -console.log(verified); +process.stdout.write(`${JSON.stringify(verified)}\n`); diff --git a/python-sdk/README.md b/python-sdk/README.md index 332f035..fe8508a 100644 --- a/python-sdk/README.md +++ b/python-sdk/README.md @@ -1,10 +1,74 @@ # CommandLayer Python SDK -This directory is a placeholder for the Python SDK. +Signs and verifies CommandLayer agent action receipts using Ed25519. -The Python SDK is not yet implemented. Until it ships, use the REST API directly: +## Install -- **Sign**: `POST https://runtime.commandlayer.org/sign` -- **Verify**: `POST https://runtime.commandlayer.org/verify` +```bash +pip install commandlayer-agent-sdk[crypto] +``` -See the [CommandLayer Protocol](https://commandlayer.org/protocol) for the full receipt format specification. +The `[crypto]` extra installs the `cryptography` package required for Ed25519 signing. + +## Quickstart + +```python +import os +from commandlayer import CommandLayer + +cl = CommandLayer( + signer=os.environ["CL_AGENT"], + key_id=os.environ["CL_KEY_ID"], + private_key_pem=os.environ["CL_PRIVATE_KEY_PEM"], +) + +result = cl.wrap("verify", lambda: {"approved": True}, input={"challenge": "abc"}) +print(result["output"]) +print(result["receipt"]) + +verification = cl.verify(result["receipt"]) +print(verification) +``` + +## Environment variables + +| Variable | Description | +|----------|-------------| +| `CL_AGENT` | ENS name of the signing agent | +| `CL_KEY_ID` | Key identifier | +| `CL_PRIVATE_KEY_PEM` | PKCS8-encoded Ed25519 private key | +| `CL_VERIFIER_URL` | Override the verifier endpoint (optional) | + +## API + +### `CommandLayer(*, signer, key_id, private_key_pem, [canonicalization], [verifier_url])` + +Constructor. `agent` is accepted as an alias for `signer`. + +### `cl.wrap(verb, fn, *, input=None) -> dict` + +Executes `fn()`, records execution metadata, signs the receipt with Ed25519, and returns `{"output": ..., "receipt": ...}`. + +If `fn()` raises, the error is recorded in `receipt.execution.error` and `status` is set to `"error"`. + +### `cl.verify(receipt) -> dict` + +POSTs the receipt to the configured verifier URL and returns the parsed JSON response. + +## Receipt proof schema + +```json +{ + "proof": { + "alg": "ed25519", + "canonical": "json.sorted_keys.v1", + "kid": "", + "signature": "", + "signer_id": "" + } +} +``` + +## License + +MIT diff --git a/python-sdk/commandlayer/__init__.py b/python-sdk/commandlayer/__init__.py index 91347a2..a0e7cc8 100644 --- a/python-sdk/commandlayer/__init__.py +++ b/python-sdk/commandlayer/__init__.py @@ -1,3 +1,198 @@ -"""CommandLayer Python SDK scaffold.""" +"""CommandLayer Python SDK. -__all__ = [] +Provides wrap() for signing agent action receipts with Ed25519, +and verify() for validating receipts against the CommandLayer verifier. +""" + +from __future__ import annotations + +import hashlib +import json +import os +import time +import urllib.request +import urllib.error +from datetime import datetime, timezone +from typing import Any, Callable, TypeVar + +try: + from cryptography.hazmat.primitives.asymmetric.ed25519 import Ed25519PrivateKey + from cryptography.hazmat.primitives.serialization import ( + Encoding, + PublicFormat, + load_pem_private_key, + ) + import base64 + _CRYPTO_AVAILABLE = True +except ImportError as _crypto_import_err: # pragma: no cover + _CRYPTO_AVAILABLE = False + _crypto_import_err_msg = str(_crypto_import_err) + +T = TypeVar("T") + +DEFAULT_VERIFIER_URL = "https://runtime.commandlayer.org/verify" +TRUST_FAMILY = "trust-verification" +TRUST_VERSION = "1.0.0" + + +def _canonicalize(value: Any) -> str: # noqa: ANN401 + """Serialize value to canonical JSON with sorted keys, recursively.""" + if isinstance(value, dict): + return "{" + ",".join( + f'{json.dumps(k)}:{_canonicalize(value[k])}' + for k in sorted(value.keys()) + ) + "}" + if isinstance(value, list): + return "[" + ",".join(_canonicalize(v) for v in value) + "]" + return json.dumps(value) + + +def _load_private_key(pem: str) -> "Ed25519PrivateKey": + if not _CRYPTO_AVAILABLE: + raise RuntimeError( + "cryptography package is required for signing. " + "Install it with: pip install cryptography" + ) + key = load_pem_private_key(pem.encode(), password=None) + if not isinstance(key, Ed25519PrivateKey): + raise ValueError("Only Ed25519 private keys are supported") + return key + + +def _sign_ed25519_base64(private_key: "Ed25519PrivateKey", message: str) -> str: + signature = private_key.sign(message.encode()) + return base64.b64encode(signature).decode() + + +class CommandLayer: + """Signs and verifies CommandLayer agent action receipts.""" + + def __init__( + self, + *, + signer: str | None = None, + agent: str | None = None, + key_id: str, + private_key_pem: str, + canonicalization: str = "json.sorted_keys.v1", + verifier_url: str = DEFAULT_VERIFIER_URL, + ) -> None: + resolved_signer = agent or signer + if not resolved_signer: + raise ValueError("signer or agent is required") + if not private_key_pem: + raise ValueError("private_key_pem is required") + if not key_id: + raise ValueError("key_id is required") + + self._signer = resolved_signer + self._key_id = key_id + self._private_key_pem = private_key_pem + self._canonicalization = canonicalization + self.verifier_url = verifier_url + + def wrap(self, verb: str, fn: Callable[[], T], *, input: Any = None) -> dict[str, Any]: # noqa: ANN401 + """Execute fn, record execution metadata, sign, and return {output, receipt}.""" + input_payload: Any = input if input is not None else {} + started_at = datetime.now(timezone.utc).isoformat() + started_ms = time.monotonic_ns() // 1_000_000 + + try: + output = fn() + completed_at = datetime.now(timezone.utc).isoformat() + duration_ms = (time.monotonic_ns() // 1_000_000) - started_ms + + receipt = self._build_receipt( + verb=verb, + input_payload=input_payload, + output=output, + started_at=started_at, + completed_at=completed_at, + duration_ms=duration_ms, + status="ok", + ) + return {"output": output, "receipt": receipt} + + except Exception as err: + completed_at = datetime.now(timezone.utc).isoformat() + duration_ms = (time.monotonic_ns() // 1_000_000) - started_ms + + receipt = self._build_receipt( + verb=verb, + input_payload=input_payload, + output=None, + started_at=started_at, + completed_at=completed_at, + duration_ms=duration_ms, + status="error", + error=str(err), + ) + return {"output": None, "receipt": receipt} + + def _build_receipt( + self, + *, + verb: str, + input_payload: Any, # noqa: ANN401 + output: Any, # noqa: ANN401 + started_at: str, + completed_at: str, + duration_ms: int, + status: str, + error: str | None = None, + ) -> dict[str, Any]: + execution: dict[str, Any] = { + "completed_at": completed_at, + "duration_ms": duration_ms, + "started_at": started_at, + "status": status, + } + if error is not None: + execution["error"] = error + + payload: dict[str, Any] = { + "execution": execution, + "family": TRUST_FAMILY, + "input": input_payload, + "output": output, + "signer": self._signer, + "ts": started_at, + "verb": verb, + "version": TRUST_VERSION, + } + + canonical = _canonicalize(payload) + private_key = _load_private_key(self._private_key_pem) + signature = _sign_ed25519_base64(private_key, canonical) + + return { + **payload, + "proof": { + "alg": "ed25519", + "canonical": self._canonicalization, + "kid": self._key_id, + "signature": signature, + "signer_id": self._signer, + }, + } + + def verify(self, receipt: dict[str, Any]) -> dict[str, Any]: # noqa: ANN401 + """Post receipt to verifier and return response as dict.""" + body = json.dumps({"receipt": receipt}).encode() + req = urllib.request.Request( + self.verifier_url, + data=body, + headers={"Content-Type": "application/json"}, + method="POST", + ) + try: + with urllib.request.urlopen(req, timeout=10) as resp: # noqa: S310 + return json.loads(resp.read()) + except urllib.error.HTTPError as err: + snippet = err.read(200).decode(errors="replace") + raise RuntimeError( + f"CommandLayer verify failed with status {err.code}: {snippet}" + ) from err + + +__all__ = ["CommandLayer", "DEFAULT_VERIFIER_URL", "TRUST_FAMILY", "TRUST_VERSION"] diff --git a/python-sdk/pyproject.toml b/python-sdk/pyproject.toml index 4f158ce..aba7fe4 100644 --- a/python-sdk/pyproject.toml +++ b/python-sdk/pyproject.toml @@ -4,9 +4,12 @@ build-backend = "setuptools.build_meta" [project] name = "commandlayer-agent-sdk" -version = "0.0.0" -description = "Early scaffold for CommandLayer Python SDK" +version = "1.0.0" +description = "Python SDK for signing and verifying CommandLayer agent receipts" readme = "README.md" requires-python = ">=3.9" license = { text = "MIT" } authors = [{ name = "CommandLayer" }] + +[project.optional-dependencies] +crypto = ["cryptography>=42.0"] diff --git a/src/index.ts b/src/index.ts index 7ee0771..adc1cd9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -31,6 +31,12 @@ export interface WrapResult { receipt: Receipt; } +export interface VerifyResult { + ok?: boolean; + status?: string; + [key: string]: unknown; +} + const TRUST_VERBS = [ "verify", "authenticate", @@ -66,10 +72,6 @@ export class CommandLayer { const signer = config.agent ?? config.signer; const privateKeyPem = config.privateKeyPem ?? config.privateKey; - if (config.privateKey && !config.privateKeyPem) { - console.warn("[CommandLayer] `privateKey` is deprecated. Use `privateKeyPem` instead."); - } - if (!signer) { throw new Error("Missing signer (agent or signer required)"); } @@ -186,12 +188,12 @@ export class CommandLayer { } } - async verify(receipt: Receipt): Promise { + async verify(receipt: Receipt): Promise { const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), 10_000); try { - const response = await fetch(this.verifierUrl ?? DEFAULT_VERIFIER_URL, { + const response = await fetch(this.verifierUrl, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ receipt }), @@ -205,7 +207,7 @@ export class CommandLayer { ); } - return response.json(); + return response.json() as Promise; } finally { clearTimeout(timeout); } diff --git a/src/schemas.trust-receipt-v1.json b/src/schemas.trust-receipt-v1.json index 6ff9586..e695201 100644 --- a/src/schemas.trust-receipt-v1.json +++ b/src/schemas.trust-receipt-v1.json @@ -2,51 +2,113 @@ "$schema": "https://json-schema.org/draft/2020-12/schema", "$id": "https://commandlayer.org/schemas/trust-receipt-v1.json", "type": "object", - "allOf": [ - { - "$ref": "trust-request-v1" + "additionalProperties": false, + "required": [ + "version", + "family", + "signer", + "verb", + "ts", + "input", + "output", + "execution", + "proof" + ], + "properties": { + "version": { + "const": "1.0.0" }, - { + "family": { + "const": "trust-verification" + }, + "signer": { + "type": "string", + "minLength": 1 + }, + "verb": { + "type": "string", + "enum": [ + "verify", + "authenticate", + "authorize", + "attest", + "sign", + "permit", + "grant", + "approve", + "reject", + "endorse" + ] + }, + "ts": { + "type": "string", + "format": "date-time" + }, + "input": true, + "output": true, + "execution": { + "type": "object", + "additionalProperties": false, + "required": [ + "status", + "duration_ms", + "started_at", + "completed_at" + ], + "properties": { + "status": { + "type": "string", + "enum": ["ok", "error"] + }, + "duration_ms": { + "type": "integer", + "minimum": 0 + }, + "started_at": { + "type": "string", + "format": "date-time" + }, + "completed_at": { + "type": "string", + "format": "date-time" + }, + "error": { + "type": "string" + } + } + }, + "proof": { "type": "object", "additionalProperties": false, "required": [ - "proof" + "canonical", + "alg", + "signature", + "kid", + "signer_id" ], "properties": { - "proof": { - "type": "object", - "additionalProperties": false, - "required": [ - "canonical", - "alg", - "signature", - "kid", - "signer_id" - ], - "properties": { - "canonical": { - "const": "json.sorted_keys.v1" - }, - "alg": { - "type": "string", - "enum": ["ed25519"] - }, - "signature": { - "type": "string", - "minLength": 16, - "pattern": "^[A-Za-z0-9+/=]+$" - }, - "kid": { - "type": "string", - "minLength": 1 - }, - "signer_id": { - "type": "string", - "minLength": 1 - } - } + "canonical": { + "const": "json.sorted_keys.v1" + }, + "alg": { + "type": "string", + "enum": ["ed25519"] + }, + "signature": { + "type": "string", + "minLength": 16, + "pattern": "^[A-Za-z0-9+/=]+$" + }, + "kid": { + "type": "string", + "minLength": 1 + }, + "signer_id": { + "type": "string", + "minLength": 1 } } } - ] + } } diff --git a/src/trust.ts b/src/trust.ts index 8eb4d81..ff75e58 100644 --- a/src/trust.ts +++ b/src/trust.ts @@ -1,5 +1,5 @@ import { createRequire } from "node:module"; -import Ajv from "ajv"; +import Ajv, { type ErrorObject } from "ajv"; import addFormats from "ajv-formats"; export interface TrustValidationResult { @@ -8,7 +8,7 @@ export interface TrustValidationResult { } const _require = createRequire(import.meta.url); -const trustRequestSchema = _require("./schemas.trust-request-v1.json") as unknown; +const trustRequestSchema = _require("./schemas.trust-request-v1.json") as Record; const trustReceiptSchema = _require("./schemas.trust-receipt-v1.json") as Record; const ajv = new Ajv({ allErrors: true, strict: false }); @@ -18,8 +18,8 @@ ajv.addSchema(trustRequestSchema, "trust-request-v1"); const validateTrustRequestSchema = ajv.compile(trustRequestSchema); const validateTrustReceiptSchema = ajv.compile(trustReceiptSchema); -function formatErrors(errors: typeof validateTrustRequestSchema.errors): string[] { - return (errors ?? []).map((error: { instancePath?: string; message?: string }) => { +function formatErrors(errors: ErrorObject[] | null | undefined): string[] { + return (errors ?? []).map((error: ErrorObject) => { const path = error.instancePath || "/"; return `${path} ${error.message ?? "is invalid"}`; });