import test from "node:test"; import assert from "node:assert/strict"; import { mkdtemp, rm, mkdir, writeFile, readFile } from "node:fs/promises"; import os from "node:os"; import path from "node:path"; import { createRequire } from "node:module"; import { EventEmitter } from "node:events"; import { createHash, generateKeyPairSync, sign } from "node:crypto"; import { execFile, spawn } from "node:child_process"; import { promisify } from "node:util"; import { fileURLToPath } from "node:url"; import { setTimeout as delay } from "node:timers/promises"; const require = createRequire(import.meta.url); const execFileAsync = promisify(execFile); const { UpdateService, verifyReleaseManifest, waitForUpdaterStarted, windowsUpdaterSpawnOptions, } = require("../src/main/update-service.cjs"); function createSignedReleaseFixture({ version, remoteSha, assetName, binary, }) { const { privateKey, publicKey } = generateKeyPairSync("ed25519"); const sha256 = createHash("sha256").update(binary).digest("hex"); const manifest = { schemaVersion: 1, product: "ForgeFlow", version, tag: `v${version}`, commit: remoteSha, buildId: "test-build", signature: { algorithm: "Ed25519", keyId: "SHA256:test" }, artifacts: [{ name: assetName, bytes: binary.length, sha256 }], }; const manifestBytes = Buffer.from(`${JSON.stringify(manifest, null, 2)}\n`); const signatureBytes = Buffer.from( `${sign(null, manifestBytes, privateKey).toString("base64")}\n`, ); return { publicKey, sha256, manifestBytes, signatureBytes }; } test("Windows updater uses a hidden non-detached PowerShell child", () => { assert.deepEqual(windowsUpdaterSpawnOptions("C:\\updates"), { detached: false, stdio: "ignore", windowsHide: true, cwd: "C:\\updates", }); }); test("update check pins version to an exact branch commit", async () => { const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-update-test-")); const saved = []; const store = { data: { gitea: { baseUrl: "https://gitea.example.test" }, updates: { owner: "Jens", repo: "ForgeFlow", branch: "main", autoCheck: true, }, }, async save() { saved.push(true); }, }; const calls = []; const gitea = { async getBranch(owner, repo, branch) { calls.push(["branch", owner, repo, branch]); return { commit: { id: "a".repeat(40) } }; }, async getRepositoryFile(input) { calls.push(["file", input]); return { decoded: JSON.stringify({ name: "forgeflow", version: "0.4.1" }), }; }, }; const service = new UpdateService({ store, gitea, diagnostics: null, appInfo: { version: "0.4.0", packaged: false }, sourcePath: temp, userDataPath: temp, }); const result = await service.check(); assert.equal(result.available, true); assert.equal(result.remoteSha, "a".repeat(40)); assert.equal(calls[1][1].ref, "a".repeat(40)); assert.equal(saved.length, 1); await rm(temp, { recursive: true, force: true }); }); test("update repository parts reject path injection", async () => { const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-update-test-")); const service = new UpdateService({ store: { data: { updates: { owner: "../Jens", repo: "ForgeFlow", branch: "main" }, }, save: async () => {}, }, gitea: {}, diagnostics: null, appInfo: { version: "0.4.0", packaged: false }, sourcePath: temp, userDataPath: temp, }); await assert.rejects(() => service.check(), /unsupported characters/); await rm(temp, { recursive: true, force: true }); }); test("source updater confirms an external STARTED marker before ForgeFlow may close", async () => { const temp = await mkdtemp( path.join(os.tmpdir(), "forgeflow-update-handshake-"), ); const source = path.join(temp, "source"); const scripts = path.join(source, "scripts"); const archive = path.join(temp, "update.zip"); await mkdir(scripts, { recursive: true }); await writeFile( path.join(scripts, "apply-source-update.ps1"), "# test helper", ); await writeFile(archive, "PK fake archive"); let capturedArgs = null; const spawnProcess = (_command, args) => { capturedArgs = args; const child = new EventEmitter(); child.pid = 4321; child.unref = () => {}; queueMicrotask(() => child.emit("spawn")); const statusIndex = args.indexOf("-StatusPath"); const statusPath = args[statusIndex + 1]; const updateIdIndex = args.indexOf("-UpdateId"); const updateId = args[updateIdIndex + 1]; setTimeout( () => writeFile( statusPath, JSON.stringify({ state: "started", expectedVersion: "0.5.3", updateId, }), ), 30, ); return child; }; const service = new UpdateService({ store: { data: { updates: {}, gitea: { baseUrl: "https://example.test" } }, save: async () => {}, }, gitea: {}, diagnostics: null, appInfo: { version: "0.5.2", packaged: false }, sourcePath: source, userDataPath: temp, platform: "win32", spawnProcess, powershellPath: "C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe", handshakeTimeoutMs: 1000, handshakePollMs: 10, }); service.staged = { archivePath: archive, remoteVersion: "0.5.3", remoteSha: "a".repeat(40), sha256: "b".repeat(64), }; const result = await service.apply(); assert.equal(result.confirmed, true); assert.ok(capturedArgs.includes("-StatusPath")); assert.ok(capturedArgs.includes("-UpdateId")); await rm(temp, { recursive: true, force: true }); }); test("source updater leaves ForgeFlow open when no STARTED marker arrives", async () => { const temp = await mkdtemp( path.join(os.tmpdir(), "forgeflow-update-timeout-"), ); const statusPath = path.join(temp, "status.json"); await writeFile(statusPath, JSON.stringify({ state: "launching" })); await assert.rejects( () => waitForUpdaterStarted(statusPath, { timeoutMs: 80, pollMs: 10, childState: { exited: false, error: null }, }), (error) => error.code === "UPDATE_HELPER_START_TIMEOUT", ); await rm(temp, { recursive: true, force: true }); }); test("completed source update result is returned once and acknowledged", async () => { const temp = await mkdtemp( path.join(os.tmpdir(), "forgeflow-update-result-"), ); const updates = path.join(temp, "updates"); await mkdir(updates, { recursive: true }); const statusPath = path.join(updates, "apply-test.status.json"); await writeFile( statusPath, JSON.stringify({ state: "success", expectedVersion: "0.5.3", installedVersion: "0.5.3", restartLaunched: false, message: "installed", logPath: "C:\\log.txt", updatedAt: new Date().toISOString(), }), ); const service = new UpdateService({ store: { data: { updates: {} }, save: async () => {} }, gitea: {}, diagnostics: null, appInfo: { version: "0.5.3", packaged: false }, sourcePath: temp, userDataPath: temp, }); const first = await service.consumeLatestResult(); const second = await service.consumeLatestResult(); assert.equal(first.state, "success"); assert.equal(first.restartLaunched, false); assert.equal(second, null); const persisted = JSON.parse(await readFile(statusPath, "utf8")); assert.ok(persisted.acknowledgedAt); await rm(temp, { recursive: true, force: true }); }); test("PowerShell update helper writes lifecycle status before waiting for ForgeFlow exit", async () => { const script = await readFile( new URL("../scripts/apply-source-update.ps1", import.meta.url), "utf8", ); assert.match(script, /\[string\]\$StatusPath/); assert.match(script, /Write-UpdateState -State "started"/); assert.match(script, /Write-UpdateState -State "success"/); assert.match(script, /Write-UpdateState -State "rolled-back"/); assert.match(script, /UTF8Encoding\(\$false\)/); assert.match(script, /WriteAllText/); }); test("PowerShell update helper starts with param and has no BOM or stray leading slash", async () => { const bytes = await readFile( new URL("../scripts/apply-source-update.ps1", import.meta.url), ); assert.notDeepEqual([...bytes.subarray(0, 3)], [0xef, 0xbb, 0xbf]); const text = bytes.toString("utf8"); assert.match(text.trimStart(), /^param\(/); assert.doesNotMatch(text.trimStart(), /^\\/); assert.match(text, /node_modules\\electron\\dist\\electron\.exe/); assert.match(text, /npm ci --no-audit --no-fund/); assert.match(text, /package-lock\.json/); assert.doesNotMatch(text, /Get-Command npm\.cmd/); assert.ok( text.indexOf('Write-UpdateState -State "success"') < text.indexOf("Start-ForgeFlow -WorkingDirectory $SourcePath"), ); }); test("release publisher verifies Gitea and bootstraps the installed updater service and helper", async () => { const script = await readFile( new URL("../Publish-ForgeFlow-Release.ps1", import.meta.url), "utf8", ); assert.match(script, /npm install --no-audit --no-fund/); assert.match(script, /package-lock\.json/); assert.match(script, /non-reproducible update/); assert.match(script, /npm run check/); assert.match(script, /npm run dist:win/); assert.match(script, /npm run release:binary/); assert.match(script, /SkipBinaryRelease/); assert.match(script, /ForgeFlow-Setup-\$version-win-x64\.exe/); assert.match(script, /ForgeFlow-Portable-\$version-win-x64\.exe/); assert.match(script, /git ls-remote origin/); assert.match(script, /publishedCommit -ne \$localCommit/); assert.match(script, /scripts\\apply-source-update\.ps1/); assert.match(script, /src\\main\\update-service\.cjs/); assert.match(script, /expectedUpdateId/); assert.match(script, /readLogTail/); assert.match(script, /HandshakeOnly/); assert.match(script, /handshakeResult\.state -ne "started"/); assert.match(script, /Windows-tested/); assert.match(script, /without changing its version/); assert.doesNotMatch(script, /Copy-Item[^\n]+package\.json/); }); test("one-click Windows release wrapper invokes the atomic publisher", async () => { const script = await readFile( new URL("../PUBLISH-AND-ENABLE-UPDATE.cmd", import.meta.url), "utf8", ); assert.match(script, /ExecutionPolicy Bypass/); assert.match(script, /Publish-ForgeFlow-Release\.ps1/); assert.match(script, /older ForgeFlow updater can now install/); assert.match(script, /exit \/b %forgeflowExitCode%/); }); test("missing binary release recovery script builds the exact Gitea commit and uploads all assets", async () => { const script = await readFile( new URL("../Publish-Missing-Binary-Release.ps1", import.meta.url), "utf8", ); assert.match(script.trimStart(), /^param\(/); assert.match(script, /git clone --branch \$Branch --single-branch/); assert.match(script, /git -C \$clone ls-remote origin/); assert.match(script, /npm ci --no-audit --no-fund/); assert.match(script, /npm run check/); assert.match(script, /npm run dist:win/); assert.match(script, /npm run release:binary/); assert.match(script, /FORGEFLOW_USER_DATA/); assert.match(script, /ForgeFlow-Setup-\$version-win-x64\.exe/); assert.match(script, /ForgeFlow-Portable-\$version-win-x64\.exe/); }); test("binary publisher derives repository coordinates from ForgeFlow settings", async () => { const script = await readFile( new URL("../scripts/publish-binary-release.cjs", import.meta.url), "utf8", ); assert.match(script, /config\.updates\?\.owner/); assert.match(script, /config\.updates\?\.repo/); assert.match(script, /config\.updates\?\.branch/); assert.match(script, /encodeURIComponent\(owner\)/); assert.match(script, /encodeURIComponent\(repo\)/); assert.doesNotMatch(script, /\/repos\/Jens\/ForgeFlow\/releases/); }); test("packaged updater passes Gitea browser download URLs to the asset downloader", async () => { const source = await readFile( new URL("../src/main/update-service.cjs", import.meta.url), "utf8", ); assert.match(source, /downloadUrl: asset\.browser_download_url/); assert.match(source, /downloadUrl: checksumAsset\.browser_download_url/); assert.match(source, /downloadUrl: manifestAsset\.browser_download_url/); assert.match(source, /downloadUrl: signatureAsset\.browser_download_url/); assert.match(source, /RELEASE_ASSET_METADATA_RECEIVED/); }); test("PowerShell helper replaces an existing launching status with a Windows-safe file API", async () => { const script = await readFile( new URL("../scripts/apply-source-update.ps1", import.meta.url), "utf8", ); assert.match( script, /System\.IO\.File\]::Replace\(\$temporary, \$StatusPath, \$backup\)/, ); assert.match( script, /System\.IO\.File\]::Copy\(\$temporary, \$StatusPath, \$true\)/, ); assert.doesNotMatch( script, /Move-Item -LiteralPath \$temporary -Destination \$StatusPath -Force/, ); assert.match(script, /\[switch\]\$HandshakeOnly/); assert.match(script, /Handshake-only verification completed successfully/); }); test("binary helper confirms startup through real Windows PowerShell", { skip: process.platform !== "win32" }, async () => { const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-binary-handshake-")); const statusPath = path.join(temp, "status.json"); const logPath = path.join(temp, "helper.log"); await writeFile(statusPath, JSON.stringify({ state: "launching", updateId: "binary-handshake" })); const powershell = path.join(process.env.SystemRoot || process.env.WINDIR, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"); const scriptPath = fileURLToPath(new URL("../scripts/apply-binary-update.ps1", import.meta.url)); const { stdout, stderr } = await execFileAsync(powershell, [ "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath, "-BinaryPath", path.join(temp, "unused.exe"), "-ExpectedSha256", "0".repeat(64), "-ExpectedVersion", "9.9.9", "-CurrentExecutable", path.join(temp, "unused-current.exe"), "-Portable", "False", "-ParentPid", "999999", "-LogPath", logPath, "-StatusPath", statusPath, "-UpdateId", "binary-handshake", "-HandshakeOnly" ], { windowsHide: true }); assert.equal(stdout, ""); assert.equal(stderr, ""); const status = JSON.parse(await readFile(statusPath, "utf8")); assert.equal(status.updateId, "binary-handshake"); assert.equal(status.state, "started"); assert.match(await readFile(logPath, "utf8"), /Handshake-only verification completed successfully/); await rm(temp, { recursive: true, force: true }); }); test("binary helper confirms startup through the production Node spawn options", { skip: process.platform !== "win32" }, async () => { const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-binary-node-spawn-")); const statusPath = path.join(temp, "status.json"); const logPath = path.join(temp, "helper.log"); const powershell = path.join(process.env.SystemRoot || process.env.WINDIR, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"); const scriptPath = fileURLToPath(new URL("../scripts/apply-binary-update.ps1", import.meta.url)); const updateId = "binary-node-spawn"; await writeFile(statusPath, JSON.stringify({ state: "launching", updateId })); const child = spawn(powershell, [ "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath, "-BinaryPath", path.join(temp, "unused.exe"), "-ExpectedSha256", "0".repeat(64), "-ExpectedVersion", "9.9.9", "-CurrentExecutable", path.join(temp, "unused-current.exe"), "-Portable", "False", "-ParentPid", String(process.pid), "-LogPath", logPath, "-StatusPath", statusPath, "-UpdateId", updateId, "-HandshakeOnly", ], windowsUpdaterSpawnOptions(temp)); const childState = { exited: false, code: null, error: null }; child.once("error", (error) => { childState.error = error; }); child.once("exit", (code) => { childState.exited = true; childState.code = code; }); const status = await waitForUpdaterStarted(statusPath, { timeoutMs: 5000, pollMs: 25, childState, expectedUpdateId: updateId, logPath, }); assert.equal(status.state, "started"); let log = ""; for (let attempt = 0; attempt < 40 && !log.includes("Handshake-only verification completed successfully"); attempt += 1) { await delay(25); log = await readFile(logPath, "utf8").catch(() => ""); } assert.match(log, /Handshake-only verification completed successfully/); if (child.exitCode === null) { await new Promise((resolve, reject) => { child.once("exit", resolve); child.once("error", reject); }); } await rm(temp, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); }); test("binary helper verifies SHA-256 without PowerShell module autoloading", { skip: process.platform !== "win32" }, async () => { const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-binary-dotnet-sha-")); const binaryPath = path.join(temp, "update.exe"); const currentPath = path.join(temp, "current.exe"); const statusPath = path.join(temp, "status.json"); const logPath = path.join(temp, "helper.log"); const bytes = Buffer.from("verified update bytes"); await writeFile(binaryPath, bytes); await writeFile(currentPath, "current"); const expectedSha256 = createHash("sha256").update(bytes).digest("hex"); const powershell = path.join(process.env.SystemRoot || process.env.WINDIR, "System32", "WindowsPowerShell", "v1.0", "powershell.exe"); const scriptPath = fileURLToPath(new URL("../scripts/apply-binary-update.ps1", import.meta.url)); const { stderr } = await execFileAsync(powershell, [ "-NoLogo", "-NoProfile", "-NonInteractive", "-ExecutionPolicy", "Bypass", "-File", scriptPath, "-BinaryPath", binaryPath, "-ExpectedSha256", expectedSha256, "-ExpectedVersion", "9.9.9", "-CurrentExecutable", currentPath, "-Portable", "False", "-ParentPid", String(process.pid), "-LogPath", logPath, "-StatusPath", statusPath, "-UpdateId", "dotnet-sha", "-VerifyOnly", ], { windowsHide: true, env: { ...process.env, PSModulePath: "" } }); assert.equal(stderr, ""); assert.match(await readFile(logPath, "utf8"), /Verification-only SHA-256 check completed successfully/); await rm(temp, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); }); test("early helper exit reports the helper log instead of only an exit code", async () => { const temp = await mkdtemp( path.join(os.tmpdir(), "forgeflow-update-log-tail-"), ); const statusPath = path.join(temp, "status.json"); const logPath = path.join(temp, "apply.log"); await writeFile( statusPath, JSON.stringify({ state: "launching", updateId: "request-1" }), ); await writeFile(logPath, "first line\nactual helper failure\n"); await assert.rejects( () => waitForUpdaterStarted(statusPath, { timeoutMs: 100, pollMs: 5, childState: { exited: true, code: 0, error: null }, expectedUpdateId: "request-1", logPath, }), (error) => error.code === "UPDATE_HELPER_EXITED_EARLY" && /actual helper failure/.test(error.message), ); await rm(temp, { recursive: true, force: true }); }); test("updater handshake rejects a stale status from another update request", async () => { const temp = await mkdtemp(path.join(os.tmpdir(), "forgeflow-update-id-")); const statusPath = path.join(temp, "status.json"); await writeFile( statusPath, JSON.stringify({ state: "started", updateId: "old-request" }), ); await assert.rejects( () => waitForUpdaterStarted(statusPath, { timeoutMs: 50, pollMs: 5, childState: { exited: false, code: null, error: null }, expectedUpdateId: "new-request", }), (error) => error.code === "UPDATE_HELPER_START_TIMEOUT", ); await rm(temp, { recursive: true, force: true }); }); test("packaged updater downloads only a publisher-signed Windows asset", async () => { const temp = await mkdtemp( path.join(os.tmpdir(), "forgeflow-binary-update-"), ); const binary = Buffer.alloc(1_100_000, 0x5a); binary[0] = 0x4d; binary[1] = 0x5a; const assetName = "ForgeFlow-Setup-0.8.2-win-x64.exe"; const remoteSha = "a".repeat(40); const signed = createSignedReleaseFixture({ version: "0.8.2", remoteSha, assetName, binary, }); const manifestName = "ForgeFlow-0.8.2-release-manifest.json"; const gitea = { async getReleaseByTag(_owner, _repo, tag) { if (tag !== "v0.8.2") return null; return { id: 82, tag_name: tag, draft: false, prerelease: false, assets: [ { id: 41, name: assetName, browser_download_url: "http://wrong-origin.test/setup", }, { name: `${assetName}.sha256`, id: 42, browser_download_url: "http://wrong-origin.test/checksum", }, { name: manifestName, id: 43, browser_download_url: "http://wrong-origin.test/manifest", }, { name: `${manifestName}.sig`, id: 44, browser_download_url: "http://wrong-origin.test/signature", }, ], }; }, async downloadReleaseAsset(_owner, _repo, releaseId, assetId, options) { assert.equal(releaseId, 82); const downloads = { 41: ["http://wrong-origin.test/setup", binary], 42: [ "http://wrong-origin.test/checksum", Buffer.from(`${signed.sha256} ${assetName}\n`), ], 43: ["http://wrong-origin.test/manifest", signed.manifestBytes], 44: ["http://wrong-origin.test/signature", signed.signatureBytes], }; assert.equal(options.downloadUrl, downloads[assetId][0]); return downloads[assetId][1]; }, }; const service = new UpdateService({ store: { data: { gitea: { baseUrl: "https://gitea.test" } }, save: async () => {}, }, gitea, diagnostics: null, appInfo: { version: "0.8.1", packaged: true, executablePath: "C:\\ForgeFlow\\ForgeFlow.exe", }, sourcePath: temp, userDataPath: temp, platform: "win32", updatePublicKey: signed.publicKey, }); const result = await service.downloadPackaged({ owner: "Jens", repo: "ForgeFlow", remoteVersion: "0.8.2", remoteSha, }); assert.equal(result.downloaded, true); assert.equal(result.sha256, signed.sha256); assert.equal(result.publisherKeyId, "SHA256:test"); assert.equal(result.portable, false); assert.equal((await readFile(result.binaryPath)).length, binary.length); await rm(temp, { recursive: true, force: true }); }); test("packaged updater rejects a binary whose checksum does not match", async () => { const temp = await mkdtemp( path.join(os.tmpdir(), "forgeflow-binary-mismatch-"), ); const binary = Buffer.alloc(1_100_000, 0x5a); binary[0] = 0x4d; binary[1] = 0x5a; const assetName = "ForgeFlow-Portable-0.8.2-win-x64.exe"; const remoteSha = "b".repeat(40); const signed = createSignedReleaseFixture({ version: "0.8.2", remoteSha, assetName, binary, }); const manifestName = "ForgeFlow-0.8.2-release-manifest.json"; const service = new UpdateService({ store: { data: { gitea: {} }, save: async () => {} }, gitea: { async getReleaseByTag() { return { id: 83, tag_name: "v0.8.2", assets: [ { id: 51, name: assetName, browser_download_url: "http://wrong-origin.test/portable", }, { id: 52, name: `${assetName}.sha256`, browser_download_url: "http://wrong-origin.test/checksum", }, { id: 53, name: manifestName }, { id: 54, name: `${manifestName}.sig` }, ], }; }, async downloadReleaseAsset(_owner, _repo, releaseId, assetId) { assert.equal(releaseId, 83); if (assetId === 51) return binary; if (assetId === 52) return Buffer.from(`${"0".repeat(64)} ${assetName}`); if (assetId === 53) return signed.manifestBytes; return signed.signatureBytes; }, }, diagnostics: null, appInfo: { version: "0.8.1", packaged: true, portableExecutablePath: "C:\\ForgeFlow-Portable.exe", }, sourcePath: temp, userDataPath: temp, platform: "win32", updatePublicKey: signed.publicKey, }); await assert.rejects( () => service.downloadPackaged({ owner: "Jens", repo: "ForgeFlow", remoteVersion: "0.8.2", remoteSha, }), /does not match the signed publisher manifest/, ); await rm(temp, { recursive: true, force: true }); }); test("release manifest verification rejects a different publisher key", () => { const binary = Buffer.alloc(1_100_000, 0x5a); const assetName = "ForgeFlow-Setup-0.8.2-win-x64.exe"; const fixture = createSignedReleaseFixture({ version: "0.8.2", remoteSha: "c".repeat(40), assetName, binary, }); const otherKey = generateKeyPairSync("ed25519").publicKey; assert.throws( () => verifyReleaseManifest({ manifestBytes: fixture.manifestBytes, signatureBytes: fixture.signatureBytes, publicKey: otherKey, update: { remoteVersion: "0.8.2", remoteSha: "c".repeat(40), }, assetName, }), (error) => error.code === "RELEASE_SIGNATURE_INVALID", ); }); test("Windows release pipeline emits signed provenance, manifest and SBOM evidence", async () => { const [pkgSource, signatureSource, checksumSource, manifestSigner] = await Promise.all([ readFile(new URL("../package.json", import.meta.url), "utf8"), readFile(new URL("../scripts/verify-release-signatures.mjs", import.meta.url), "utf8"), readFile(new URL("../scripts/write-release-checksums.mjs", import.meta.url), "utf8"), readFile(new URL("../scripts/sign-release-manifest.mjs", import.meta.url), "utf8"), ]); assert.match(pkgSource, /verify-release-signatures\.mjs/); assert.match(signatureSource, /FORGEFLOW_SIGNED_RELEASE/); assert.match(signatureSource, /FORGEFLOW_EXPECTED_PUBLISHER/); assert.match(signatureSource, /TimestampSubject/); assert.match(signatureSource, /Signed release verification failed/); assert.match(signatureSource, /Authenticode inspection unavailable/); assert.match(checksumSource, /provenance\.json/); assert.match(checksumSource, /sbom\.cdx\.json/); assert.match(checksumSource, /CycloneDX/); assert.match(checksumSource, /publisherManifestSignature/); assert.match(manifestSigner, /Ed25519/); assert.match(manifestSigner, /release-manifest\.json/); assert.match(pkgSource, /sign-release-manifest\.mjs/); const publisher = await readFile(new URL("../scripts/publish-binary-release.cjs", import.meta.url), "utf8"); assert.match(publisher, /draft: true/); assert.match(publisher, /requiredAssets/); assert.match(publisher, /Release remains draft because required assets are missing/); assert.match(publisher, /sbom\.cdx\.json/); }); test("the supported Windows build uses free offline Ed25519 publisher signing", async () => { const [pkg, keySetup, manifestSigner, publicKey] = await Promise.all([ readFile(new URL("../package.json", import.meta.url), "utf8"), readFile(new URL("../scripts/setup-update-signing-key.mjs", import.meta.url), "utf8"), readFile(new URL("../scripts/sign-release-manifest.mjs", import.meta.url), "utf8"), readFile(new URL("../build/update-signing-public.pem", import.meta.url), "utf8"), ]); assert.doesNotMatch(pkg, /dist:win:signed/); assert.match(pkg, /dist:win/); assert.match(pkg, /signing:setup/); assert.match(keySetup, /release-signing-private\.pem/); assert.match(manifestSigner, /sign\(null, manifestBytes, privateKey\)/); assert.match(publicKey, /BEGIN PUBLIC KEY/); assert.doesNotMatch(publicKey, /PRIVATE KEY/); }); test("binary update helper verifies, waits, applies and records restart state", async () => { const helper = await readFile( new URL("../scripts/apply-binary-update.ps1", import.meta.url), "utf8", ); for (const marker of [ "Security.Cryptography.SHA256", "Wait-Process", 'Write-UpdateState -State "started"', 'Write-UpdateState -State "waiting-for-exit"', 'Write-UpdateState -State "applying"', 'Write-UpdateState -State "success"', 'Start-Process -FilePath $BinaryPath -ArgumentList "/S"', "Copy-Item -LiteralPath $BinaryPath -Destination $CurrentExecutable", ]) { assert.ok( helper.includes(marker), `missing binary updater marker: ${marker}`, ); } assert.doesNotMatch(helper, /Get-FileHash/); });