import { createHash, createPrivateKey, createPublicKey, sign, verify } from "node:crypto"; import { readFile, stat, writeFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { fileURLToPath } from "node:url"; const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), ".."); const pkg = JSON.parse(await readFile(path.join(root, "package.json"), "utf8")); const privatePath = path.resolve( process.env.FORGEFLOW_UPDATE_SIGNING_PRIVATE_KEY || path.join(process.env.APPDATA || path.join(os.homedir(), "AppData", "Roaming"), "forgeflow", "release-signing-private.pem"), ); const publicPath = path.join(root, "build", "update-signing-public.pem"); const privateKey = createPrivateKey(await readFile(privatePath).catch((error) => { if (error.code === "ENOENT") throw new Error(`ForgeFlow update signing key is missing. Run npm run signing:setup once. Expected: ${privatePath}`); throw error; })); const publicKey = createPublicKey(await readFile(publicPath)); if (!publicKey.equals(createPublicKey(privateKey))) throw new Error("The release private key does not match the public key embedded in ForgeFlow."); const provenance = JSON.parse(await readFile(path.join(root, "dist", `ForgeFlow-${pkg.version}-provenance.json`), "utf8")); const artifacts = []; for (const kind of ["Setup", "Portable"]) { const name = `ForgeFlow-${kind}-${pkg.version}-win-x64.exe`; const filePath = path.join(root, "dist", name); const bytes = await readFile(filePath); artifacts.push({ name, bytes: (await stat(filePath)).size, sha256: createHash("sha256").update(bytes).digest("hex") }); } const keyId = createHash("sha256").update(publicKey.export({ type: "spki", format: "der" })).digest("hex"); const manifest = { schemaVersion: 1, product: "ForgeFlow", version: pkg.version, tag: `v${pkg.version}`, commit: provenance.commit, buildId: provenance.buildId, signature: { algorithm: "Ed25519", keyId: `SHA256:${keyId}` }, artifacts, }; const manifestBytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`, "utf8"); const signature = sign(null, manifestBytes, privateKey); if (!verify(null, manifestBytes, publicKey, signature)) throw new Error("The generated release signature did not verify."); const manifestName = `ForgeFlow-${pkg.version}-release-manifest.json`; await writeFile(path.join(root, "dist", manifestName), manifestBytes, { mode: 0o644 }); await writeFile(path.join(root, "dist", `${manifestName}.sig`), `${signature.toString("base64")}\n`, { mode: 0o644 }); console.log(`${manifestName}: signed with SHA256:${keyId}`);