diff --git a/typescript-sdk/package-lock.json b/typescript-sdk/package-lock.json index 8fc254b..1d56e32 100644 --- a/typescript-sdk/package-lock.json +++ b/typescript-sdk/package-lock.json @@ -25,7 +25,7 @@ "node": ">=20" }, "optionalDependencies": { - "@rollup/rollup-win32-x64-msvc": "4.57.1" + "@rollup/rollup-win32-x64-msvc": "^4.0.0" } }, "node_modules/@adraffy/ens-normalize": { @@ -882,7 +882,6 @@ "cpu": [ "x64" ], - "dev": true, "license": "MIT", "optional": true, "os": [ diff --git a/typescript-sdk/scripts/unit-tests.mjs b/typescript-sdk/scripts/unit-tests.mjs index 46255f7..cc9d210 100644 --- a/typescript-sdk/scripts/unit-tests.mjs +++ b/typescript-sdk/scripts/unit-tests.mjs @@ -85,6 +85,9 @@ const { verifyEd25519SignatureOverUtf8HashString, recomputeReceiptHashSha256, verifyReceipt, + isSingleProofSignature, + isMultiProofSignature, + getPrimaryProofSignature, resolveSignerKey, buildCommonsRequest, extractReceiptVerb, @@ -221,6 +224,14 @@ assert(vr.ok === true, "verifyReceipt ok for valid receipt (explicit key)"); assert(vr.checks.hash_matches === true, "verifyReceipt hash matches"); assert(vr.checks.signature_valid === true, "verifyReceipt signature valid"); assert(vr.checks.receipt_id_matches === true, "verifyReceipt tolerates absent receipt_id"); +assert(isSingleProofSignature({ alg: "Ed25519", value: "abc", kid: "kid-1" }), "isSingleProofSignature accepts valid object"); +assert( + isMultiProofSignature([ + { alg: "Ed25519", value: "abc", kid: "kid-1", role: "runtime" }, + { alg: "Ed25519", value: "def", kid: "kid-2", role: "verifier" } + ]), + "isMultiProofSignature accepts valid array" +); const vrEns = await verifyReceipt(receipt, { ens: { @@ -232,6 +243,31 @@ assert(vrEns.ok === true, "verifyReceipt ok with ENS cl.receipt.signer + cl.sig. assert(vrEns.values.pubkey_source === "ens", "verifyReceipt reports ENS key source"); assert(extractReceiptVerb(receipt) === "summarize", "extractReceiptVerb reads canonical verb field"); +const receiptWithTrace = JSON.parse(JSON.stringify(receipt)); +receiptWithTrace.metadata.trace = { trace_id: "trace_123", parent_trace_id: null }; +const { hash_sha256: hashWithTrace } = recomputeReceiptHashSha256(receiptWithTrace); +const traceSig = nacl.sign.detached(new Uint8Array(Buffer.from(hashWithTrace, "utf8")), kp.secretKey); +receiptWithTrace.metadata.proof.hash_sha256 = hashWithTrace; +receiptWithTrace.metadata.proof.signature_b64 = Buffer.from(traceSig).toString("base64"); +const vrWithTrace = await verifyReceipt(receiptWithTrace, { publicKey: `ed25519:${b64Key}` }); +assert(vrWithTrace.ok === true, "verifyReceipt supports optional metadata.trace"); + +const multiSigReceipt = JSON.parse(JSON.stringify(receipt)); +multiSigReceipt.metadata.proof.signature = [ + { alg: "Ed25519", value: receipt.metadata.proof.signature_b64, kid: "kid-runtime", role: "runtime" }, + { alg: "Ed25519", value: receipt.metadata.proof.signature_b64, kid: "kid-verifier", role: "verifier" } +]; +delete multiSigReceipt.metadata.proof.signature_b64; +const primarySig = getPrimaryProofSignature(multiSigReceipt.metadata.proof); +assert(primarySig?.kid === "kid-runtime", "getPrimaryProofSignature selects first multi-signature"); +const vrMulti = await verifyReceipt(multiSigReceipt, { publicKey: `ed25519:${b64Key}` }); +assert(vrMulti.ok === true, "verifyReceipt ok for multi-signature proof"); + +assert( + !isMultiProofSignature([{ alg: "Ed25519", value: "abc", kid: "kid-1", role: "invalid-role" }]), + "isMultiProofSignature rejects malformed role" +); + const builtRequest = buildCommonsRequest( "summarize", { input: { content: "hello" }, limits: { max_output_tokens: 10 } }, diff --git a/typescript-sdk/src/index.ts b/typescript-sdk/src/index.ts index f0fcd31..642d00e 100644 --- a/typescript-sdk/src/index.ts +++ b/typescript-sdk/src/index.ts @@ -33,9 +33,22 @@ export type ReceiptProof = { signer_id: string; hash_sha256?: string; signature_b64?: string; + signature?: ProofSignature; [k: string]: unknown; }; +export type SingleProofSignature = { + alg: "Ed25519"; + value: string; + kid: string; +}; + +export type MultiProofSignature = Array< + SingleProofSignature & { role: "user" | "solver" | "relayer" | "agent" | "runtime" | "verifier" } +>; + +export type ProofSignature = SingleProofSignature | MultiProofSignature; + export type ReceiptMetadata = { receipt_id?: string; proof: ReceiptProof; @@ -164,6 +177,31 @@ function isRecord(value: unknown): value is Record { return !!value && typeof value === "object" && !Array.isArray(value); } +export function isSingleProofSignature(value: unknown): value is SingleProofSignature { + return isRecord(value) && value.alg === "Ed25519" && typeof value.value === "string" && typeof value.kid === "string"; +} + +export function isMultiProofSignature(value: unknown): value is MultiProofSignature { + const validRoles = new Set(["user", "solver", "relayer", "agent", "runtime", "verifier"]); + return ( + Array.isArray(value) && + value.length > 0 && + value.every((entry) => { + if (!isRecord(entry)) return false; + const role = entry.role; + if (!isSingleProofSignature(entry)) return false; + return typeof role === "string" && validRoles.has(role); + }) + ); +} + +export function getPrimaryProofSignature(proof: Record): SingleProofSignature | null { + const sig = proof.signature; + if (isSingleProofSignature(sig)) return sig; + if (isMultiProofSignature(sig)) return sig[0] ?? null; + return null; +} + function isVerb(value: string): value is Verb { return (VERBS as readonly string[]).includes(value); } @@ -351,7 +389,8 @@ export async function verifyReceipt(receiptLike: CanonicalReceipt | CommandRespo const receipt = extractReceipt(receiptLike); const proof = isRecord(receipt.metadata?.proof) ? (receipt.metadata.proof as Record) : {}; const claimedHash = typeof proof.hash_sha256 === "string" ? proof.hash_sha256 : null; - const signatureB64 = typeof proof.signature_b64 === "string" ? proof.signature_b64 : null; + const primarySignature = getPrimaryProofSignature(proof); + const signatureB64 = typeof proof.signature_b64 === "string" ? proof.signature_b64 : primarySignature?.value ?? null; const alg = typeof proof.alg === "string" ? proof.alg : null; const canonical = typeof proof.canonical === "string" ? proof.canonical : null; const signerId = typeof proof.signer_id === "string" ? proof.signer_id : null;