#!/usr/bin/env node /** * Distribution provenance gate. * * Every tracked media/model/font artifact must be listed exactly once in * PROVENANCE.json and match its recorded SHA-256. Original generated artifacts * must name a tracked generator and lineage note. A copied item additionally * needs an immutable upstream revision, exact upstream path, and intake record. */ import { createHash } from "node:crypto"; import { execFileSync } from "node:child_process"; import { readFileSync } from "node:fs"; import { dirname, extname, resolve } from "node:path"; import { fileURLToPath } from "node:url"; const root = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const manifestPath = resolve(root, "PROVENANCE.json"); const ARTIFACT_EXTENSIONS = new Set([ ".png", ".jpg", ".jpeg", ".webp", ".gif", ".svg", ".ico", ".glb", ".gltf", ".fbx", ".obj", ".hdr", ".exr", ".ttf", ".otf", ".woff", ".woff2", ".mp3", ".wav", ".ogg", ".mp4", ".webm", ]); const FONT_EXTENSIONS = new Set([".ttf", ".otf", ".woff", ".woff2"]); const ALLOWED_ARTIFACT_LICENSES = new Set([ "Apache-2.0", "BSD-3-Clause", "CC0-1.0", "CC-BY-4.0", "ISC", "MIT", ]); const problems = []; function trackedFiles() { try { return execFileSync("git", ["ls-files", "-z"], { cwd: root, encoding: "utf8", maxBuffer: 64 * 1024 * 1024, }).split("\0").filter(Boolean); } catch { console.error("check-provenance: FAIL — run this gate inside a git checkout."); process.exit(1); } } function nonEmpty(value) { return typeof value === "string" && value.trim().length > 0; } function validDate(value) { return typeof value === "string" && /^\d{4}-\d{2}-\d{2}$/.test(value); } function hash(path) { return createHash("sha256").update(readFileSync(resolve(root, path))).digest("hex"); } function pathIsCovered(path, tracked) { if (path.endsWith("/")) return [...tracked].some((candidate) => candidate.startsWith(path)); return tracked.has(path); } let manifest; try { manifest = JSON.parse(readFileSync(manifestPath, "utf8")); } catch (error) { console.error(`check-provenance: FAIL — cannot parse PROVENANCE.json (${String(error)}).`); process.exit(1); } const tracked = new Set(trackedFiles()); const artifacts = [...tracked].filter((path) => ARTIFACT_EXTENSIONS.has(extname(path).toLowerCase())); const records = Array.isArray(manifest.distributedArtifacts) ? manifest.distributedArtifacts : []; const recordCounts = new Map(); if (manifest.schemaVersion !== 1) problems.push("schemaVersion must be 1"); if (!nonEmpty(manifest.projectLicense)) problems.push("projectLicense is required"); for (const [index, record] of records.entries()) { const at = `distributedArtifacts[${index}]`; if (!nonEmpty(record?.path)) { problems.push(`${at}: path is required`); continue; } recordCounts.set(record.path, (recordCounts.get(record.path) ?? 0) + 1); if (!tracked.has(record.path)) problems.push(`${at}: ${record.path} is not a tracked file`); if (!ARTIFACT_EXTENSIONS.has(extname(record.path).toLowerCase())) { problems.push(`${at}: ${record.path} is not a recognized distribution artifact`); } if (!nonEmpty(record.kind)) problems.push(`${at}: kind is required`); if (!ALLOWED_ARTIFACT_LICENSES.has(record.license)) { problems.push(`${at}: license ${String(record.license)} is not in the reviewed allowlist`); } if (!/^[a-f0-9]{64}$/.test(record.sha256 ?? "")) problems.push(`${at}: sha256 must be 64 lowercase hex characters`); else if (tracked.has(record.path) && hash(record.path) !== record.sha256) problems.push(`${at}: SHA-256 mismatch for ${record.path}`); if (!validDate(record.intakeDate)) problems.push(`${at}: intakeDate must be YYYY-MM-DD`); if (!nonEmpty(record.intakeNote)) problems.push(`${at}: intakeNote is required`); if (record.origin === "repository-generated") { if (!nonEmpty(record.generator) || !tracked.has(record.generator)) { problems.push(`${at}: repository-generated artifact requires a tracked generator`); } if (!Array.isArray(record.inputs) || record.inputs.length === 0 || record.inputs.some((input) => !nonEmpty(input) || !tracked.has(input))) { problems.push(`${at}: repository-generated artifact requires non-empty tracked inputs`); } if (record.source !== undefined) problems.push(`${at}: repository-generated artifact must not claim an upstream source`); } else if (record.origin === "copied") { const source = record.source; if (!nonEmpty(source?.url) || !/^https:\/\//.test(source.url)) problems.push(`${at}: copied item requires an HTTPS source URL`); if (!nonEmpty(source?.upstreamPath)) problems.push(`${at}: copied item requires its exact upstreamPath`); if (!nonEmpty(source?.revision) || !/^[a-f0-9]{40,64}$/i.test(source.revision)) { problems.push(`${at}: copied item requires an immutable 40-64 hex upstream revision`); } else if (nonEmpty(source?.url) && !source.url.toLowerCase().includes(source.revision.toLowerCase())) { problems.push(`${at}: copied item source URL must be pinned to its recorded revision`); } } else { problems.push(`${at}: origin must be repository-generated or copied`); } } for (const artifact of artifacts) { const count = recordCounts.get(artifact) ?? 0; if (count !== 1) problems.push(`${artifact}: expected exactly one manifest record, found ${count}`); } for (const [path, count] of recordCounts) { if (count !== 1) problems.push(`${path}: duplicate manifest records (${count})`); } const lineage = Array.isArray(manifest.proceduralLineage) ? manifest.proceduralLineage : []; if (lineage.length === 0) problems.push("proceduralLineage must contain at least one original-source record"); for (const [index, record] of lineage.entries()) { const at = `proceduralLineage[${index}]`; if (!nonEmpty(record?.path) || !pathIsCovered(record.path, tracked)) problems.push(`${at}: path must identify tracked source`); if (record?.origin !== "original") problems.push(`${at}: origin must be original`); if (!nonEmpty(record?.kind)) problems.push(`${at}: kind is required`); if (record?.sourceLicense !== "Apache-2.0") problems.push(`${at}: sourceLicense must be Apache-2.0`); if (!nonEmpty(record?.lineage)) problems.push(`${at}: lineage is required`); } const trackedFonts = artifacts.filter((path) => FONT_EXTENSIONS.has(extname(path).toLowerCase())); const codeFiles = [...tracked].filter((path) => path !== "scripts/check-provenance.mjs" && /\.(?:css|html|js|mjs|ts|tsx)$/.test(path) ); for (const path of codeFiles) { const text = readFileSync(resolve(root, path), "utf8"); if (/@font-face\b/i.test(text) || /fonts\.(?:googleapis|gstatic)\.com/i.test(text)) { problems.push(`${path}: bundled/remote web fonts are forbidden by the system-font policy`); } } if (problems.length > 0) { console.error("\ncheck-provenance: FAIL\n"); for (const problem of problems) console.error(` ${problem}`); console.error(""); process.exit(1); } console.log(`check-provenance: ok — ${artifacts.length} tracked distribution artifacts are hash-manifested.`); console.log(` ${records.filter((record) => record.origin === "copied").length} copied items; ${lineage.length} original procedural/data lineage records.`); console.log(` ${trackedFonts.length} bundled fonts; system-font/no-remote-font policy verified.`);