import { spawnSync } from "node:child_process"; import { createHash } from "node:crypto"; import { copyFileSync, existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs"; import { basename, join, resolve } from "node:path"; import { buildAndValidatePortablePackage } from "./portable-package.mjs"; const fixedPort = 43121; const versionPattern = /^\d+\.\d+\.\d+\.\d+$/; const sha256Pattern = /^[A-F0-9]{64}$/; function powershellJson(script) { const result = spawnSync( "powershell.exe", ["-NoProfile", "-NonInteractive", "-Command", script], { encoding: "utf8", windowsHide: true }, ); if (result.status !== 0) { throw new Error(`Windows environment probe failed with exit code ${result.status ?? 1}.`); } return JSON.parse(result.stdout.trim()); } export function readCandidateEnvironment() { if (process.platform !== "win32" || process.arch !== "x64") { throw new Error("Release candidates must be recorded on Windows x64."); } return powershellJson(String.raw` $ErrorActionPreference = 'Stop' function Find-Browser([string] $brand, [string] $fileName, [string[]] $candidates) { $path = $candidates | Where-Object { $_ -and (Test-Path -LiteralPath $_) } | Select-Object -First 1 if (-not $path) { throw "Required browser is not installed: $brand" } $item = Get-Item -LiteralPath $path if ($item.VersionInfo.ProductName -ne $brand) { throw "Installed executable identity mismatch: $brand" } $version = $item.VersionInfo.ProductVersion [pscustomobject]@{ brand = $brand executable_sha256 = (Get-FileHash -LiteralPath $path -Algorithm SHA256).Hash file_name = $fileName full_version = $version major = [int]($version.Split('.')[0]) product_name = $item.VersionInfo.ProductName source = 'installed_executable' } } $chromeRegistry = @( (Get-ItemProperty 'Registry::HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe' -ErrorAction SilentlyContinue).'(default)', (Get-ItemProperty 'Registry::HKEY_CURRENT_USER\SOFTWARE\Microsoft\Windows\CurrentVersion\App Paths\chrome.exe' -ErrorAction SilentlyContinue).'(default)' ) $chrome = Find-Browser 'Google Chrome' 'chrome.exe' @( (Join-Path $env:LOCALAPPDATA 'Google\Chrome\Application\chrome.exe'), 'C:\Program Files\Google\Chrome\Application\chrome.exe', 'C:\Program Files (x86)\Google\Chrome\Application\chrome.exe', $chromeRegistry[0], $chromeRegistry[1] ) $edge = Find-Browser 'Microsoft Edge' 'msedge.exe' @( 'C:\Program Files (x86)\Microsoft\Edge\Application\msedge.exe', 'C:\Program Files\Microsoft\Edge\Application\msedge.exe' ) $windows = Get-ItemProperty -LiteralPath 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion' [pscustomobject]@{ browsers = @($chrome, $edge) windows = [pscustomobject]@{ arch = 'x64' build = "$($windows.CurrentBuildNumber).$($windows.UBR)" display_version = $windows.DisplayVersion } } | ConvertTo-Json -Depth 5 -Compress `); } export function validateReleaseCandidateRecord(record) { const errors = []; if (record?.schema_version !== "1.0") errors.push("schema_version"); if (record?.status !== "candidate_unvalidated") errors.push("status"); if (record?.final_release !== false) errors.push("final_release"); if (record?.fixed_port !== fixedPort) errors.push("fixed_port"); if (!/^[a-f0-9]{40}$/.test(record?.build_commit ?? "")) errors.push("build_commit"); if (!/^\d+\.\d+$/.test(record?.windows?.build ?? "")) errors.push("windows.build"); if (!Number.isFinite(Date.parse(record?.recorded_at ?? ""))) errors.push("recorded_at"); if (!sha256Pattern.test(record?.candidate_package?.sha256 ?? "")) errors.push("candidate_package.sha256"); if (record?.candidate_package?.fixed_port !== fixedPort) errors.push("candidate_package.fixed_port"); if (record?.candidate_package?.release_status !== "candidate_unvalidated") { errors.push("candidate_package.release_status"); } if (!Array.isArray(record?.browsers) || record.browsers.length !== 2) { errors.push("browsers"); } else { const brands = record.browsers.map(({ brand }) => brand).sort(); if (brands.join("|") !== "Google Chrome|Microsoft Edge") errors.push("browsers.brand"); for (const browser of record.browsers) { if (!versionPattern.test(browser.full_version ?? "")) errors.push(`${browser.brand}.full_version`); if (browser.major !== Number.parseInt(browser.full_version?.split(".")[0] ?? "", 10)) { errors.push(`${browser.brand}.major`); } if (!sha256Pattern.test(browser.executable_sha256 ?? "")) errors.push(`${browser.brand}.sha256`); if (browser.source !== "installed_executable") errors.push(`${browser.brand}.source`); if ("path" in browser || "executable_path" in browser) errors.push(`${browser.brand}.path`); } } const serialized = JSON.stringify(record); if (/[A-Za-z]:\\Users\\/i.test(serialized)) errors.push("absolute_user_path"); if (errors.length > 0) throw new Error(`Invalid release candidate record: ${[...new Set(errors)].join(", ")}`); return record; } function sha256(path) { return createHash("sha256").update(readFileSync(path)).digest("hex").toUpperCase(); } export async function createReleaseCandidate({ commit, evidenceRoot, outputRoot, recordedAt = new Date().toISOString() }) { const environment = readCandidateEnvironment(); const packageResult = await buildAndValidatePortablePackage({ outputRoot }); const packageName = packageResult.packageManifest.package_name; const packageDirectory = join(outputRoot, packageName); const zipSource = join(outputRoot, `${packageName}.zip`); const shaSource = `${zipSource}.sha256`; const zipSha256 = sha256(zipSource); if (zipSha256 !== packageResult.packageManifest.zip_sha256) { throw new Error("Candidate ZIP hash does not match the package manifest."); } if (!readFileSync(shaSource, "utf8").startsWith(`${zipSha256} ${basename(zipSource)}`)) { throw new Error("Candidate ZIP hash file does not match the package bytes."); } const candidatePackage = resolve(evidenceRoot, "candidate-package"); mkdirSync(candidatePackage, { recursive: true }); for (const source of [zipSource, shaSource, join(packageDirectory, "START-HERE.txt")]) { if (!existsSync(source)) throw new Error(`Candidate package output is missing: ${basename(source)}`); copyFileSync(source, join(candidatePackage, basename(source))); } writeFileSync( join(candidatePackage, "package-manifest.json"), `${JSON.stringify(packageResult.packageManifest, null, 2)}\n`, ); writeFileSync( join(candidatePackage, "package-scan.json"), `${JSON.stringify(packageResult.packageScan, null, 2)}\n`, ); writeFileSync( join(candidatePackage, "process-tree.json"), `${JSON.stringify(packageResult.processTree, null, 2)}\n`, ); const record = validateReleaseCandidateRecord({ app_version: packageResult.packageManifest.app_version, browsers: environment.browsers.map((browser) => ({ brand: browser.brand, executable_sha256: browser.executable_sha256, file_name: browser.file_name, full_version: browser.full_version, major: browser.major, source: browser.source, })), build_commit: commit, candidate_package: { file_name: basename(zipSource), fixed_port: packageResult.packageManifest.fixed_port, release_status: packageResult.packageManifest.release_status, sha256: zipSha256, size_bytes: statSync(zipSource).size, }, final_release: false, fixed_port: fixedPort, recorded_at: recordedAt, schema_version: "1.0", status: "candidate_unvalidated", windows: environment.windows, }); writeFileSync(resolve(evidenceRoot, "release-candidate.json"), `${JSON.stringify(record, null, 2)}\n`); return { packageResult, record }; }