import { readFile, writeFile } from 'node:fs/promises' import { join } from 'node:path' import { createHash } from 'node:crypto' import { parse } from 'yaml' import { validateFeedUrl } from '../build/config.mjs' /** Hash an artifact for local tamper/corruption checks and upload verification. */ export function digest(data, algorithm = 'sha256', encoding = 'hex') { return createHash(algorithm).update(data).digest(encoding) } /** Read only the x64 stable installer referenced by electron-builder metadata. */ export async function readReleaseArtifacts(directory) { const metadata = await readFile(join(directory, 'latest.yml')) const info = parse(metadata.toString('utf8')) if (!/^\d+\.\d+\.\d+$/.test(info?.version) || info.files?.length !== 1) { throw new Error('Expected one Windows x64 installer for a stable semantic version.') } const installer = `Capsule-${info.version}-x64-Setup.exe` if (info.files[0].url !== installer || (info.path && info.path !== installer)) { throw new Error('Release metadata must reference the expected local installer filename.') } const binary = await readFile(join(directory, installer)) if (info.files[0].sha512 !== digest(binary, 'sha512', 'base64') || info.files[0].size !== binary.length) { throw new Error('Installer does not match latest.yml checksum or size.') } const blockmap = await readFile(join(directory, `${installer}.blockmap`)) return { version: info.version, files: [ { name: installer, data: binary }, { name: `${installer}.blockmap`, data: blockmap }, // Publish the pointer only after both immutable artifacts are available. { name: 'latest.yml', data: metadata } ] } } /** Mark a successful signed build with hashes of its exact publishable files. */ export async function writeReleaseManifest(directory, feedUrl) { const release = await readReleaseArtifacts(directory) const manifest = { version: release.version, feedUrl: validateFeedUrl(feedUrl), files: release.files.map(({ name, data }) => ({ name, sha256: digest(data) })) } await writeFile(join(directory, 'release-ready.json'), JSON.stringify(manifest, null, 2)) } /** Refuse local builds, missing files, stale metadata, and changed release assets. */ export async function verifyRelease(directory) { const manifest = JSON.parse(await readFile(join(directory, 'release-ready.json'), 'utf8')) validateFeedUrl(manifest.feedUrl) const release = await readReleaseArtifacts(directory) if (release.version !== manifest.version || manifest.files?.length !== release.files.length || release.files.some(({ name, data }, index) => manifest.files[index].name !== name || manifest.files[index].sha256 !== digest(data))) { throw new Error('Release artifacts have changed since the signed build. Rebuild before publishing.') } return { ...release, feedUrl: manifest.feedUrl } }