88 lines
5.5 KiB
JavaScript
88 lines
5.5 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import crypto from "node:crypto";
|
|
import fs from "node:fs";
|
|
import os from "node:os";
|
|
import path from "node:path";
|
|
import { execFileSync } from "node:child_process";
|
|
import { fileURLToPath, pathToFileURL } from "node:url";
|
|
import ts from "../../web/node_modules/typescript/lib/typescript.js";
|
|
|
|
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
const parentPath = path.join(repoRoot, "tests/golden/M12-05E/bound-runtime-receipts.json");
|
|
const freshPath = path.join(repoRoot, "tests/golden/M12-05F/fresh-runtime-receipts.json");
|
|
const appFreshPath = path.join(repoRoot, "web/app/src/capabilities/io-format-runtime-receipts-freshness.json");
|
|
const appExpectedPath = path.join(repoRoot, "web/app/src/capabilities/io-format-runtime-receipts-freshness-expected.json");
|
|
const appPath = path.join(repoRoot, "web/app/src/app/App.tsx");
|
|
const protocolPath = path.join(repoRoot, "web/protocol/io-format-receipt-freshness.ts");
|
|
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "io-format-receipt-freshness-"));
|
|
const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
const stableValue = (value) => Array.isArray(value) ? value.map(stableValue) : value && typeof value === "object" ? Object.fromEntries(Object.keys(value).sort().map((key) => [key, stableValue(value[key])])) : value;
|
|
const stableSha256 = (value) => sha256(JSON.stringify(stableValue(value)));
|
|
|
|
function transpile(sourcePath, outputName, replacements = []) {
|
|
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, fileName: sourcePath, reportDiagnostics: true });
|
|
assert.deepEqual(transpiled.diagnostics, []);
|
|
let output = transpiled.outputText;
|
|
for (const [from, to] of replacements) output = output.replaceAll(from, to);
|
|
const outputPath = path.join(temporary, outputName);
|
|
fs.writeFileSync(outputPath, output);
|
|
return outputPath;
|
|
}
|
|
|
|
try {
|
|
const parentBytes = fs.readFileSync(parentPath);
|
|
const bound = JSON.parse(parentBytes);
|
|
const freshBytes = fs.readFileSync(freshPath);
|
|
const fresh = JSON.parse(freshBytes);
|
|
assert.deepEqual(fs.readFileSync(appFreshPath), freshBytes, "App freshness receipt drifted from golden");
|
|
const appExpected = JSON.parse(fs.readFileSync(appExpectedPath));
|
|
const runtimeModule = transpile(path.join(repoRoot, "web/protocol/io-format-runtime-receipt.ts"), "io-format-runtime-receipt.mjs");
|
|
const bindingModule = transpile(path.join(repoRoot, "web/protocol/io-format-receipt-binding.ts"), "io-format-receipt-binding.mjs");
|
|
const protocolModule = transpile(protocolPath, "io-format-receipt-freshness.mjs", [["./io-format-receipt-binding\"", "./io-format-receipt-binding.mjs\""], ["./io-format-runtime-receipt\"", "./io-format-runtime-receipt.mjs\""]]);
|
|
const protocol = await import(pathToFileURL(protocolModule));
|
|
const expected = {
|
|
parentBindingSha256: sha256(parentBytes),
|
|
parentReceiptSetSha256: bound.parentReceiptSetSha256,
|
|
inventorySha256: bound.inventorySha256,
|
|
boundReceiptSetSha256: stableSha256(bound),
|
|
runtimeSha256: stableSha256(bound.runtime),
|
|
runtime: bound.runtime,
|
|
receiptIdentities: bound.receipts,
|
|
};
|
|
assert.equal(fresh.parentBindingSha256, expected.parentBindingSha256);
|
|
assert.equal(fresh.boundReceiptSetSha256, expected.boundReceiptSetSha256);
|
|
assert.equal(fresh.runtimeSha256, expected.runtimeSha256);
|
|
assert.deepEqual(appExpected, {
|
|
parentBindingSha256: expected.parentBindingSha256,
|
|
parentReceiptSetSha256: expected.parentReceiptSetSha256,
|
|
inventorySha256: expected.inventorySha256,
|
|
boundReceiptSetSha256: expected.boundReceiptSetSha256,
|
|
runtimeSha256: expected.runtimeSha256,
|
|
runtime: expected.runtime,
|
|
receiptIdentities: expected.receiptIdentities,
|
|
});
|
|
const appSource = fs.readFileSync(appPath, "utf8");
|
|
assert.match(appSource, /resolveFreshIOFormatRuntimeRoute\(IO_FORMAT_RUNTIME_RECEIPTS, IO_FORMAT_RUNTIME_RECEIPT_EXPECTED/);
|
|
await protocol.verifyIOFormatReceiptFreshness(fresh, expected);
|
|
assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(fresh, expected, { format: "GLB", operation: "EXPORT" }).status, "READY");
|
|
assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(fresh, expected, { format: "USD", operation: "EXPORT" }).status, "BLOCKED");
|
|
|
|
const forged = structuredClone(fresh);
|
|
forged.bound.receipts[0].operator = "forged.operator";
|
|
await assert.rejects(() => protocol.verifyIOFormatReceiptFreshness(forged, expected), (error) => error.reason === "RECEIPT_FORGED");
|
|
const stale = structuredClone(fresh);
|
|
stale.bound.inventorySha256 = "a".repeat(64);
|
|
assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(stale, expected, { format: "GLB", operation: "EXPORT" }).reason, "RECEIPT_STALE");
|
|
const crossVersion = structuredClone(fresh);
|
|
crossVersion.bound.runtime.versionTuple = [5, 3, 0];
|
|
assert.equal(protocol.resolveFreshIOFormatRuntimeRoute(crossVersion, expected, { format: "GLB", operation: "EXPORT" }).reason, "RECEIPT_CROSS_VERSION");
|
|
|
|
const regenerated = path.join(temporary, "fresh-runtime-receipts.json");
|
|
execFileSync(process.execPath, [path.join(repoRoot, "tools/web/generate-io-format-receipt-freshness.mjs"), "--output", regenerated], { cwd: repoRoot });
|
|
assert.deepEqual(fs.readFileSync(regenerated), freshBytes, "freshness receipt is not deterministic");
|
|
process.stdout.write("io-format-receipt-freshness-ok receipts=14 forged=BLOCKED stale=BLOCKED cross-version=BLOCKED deterministic=true\n");
|
|
}
|
|
finally {
|
|
fs.rmSync(temporary, { recursive: true, force: true });
|
|
}
|