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
3 changes: 1 addition & 2 deletions typescript-sdk/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

36 changes: 36 additions & 0 deletions typescript-sdk/scripts/unit-tests.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,9 @@ const {
verifyEd25519SignatureOverUtf8HashString,
recomputeReceiptHashSha256,
verifyReceipt,
isSingleProofSignature,
isMultiProofSignature,
getPrimaryProofSignature,
resolveSignerKey,
buildCommonsRequest,
extractReceiptVerb,
Expand Down Expand Up @@ -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: {
Expand All @@ -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 } },
Expand Down
41 changes: 40 additions & 1 deletion typescript-sdk/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -164,6 +177,31 @@ function isRecord(value: unknown): value is Record<string, unknown> {
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<string, unknown>): 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);
}
Expand Down Expand Up @@ -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<string, unknown>) : {};
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;
Expand Down
Loading