import { createHash } from "node:crypto"; import { mkdir, readFile, writeFile } from "node:fs/promises"; import { isValidSha512Integrity, parsePnpmLockfilePackages, supplyChainDigest, verifySupplyChainCoherence, } from "./lib/supply-chain.ts"; import { assertMatchesJsonSchema } from "./lib/json-schema.ts"; import { collectDistOutputs, distSha256, } from "./lib/release-candidate.ts"; type Document = Record; function isRecord(value: unknown): value is Document { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } function parseDocument(text: string, label: string): Document { const parsed: unknown = JSON.parse(text); if (!isRecord(parsed)) throw new Error(`${label} must be a JSON object`); return parsed; } function recordRows(value: unknown): Document[] { return Array.isArray(value) ? value.filter(isRecord) : []; } async function readDocument(file: string): Promise { return parseDocument(await readFile(file, "utf8"), file); } const inventory = await readDocument( "artifacts/release/dependency-inventory.json", ); const sbom = await readDocument("artifacts/release/sbom.cdx.json"); const provenance = await readDocument("artifacts/release/provenance.json"); const verification = await readDocument( "artifacts/security/supply-chain-verification.json", ); const artifactSchemaFailures: string[] = []; for (const [schemaPath, artifact, label] of [ [ "schemas/artifacts/dependency-inventory.schema.json", inventory, "dependency inventory", ], [ "schemas/artifacts/supply-chain-verification.schema.json", verification, "supply-chain verification", ], ] as const) { try { assertMatchesJsonSchema(await readDocument(schemaPath), artifact, label); } catch { artifactSchemaFailures.push(`${label} JSON Schema mismatch`); } } const lockfileText = await readFile("pnpm-lock.yaml", "utf8"); const lockfileSha256 = createHash("sha256") .update(lockfileText) .digest("hex"); const outputs = await collectDistOutputs(); const distDigest = distSha256(outputs); const coherence = verifySupplyChainCoherence( sbom, inventory, provenance, distDigest, ); const failures: string[] = [ ...artifactSchemaFailures, ...coherence.failures, ]; if ( inventory.lockfileSha256 !== lockfileSha256 || verification.lockfileSha256 !== lockfileSha256 ) { failures.push("inventory/verification lockfile digest mismatch"); } if ( verification.distSha256 !== distDigest || verification.sbomSha256 !== supplyChainDigest(sbom) ) { failures.push("verification digest set is incoherent"); } const lockRows = parsePnpmLockfilePackages(lockfileText); const inventoryRows = recordRows(inventory.dependencies); const inventoryByIdentity = new Map( inventoryRows.map( (entry) => [ `${String(entry.name ?? "")}@${String(entry.version ?? "")}`, entry, ] as const, ), ); if (lockRows.length !== inventoryRows.length) { failures.push("transitive dependency count differs from lockfile"); } for (const lockRow of lockRows) { const identity = `${lockRow.name}@${lockRow.version}`; const dependency = inventoryByIdentity.get(identity); if ( !dependency || dependency.integrity !== lockRow.integrity || !isValidSha512Integrity(lockRow.integrity) ) { failures.push(`lockfile inventory integrity mismatch: ${identity}`); } } const report = { schemaVersion: 1, status: failures.length === 0 ? "PASS" : "FAIL", dependencyCount: inventoryRows.length, lockfileSha256, distSha256: distDigest, sbomSha256: supplyChainDigest(sbom), failures, }; await mkdir("artifacts/security", { recursive: true }); await writeFile( "artifacts/security/supply-chain-coherence.json", `${JSON.stringify(report, null, 2)}\n`, ); if (failures.length > 0) { process.stderr.write( `Supply-chain artifact coherence failed:\n- ${failures.join("\n- ")}\n`, ); process.exit(1); } process.stdout.write( `Supply-chain artifact coherence: PASS (${inventoryRows.length} dependencies)\n`, );