import { readFile, writeFile, mkdir, rm } from "node:fs/promises"; import { createHash } from "node:crypto"; import { resolve } from "node:path"; const development = process.argv.includes("--development-fixture"); const retained = JSON.parse(process.env.FLAREBOT_RETAINED_RELEASES ?? "[]"); if ( !Array.isArray(retained) || retained.some((path) => typeof path !== "string" || !path) ) throw new Error("Invalid retained release inputs"); if (retained.length || process.argv.some((a) => a.startsWith("--retain="))) throw new Error( "Retained releases are not supported by the latest-only catalog", ); const directories = [resolve("dist/release")]; const sha = (bytes) => createHash("sha256").update(bytes).digest("hex"); const bundles = []; let total = 0; for (const directory of directories) { const manifestBytes = await readFile(`${directory}/manifest.json`); const manifest = JSON.parse(manifestBytes); if (manifest.sourceDirty && !development) throw new Error( "Refusing to publish a dirty customer artifact. Commit source and rebuild, or select --development-fixture.", ); if ( manifest.files.reduce((sum, f) => sum + f.size, 0) > 24 * 1024 * 1024 || manifest.files .filter((f) => f.path.startsWith("worker/")) .reduce((sum, f) => sum + f.size, 0) > 16 * 1024 * 1024 ) throw new Error("Release exceeds deployment memory budget"); const artifactDigest = sha(manifestBytes); if ( artifactDigest !== (await readFile(`${directory}/manifest.sha256`, "utf8")).trim() ) throw new Error("Release manifest integrity mismatch"); const files = []; for (const file of [ { path: "manifest.json", size: manifestBytes.length, sha256: artifactDigest, }, ...manifest.files, ]) { if ( !/^(manifest\.json|deployment\.json|(?:worker|assets)\/[A-Za-z0-9_./-]+)$/.test( file.path, ) || file.path.split("/").some((p) => p === ".." || !p) ) throw new Error("Unsafe release path"); const bytes = await readFile(`${directory}/${file.path}`); if (bytes.length !== file.size || sha(bytes) !== file.sha256) throw new Error("Release file integrity mismatch"); total += bytes.length; if (total > 48 * 1024 * 1024) throw new Error("Catalog exceeds aggregate bundle budget"); files.push({ path: file.path, bytes }); } bundles.push({ identity: { version: manifest.release, sourceRevision: manifest.sourceRevision, artifactDigest, }, development, files, }); } await rm("dist/catalog", { recursive: true, force: true }); await mkdir("dist/catalog", { recursive: true }); let source = "// Generated immutable publisher catalog. Opaque Data, never browser assets.\n"; const entries = []; let index = 0; for (const bundle of bundles) { const files = []; for (const file of bundle.files) { const i = index++; await writeFile(`dist/catalog/${i}.bin`, file.bytes); source += `import data${i} from "./${i}.bin";\n`; files.push(`${JSON.stringify(file.path)}: data${i}`); } entries.push( `{ identity: ${JSON.stringify(bundle.identity)}, development: ${development}, files: {${files.join(",")}} }`, ); } source += `export default [${entries.join(",")}];\n`; await writeFile("dist/catalog/catalog.ts", source); console.log( `Pinned publisher catalog: latest immutable release, ${total} bytes${development ? " (development fixture)" : ""}`, );