#!/usr/bin/env node import { lstat, mkdir, readFile, rename, rm, writeFile, } from "node:fs/promises"; import { dirname, resolve } from "node:path"; import { pathToFileURL } from "node:url"; import { canonicalValueBytes } from "../src/security/canonical-digest.ts"; import { createDeterministicTar, enumerateRegularFiles, packageIndexFor, parseDeterministicTar, validatePackageFiles, verifySignedRelease, verifySignedReleaseForTest, writeArchiveFiles, } from "./deploy/lib/release-common.mjs"; function parseArgs(args) { const allowed = new Set([ "dist", "signed-manifest", "sbom", "output", "authority-root", "protected-state", ]); const values = {}; for (let index = 0; index < args.length; index += 2) { const token = args[index]; const value = args[index + 1]; if ( typeof token !== "string" || !token.startsWith("--") || typeof value !== "string" || value.length === 0 ) throw new Error( "Release package arguments must be complete name/value pairs.", ); const name = token.slice(2); if (!allowed.has(name) || Object.hasOwn(values, name)) throw new Error( `Unknown or duplicate release package argument: ${token}`, ); values[name] = value; } if ([...allowed].some((name) => !Object.hasOwn(values, name))) throw new Error("Release package arguments are incomplete."); let protectedState; try { protectedState = JSON.parse(values["protected-state"]); } catch { throw new Error("Protected state must be valid JSON."); } return { distDir: resolve(values.dist), signedManifestPath: resolve(values["signed-manifest"]), sbomPath: resolve(values.sbom), outputPath: resolve(values.output), authorityRoot: resolve(values["authority-root"]), protectedState, }; } async function buildReleasePackageWith(input, verify) { const outputInfo = await lstat(input.outputPath).catch((error) => error?.code === "ENOENT" ? null : Promise.reject(error), ); if (outputInfo !== null) throw new Error("Release archive output already exists."); const verified = await verify(input); const { admission, manifest, signedReleaseBytes } = verified; const sourceFiles = await enumerateRegularFiles(input.distDir); const actualPaths = sourceFiles.map((file) => file.publicPath); const expectedPaths = [...manifest.deployFiles].sort((left, right) => Buffer.compare(Buffer.from(left), Buffer.from(right)), ); if ( actualPaths.length !== expectedPaths.length || actualPaths.some((path, index) => path !== expectedPaths[index]) ) { throw new Error("dist files must exactly match the signed deploy closure."); } const sourceByPath = new Map( sourceFiles.map((file) => [file.publicPath, file.path]), ); const artifactFiles = []; for (const artifact of manifest.artifacts) { if (artifact.path.startsWith(".release/")) throw new Error("Signed deploy closure uses the reserved .release path."); const source = sourceByPath.get(artifact.path); if (source === undefined) throw new Error(`Signed artifact is missing: ${artifact.path}`); const bytes = await readFile(source); const digest = ( await import("../src/security/canonical-digest.ts") ).byteDigest(bytes); if (digest !== artifact.sha256 || bytes.length !== artifact.bytes) throw new Error(`Artifact hash or size mismatch: ${artifact.path}`); artifactFiles.push({ path: artifact.path, bytes }); } const sbomBytes = await readFile(input.sbomPath); const index = packageIndexFor({ admission, manifest, signedBytes: signedReleaseBytes, sbomBytes, }); const archiveBytes = createDeterministicTar([ ...artifactFiles, { path: ".release/package-index.json", bytes: canonicalValueBytes(index) }, { path: ".release/sbom.cdx.json", bytes: sbomBytes }, { path: ".release/signed-release-manifest.json", bytes: signedReleaseBytes, }, ]); const temporary = `${input.outputPath}.partial-${process.pid}`; await mkdir(dirname(input.outputPath), { recursive: true }); await writeFile(temporary, archiveBytes, { flag: "wx", mode: 0o400 }); const verifyRoot = `${input.outputPath}.verify-${process.pid}`; try { const parsed = parseDeterministicTar(await readFile(temporary)); validatePackageFiles(parsed); await writeArchiveFiles(parsed, verifyRoot); const extracted = await enumerateRegularFiles(verifyRoot); if (extracted.length !== parsed.length) throw new Error("Extracted release archive closure mismatch."); await rename(temporary, input.outputPath); } finally { await rm(verifyRoot, { recursive: true, force: true }); await rm(temporary, { force: true }); } return { releaseId: index.releaseId, manifestDigest: admission.manifestDigest, archivePath: input.outputPath, }; } export async function buildReleasePackage(input) { const allowed = [ "distDir", "signedManifestPath", "sbomPath", "outputPath", "authorityRoot", "protectedState", ]; if (Object.keys(input).sort().join("\0") !== allowed.sort().join("\0")) throw new Error( "Production package builder rejects caller verifier, policy, or time inputs.", ); return buildReleasePackageWith(input, (request) => verifySignedRelease({ signedManifestPath: request.signedManifestPath, authorityRoot: request.authorityRoot, protectedState: request.protectedState, }), ); } export async function buildReleasePackageForTest(input) { return buildReleasePackageWith(input, (request) => verifySignedReleaseForTest({ signedManifestPath: request.signedManifestPath, trustPolicyPath: request.trustPolicyPath, verifySignedReleaseEnvelope: request.verifySignedReleaseEnvelope, protectedState: request.protectedState, now: request.now, }), ); } async function main() { const result = await buildReleasePackage(parseArgs(process.argv.slice(2))); process.stdout.write(`${JSON.stringify(result)}\n`); } if ( process.argv[1] && import.meta.url === pathToFileURL(process.argv[1]).href ) { main().catch((error) => { process.stderr.write( `Release packaging failed: ${error instanceof Error ? error.message : "unknown error"}\n`, ); process.exitCode = 1; }); }