From 58153679dddb57dd718bf629a1c706b44f55e37c Mon Sep 17 00:00:00 2001 From: webadderall <131426131+webadderall@users.noreply.github.com> Date: Sat, 28 Mar 2026 16:17:29 +1100 Subject: [PATCH] chore(windows): track bundled helper manifests --- .../bin/win32-x64/helpers-manifest.json | 21 ++ scripts/build-cursor-monitor.mjs | 25 +++ scripts/build-windows-capture.mjs | 25 +++ scripts/native-helper-manifest.mjs | 189 ++++++++++++++++++ 4 files changed, 260 insertions(+) create mode 100644 electron/native/bin/win32-x64/helpers-manifest.json create mode 100644 scripts/native-helper-manifest.mjs diff --git a/electron/native/bin/win32-x64/helpers-manifest.json b/electron/native/bin/win32-x64/helpers-manifest.json new file mode 100644 index 00000000..234719bf --- /dev/null +++ b/electron/native/bin/win32-x64/helpers-manifest.json @@ -0,0 +1,21 @@ +{ + "version": 1, + "platform": "win32", + "arch": "x64", + "helpers": { + "wgc-capture": { + "binaryName": "wgc-capture.exe", + "binarySha256": "72d8f0703a5360a0e911a95bf8839354c877df5fa51294af75ded83a17c0c32e", + "sourceDir": "electron/native/wgc-capture", + "sourceFingerprint": "f30dee6b4956a5b57f4fc4d8d586f3470c793c8f7e3242f4160394debbb3f3be", + "updatedAt": "2026-03-28T05:17:10.449Z" + }, + "cursor-monitor": { + "binaryName": "cursor-monitor.exe", + "binarySha256": "916ed064c12a3b861d7721df8b9209d10ec0fdb8078564a2ba3ea8cf4f4354c4", + "sourceDir": "electron/native/cursor-monitor", + "sourceFingerprint": "6ad1b8b50bb336f2a48937b06f5ec56d90b6ab4a3e56a4bca278cf67a5d3e52e", + "updatedAt": "2026-03-28T05:17:17.697Z" + } + } +} diff --git a/scripts/build-cursor-monitor.mjs b/scripts/build-cursor-monitor.mjs index e6393872..36917ddf 100644 --- a/scripts/build-cursor-monitor.mjs +++ b/scripts/build-cursor-monitor.mjs @@ -2,6 +2,12 @@ import { execSync } from "node:child_process"; import { copyFileSync, existsSync, mkdirSync, rmSync } from "node:fs"; import path from "node:path"; +import { + formatNativeHelperManifestWarning, + updateNativeHelperManifest, + verifyNativeHelperManifest, +} from "./native-helper-manifest.mjs"; + const projectRoot = process.cwd(); const sourceDir = path.join(projectRoot, "electron", "native", "cursor-monitor"); const buildDir = path.join(sourceDir, "build"); @@ -13,6 +19,7 @@ const bundledDir = path.join( process.arch === "arm64" ? "win32-arm64" : "win32-x64", ); const bundledExePath = path.join(bundledDir, "cursor-monitor.exe"); +const helperId = "cursor-monitor"; if (process.platform !== "win32") { console.log("[build-cursor-monitor] Skipping: host platform is not Windows."); @@ -79,6 +86,16 @@ function findCmake() { const cmake = findCmake(); if (!cmake) { if (existsSync(bundledExePath)) { + const verification = verifyNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "cursor-monitor.exe", + }); + if (!verification.ok) { + console.warn(formatNativeHelperManifestWarning("build-cursor-monitor", verification)); + } console.log(`[build-cursor-monitor] Using bundled helper: ${bundledExePath}`); process.exit(0); } @@ -139,6 +156,14 @@ if (existsSync(exePath)) { mkdirSync(bundledDir, { recursive: true }); copyFileSync(exePath, bundledExePath); console.log(`[build-cursor-monitor] Staged bundled helper: ${bundledExePath}`); + const manifestPath = updateNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "cursor-monitor.exe", + }); + console.log(`[build-cursor-monitor] Updated helper manifest: ${manifestPath}`); } else { console.error("[build-cursor-monitor] Expected exe not found at", exePath); process.exit(1); diff --git a/scripts/build-windows-capture.mjs b/scripts/build-windows-capture.mjs index 86092806..18f5ca36 100644 --- a/scripts/build-windows-capture.mjs +++ b/scripts/build-windows-capture.mjs @@ -2,6 +2,12 @@ import { execSync } from "node:child_process"; import { copyFileSync, mkdirSync, existsSync, rmSync } from "node:fs"; import path from "node:path"; +import { + formatNativeHelperManifestWarning, + updateNativeHelperManifest, + verifyNativeHelperManifest, +} from "./native-helper-manifest.mjs"; + const projectRoot = process.cwd(); const sourceDir = path.join(projectRoot, "electron", "native", "wgc-capture"); const buildDir = path.join(sourceDir, "build"); @@ -13,6 +19,7 @@ const bundledDir = path.join( process.arch === "arm64" ? "win32-arm64" : "win32-x64", ); const bundledExePath = path.join(bundledDir, "wgc-capture.exe"); +const helperId = "wgc-capture"; if (process.platform !== 'win32') { console.log('[build-windows-capture] Skipping native Windows capture build: host platform is not Windows.'); @@ -79,6 +86,16 @@ function findCmake() { const cmake = findCmake(); if (!cmake) { if (existsSync(bundledExePath)) { + const verification = verifyNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "wgc-capture.exe", + }); + if (!verification.ok) { + console.warn(formatNativeHelperManifestWarning("build-windows-capture", verification)); + } console.log(`[build-windows-capture] Using bundled helper: ${bundledExePath}`); process.exit(0); } @@ -139,6 +156,14 @@ if (existsSync(exePath)) { mkdirSync(bundledDir, { recursive: true }); copyFileSync(exePath, bundledExePath); console.log(`[build-windows-capture] Staged bundled helper: ${bundledExePath}`); + const manifestPath = updateNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath: bundledExePath, + binaryName: "wgc-capture.exe", + }); + console.log(`[build-windows-capture] Updated helper manifest: ${manifestPath}`); } else { console.error('[build-windows-capture] Expected exe not found at', exePath); process.exit(1); diff --git a/scripts/native-helper-manifest.mjs b/scripts/native-helper-manifest.mjs new file mode 100644 index 00000000..5c13996e --- /dev/null +++ b/scripts/native-helper-manifest.mjs @@ -0,0 +1,189 @@ +import { createHash } from "node:crypto"; +import { + existsSync, + mkdirSync, + readFileSync, + readdirSync, + statSync, + writeFileSync, +} from "node:fs"; +import path from "node:path"; + +const MANIFEST_FILE_NAME = "helpers-manifest.json"; + +function getPlatformArchDir(platform, arch) { + if (platform !== "win32") { + throw new Error(`Unsupported native helper manifest platform: ${platform}`); + } + + return arch === "arm64" ? "win32-arm64" : "win32-x64"; +} + +function hashBuffer(buffer) { + return createHash("sha256").update(buffer).digest("hex"); +} + +function collectSourceFiles(sourceDir, rootDir = sourceDir) { + const entries = readdirSync(sourceDir, { withFileTypes: true }); + const files = []; + + for (const entry of entries) { + if (entry.name === "build" || entry.name === "bin") { + continue; + } + + const absolutePath = path.join(sourceDir, entry.name); + if (entry.isDirectory()) { + files.push(...collectSourceFiles(absolutePath, rootDir)); + continue; + } + + if (!entry.isFile()) { + continue; + } + + files.push(path.relative(rootDir, absolutePath).replaceAll("\\", "/")); + } + + files.sort((left, right) => left.localeCompare(right)); + return files; +} + +function hashSourceTree(sourceDir) { + const hash = createHash("sha256"); + for (const relativePath of collectSourceFiles(sourceDir)) { + const absolutePath = path.join(sourceDir, relativePath); + hash.update(relativePath); + hash.update("\n"); + hash.update(readFileSync(absolutePath)); + hash.update("\n"); + } + + return hash.digest("hex"); +} + +function hashFile(filePath) { + return hashBuffer(readFileSync(filePath)); +} + +export function getNativeHelperManifestPath({ projectRoot, platform = process.platform, arch = process.arch }) { + return path.join( + projectRoot, + "electron", + "native", + "bin", + getPlatformArchDir(platform, arch), + MANIFEST_FILE_NAME, + ); +} + +function readManifest(manifestPath, platform, arch) { + if (!existsSync(manifestPath)) { + return { + version: 1, + platform, + arch, + helpers: {}, + }; + } + + const manifestContent = JSON.parse(readFileSync(manifestPath, "utf8")); + return { + version: 1, + platform, + arch, + helpers: {}, + ...manifestContent, + }; +} + +export function updateNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath, + binaryName, + platform = process.platform, + arch = process.arch, +}) { + const manifestPath = getNativeHelperManifestPath({ projectRoot, platform, arch }); + const manifestDir = path.dirname(manifestPath); + mkdirSync(manifestDir, { recursive: true }); + + const manifest = readManifest(manifestPath, platform, arch); + manifest.helpers[helperId] = { + binaryName, + binarySha256: hashFile(binaryPath), + sourceDir: path.relative(projectRoot, sourceDir).replaceAll("\\", "/"), + sourceFingerprint: hashSourceTree(sourceDir), + updatedAt: new Date().toISOString(), + }; + + writeFileSync(`${manifestPath}`, `${JSON.stringify(manifest, null, 2)}\n`); + return manifestPath; +} + +export function verifyNativeHelperManifest({ + projectRoot, + helperId, + sourceDir, + binaryPath, + binaryName, + platform = process.platform, + arch = process.arch, +}) { + const manifestPath = getNativeHelperManifestPath({ projectRoot, platform, arch }); + if (!existsSync(manifestPath)) { + return { + ok: false, + manifestPath, + reasons: ["manifest missing"], + }; + } + + if (!existsSync(binaryPath)) { + return { + ok: false, + manifestPath, + reasons: ["bundled helper missing"], + }; + } + + const manifest = readManifest(manifestPath, platform, arch); + const helperManifest = manifest.helpers?.[helperId]; + if (!helperManifest) { + return { + ok: false, + manifestPath, + reasons: [`${helperId} entry missing`], + }; + } + + const reasons = []; + if (helperManifest.binaryName !== binaryName) { + reasons.push(`expected binary ${binaryName}, found ${helperManifest.binaryName ?? "unknown"}`); + } + + const expectedBinaryHash = helperManifest.binarySha256; + const actualBinaryHash = hashFile(binaryPath); + if (expectedBinaryHash !== actualBinaryHash) { + reasons.push("binary hash mismatch"); + } + + const expectedSourceFingerprint = helperManifest.sourceFingerprint; + const actualSourceFingerprint = hashSourceTree(sourceDir); + if (expectedSourceFingerprint !== actualSourceFingerprint) { + reasons.push("source fingerprint mismatch"); + } + + return { + ok: reasons.length === 0, + manifestPath, + reasons, + }; +} + +export function formatNativeHelperManifestWarning(helperLabel, verificationResult) { + const reasonText = verificationResult.reasons.join(", "); + return `[${helperLabel}] Bundled helper provenance check failed (${reasonText}). Rebuild the helper to refresh ${path.basename(verificationResult.manifestPath)}.`; +} \ No newline at end of file