// Copyright (C) 2026 Kingsrook, LLC. // SPDX-License-Identifier: Apache-2.0 import { createHash } from "node:crypto"; import { copyFile, lstat, mkdir, readFile, readdir, writeFile } from "node:fs/promises"; import path from "node:path"; // Packaging only. Never imports the compiler, executes source or installs packages. // Root runs this with the approved exact Node executable after worker npm run build. if (process.versions.node !== "24.22.2" || process.argv.length !== 4) { throw new Error("Use Node 24.22.2: build-compiler-image.mjs "); } const worker = process.argv[2]; const output = process.argv[3]; if (!path.isAbsolute(worker) || !path.isAbsolute(output) || output.includes("*") || output.includes(",")) { throw new Error("Absolute literal paths required"); } const tsPackage = JSON.parse(await readFile(path.join(worker, "node_modules/typescript/package.json"), "utf8")); if (tsPackage.version !== "6.0.3") throw new Error("TypeScript 6.0.3 required"); const libs = (await readdir(path.join(worker, "node_modules/typescript/lib"))) .filter(name => /^lib\.[a-z0-9.]+\.d\.ts$/.test(name)) .map(name => "node_modules/typescript/lib/" + name); const files = ["package.json", "dist/compiler-entry.js", "dist/compiler.js", "dist/frames.js", "dist/protocol.js", "node_modules/typescript/package.json", "node_modules/typescript/lib/typescript.js", ...libs].sort(); if (files.length > 256 || libs.length === 0) throw new Error("Compiler closure size"); await mkdir(output); // Deliberately refuses any existing output; never overwrites a sealed image. const root = path.join(output, "compiler"); await mkdir(root); let total = 0; const hashes = {}; for (const name of files) { const source = path.join(worker, name); const stat = await lstat(source); if (!stat.isFile() || stat.isSymbolicLink() || stat.size < 1 || stat.size > 16 * 1024 * 1024) throw new Error("Invalid compiler source file"); total += stat.size; if (total > 64 * 1024 * 1024) throw new Error("Compiler closure bytes"); const content = await readFile(source); hashes[name] = createHash("sha256").update(content).digest("hex"); const target = path.join(root, name); await mkdir(path.dirname(target), { recursive: true }); await writeFile(target, content, { flag: "wx" }); } const manifest = JSON.stringify({ protocol: "qqq-typescript-image/1", nodeVersion: "24.22.2", typescriptVersion: "6.0.3", files: hashes }, null, 2) + "\n"; await writeFile(path.join(root, "compiler-manifest.json"), manifest, { flag: "wx" }); // Notices are adjacent to, never extra executable files inside, the closed tree. const notices = path.join(output, "notices"); await mkdir(notices); await copyFile(path.join(worker, "node_modules/typescript/LICENSE.txt"), path.join(notices, "typescript-LICENSE.txt")); await copyFile(path.join(worker, "LICENSE"), path.join(notices, "worker-LICENSE")); await copyFile(path.join(worker, "NOTICE"), path.join(notices, "worker-NOTICE")); const nodeHash = createHash("sha256").update(await readFile(process.execPath)).digest("hex"); const evidence = { nodePath: process.execPath, nodeSha256: nodeHash, compilerRoot: root, manifestSha256: createHash("sha256").update(manifest).digest("hex"), files: files.length, bytes: total }; await writeFile(path.join(output, "compiler-image.json"), JSON.stringify(evidence, null, 2) + "\n", { flag: "wx" }); console.log(JSON.stringify(evidence));