import { createHash } from "node:crypto"; import { readFile } from "node:fs/promises"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { lockedPackages } from "./lib/npm-locked-packages.mjs"; import { redactTransportAuthentication } from "./lib/transport-authentication.mjs"; import { ensureGeneratedFile } from "./lib/generated-file.mjs"; const arguments_ = new Set(process.argv.slice(2)); for (const argument of arguments_) if (argument !== "--check") throw new Error(`Unknown inventory option: ${argument}`); const root = join(dirname(fileURLToPath(import.meta.url)), ".."); const lock = JSON.parse( await readFile(join(root, "package-lock.json"), "utf8"), ); const entries = lockedPackages(lock).map((entry) => ({ ...entry, resolved: redactTransportAuthentication(entry.resolved), })); const runtime = entries.filter(({ dev }) => !dev); const runtimeFingerprint = createHash("sha256") .update(JSON.stringify(runtime)) .digest("hex"); const source = [ "REA npm dependency inventory", "Prepared from the root package-lock.json; refresh this inventory when locked dependencies change.", "License identifiers are upstream lockfile declarations, not a legal assessment.", "Runtime includes transitive and optional platform packages; development includes build, test and website tooling.", `Locked package entries: ${entries.length}; runtime: ${runtime.length}.`, "Full runtime copyright/license/NOTICE texts: npm-runtime-notices.txt.", "", ...entries.flatMap((entry) => [ `${entry.name}@${entry.version} | ${entry.dev ? "development" : "runtime"}${entry.optional ? ", optional" : ""} | ${entry.license}`, ` Lockfile path: ${entry.path}`, ` Package: https://www.npmjs.com/package/${entry.name}/v/${entry.version}`, ` Archive: ${entry.resolved}`, ` Integrity: ${entry.integrity}`, ]), "", ].join("\n"); await ensureGeneratedFile({ path: join(root, "licenses/npm-dependencies.txt"), source, check: arguments_.has("--check"), generateCommand: "npm run licenses:generate", }); if (arguments_.has("--check")) { const notices = await readFile( join(root, "licenses/npm-runtime-notices.txt"), "utf8", ); const headings = [...notices.matchAll(/^===== (.+) =====$/gmu)]; const sections = new Map(); for (const [index, heading] of headings.entries()) { if (sections.has(heading[1])) throw new Error(`Duplicate runtime notice: ${heading[1]}`); sections.set( heading[1], notices.slice( heading.index + heading[0].length + 1, headings[index + 1]?.index, ), ); } for (const entry of runtime) { const identity = `${entry.name}@${entry.version}`; const section = sections.get(identity); if (section === undefined) throw new Error(`Missing runtime notice: ${identity}`); const metadata = [ `Declared license: ${entry.license}`, `Archive: ${entry.resolved}`, `Integrity: ${entry.integrity}`, "", ].join("\n"); if (!section.startsWith(metadata)) throw new Error(`Runtime notice metadata differs for ${identity}`); } if ( !notices .slice(0, headings[0]?.index) .includes( `Runtime dependency fingerprint (SHA-256): ${runtimeFingerprint}\n`, ) ) throw new Error( `Runtime notice fingerprint differs; refresh verified notices for ${runtimeFingerprint}`, ); const references = JSON.parse( await readFile(join(root, "licenses/upstream-sources.json"), "utf8"), ); for (const reference of references) { const bytes = await readFile(join(root, reference.file)); if (createHash("sha256").update(bytes).digest("hex") !== reference.sha256) throw new Error(`Preserved notice digest differs: ${reference.file}`); } } console.log( JSON.stringify({ locked_packages: entries.length, runtime_packages: runtime.length, runtime_dependency_fingerprint: runtimeFingerprint, }), );