const SHA40 = /^[0-9a-f]{40}$/; const SHA256 = /^[0-9a-f]{64}$/; const RESULT_STATES = new Set(["PASS", "FAIL"]); function exactKeys(value, expected, label) { if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(`${label} must be an object`); } const actual = Object.keys(value).sort(); const wanted = [...expected].sort(); if (JSON.stringify(actual) !== JSON.stringify(wanted)) { throw new Error(`${label} fields do not match the contract`); } } export function validateSnapshot(index, receipt) { if (!index || !Array.isArray(index.kernels) || !index.kernels.length) { throw new Error("contract index has no kernels"); } if (!receipt || !Array.isArray(receipt.results)) { throw new Error("receipt has no results"); } if ( typeof index.recorded_at !== "string" || index.recorded_at.length === 0 || receipt.recorded_at !== index.recorded_at ) { throw new Error("receipt recorded_at does not match the contract index"); } const resultById = new Map(); for (const result of receipt.results) { if (!result || typeof result.id !== "string" || resultById.has(result.id)) { throw new Error("receipt result IDs must be unique non-empty strings"); } if (!RESULT_STATES.has(result.status)) { throw new Error(`receipt result ${result.id} has an unsupported status`); } if (!SHA40.test(result.revision || "")) { throw new Error(`receipt result ${result.id} has an invalid revision`); } if (!SHA256.test(result.tree_digest_sha256 || "")) { throw new Error(`receipt result ${result.id} has an invalid tree digest`); } resultById.set(result.id, result); } const indexIds = new Set(); const statuses = new Map(); for (const row of index.kernels) { if (!row || typeof row.id !== "string" || indexIds.has(row.id)) { throw new Error("contract index IDs must be unique non-empty strings"); } indexIds.add(row.id); const result = resultById.get(row.id); if (!result) { throw new Error(`receipt is missing ${row.id}`); } if (result.revision !== row.revision) { throw new Error(`receipt revision does not match ${row.id}`); } if (result.tree_digest_sha256 !== row.tree_digest_sha256) { throw new Error(`receipt tree digest does not match ${row.id}`); } statuses.set(row.id, result.status); } if (resultById.size !== indexIds.size) { throw new Error("receipt and contract index ID sets do not match"); } const summary = { kernels: receipt.results.length, passed: receipt.results.filter((result) => result.status === "PASS").length, failed: receipt.results.filter((result) => result.status === "FAIL").length, }; exactKeys(receipt.summary, ["failed", "kernels", "passed"], "receipt summary"); if ( receipt.summary.kernels !== summary.kernels || receipt.summary.passed !== summary.passed || receipt.summary.failed !== summary.failed || summary.kernels !== index.kernels.length ) { throw new Error("receipt summary does not recompute from exact results"); } return statuses; } export function classifyLiveHead(data, expectedRevision) { if (!SHA40.test(expectedRevision || "")) { return { state: "UNAVAILABLE", reason: "Recorded revision is malformed" }; } if (!data || typeof data !== "object" || !SHA40.test(data.sha || "")) { return { state: "UNAVAILABLE", reason: "Live-head response is malformed" }; } if (data.sha === expectedRevision) { return { state: "MATCH", sha: data.sha }; } return { state: "DRIFT", sha: data.sha }; } export async function fetchLiveHead( url, expectedRevision, { fetchImpl = globalThis.fetch, timeoutMs = 8000 } = {}, ) { if (typeof fetchImpl !== "function") { return { state: "UNAVAILABLE", reason: "Fetch is unavailable" }; } const controller = new AbortController(); const timeout = setTimeout(() => controller.abort(), timeoutMs); try { const response = await fetchImpl(url, { cache: "no-store", headers: { Accept: "application/json" }, signal: controller.signal, }); if (!response.ok) { return { state: "UNAVAILABLE", reason: `HTTP ${response.status}` }; } return classifyLiveHead(await response.json(), expectedRevision); } catch (error) { const reason = controller.signal.aborted ? `Live-head request timed out after ${timeoutMs}ms` : error instanceof Error ? error.message : "Live-head request failed"; return { state: "UNAVAILABLE", reason }; } finally { clearTimeout(timeout); } }