From aa3acd5a9efdbec2d25b02619db8915d4a071628 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 22 Sep 2026 10:15:59 -0700 Subject: [PATCH 1/3] Scope electron-rebuild to better-sqlite3 electron-rebuild was rebuilding every native module against Electron 44's headers, including macos-alias and fs-xattr (transitive deps of the optional appdmg package). macos-alias uses nan, which no longer compiles against Electron 44's V8 due to the new ExternalPointerTypeTag argument on v8::External::New and External::Value, so npm install failed outright. appdmg is a build-time DMG packager that runs under Node, never inside Electron, so rebuilding it against Electron headers was incorrect regardless. better-sqlite3 is the only native module the app loads at runtime. Co-Authored-By: Claude Opus 5 (1M context) --- package.json | 4 ++-- scripts/postinstall.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 266ce7a4..f919a98f 100644 --- a/package.json +++ b/package.json @@ -8,8 +8,8 @@ "authors": "Lockdown Systems LLC", "scripts": { "clean": "node ./scripts/clean.mjs", - "rebuild": "npx electron-rebuild --force", - "rebuild-clean": "rm -rf node_modules/better-sqlite3/build && npx electron-rebuild --force", + "rebuild": "npx electron-rebuild --force --only better-sqlite3", + "rebuild-clean": "rm -rf node_modules/better-sqlite3/build && npx electron-rebuild --force --only better-sqlite3", "postinstall": "node scripts/postinstall.js", "start": "electron-forge start", "dev": "electron-forge start", diff --git a/scripts/postinstall.js b/scripts/postinstall.js index c13e07fc..23617eb3 100755 --- a/scripts/postinstall.js +++ b/scripts/postinstall.js @@ -12,7 +12,7 @@ if (process.env.CI === "true" || process.env.CI === "1") { console.log("Rebuilding native modules for Electron..."); console.log("- Searching dependency tree"); try { - execSync("npx electron-rebuild --force", { + execSync("npx electron-rebuild --force --only better-sqlite3", { stdio: "inherit", cwd: process.cwd(), }); From e14fb1c870d3aac4d1bb594f4c7e0a2730ff1993 Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 22 Sep 2026 10:54:19 -0700 Subject: [PATCH 2/3] Fix macOS build: yauzl, scoped image-size, forge rebuild Three independent failures broke `npm run publish-prod` on macOS after the dependency upgrade. Each one masked the next. yauzl: extract-zip@2.0.1 pins yauzl@2.10.0, which uses fd-slicer@1.1.0. On Node 24 it stops delivering read streams partway through an archive (20 of 583 entries, 2 streams opened) and never settles. Inside electron-packager's promise chain the event loop simply drains, so the build exited 0 with no error and no out/ directory while extracting the Electron template zips. yauzl@3 drops fd-slicer and handles the same archive completely; extract-zip only calls yauzl.open, so the API is compatible. image-size: the unscoped "image-size": "^2.0.4" override upgraded appdmg's copy too. appdmg declares ^0.7.4 and calls it callback-style, but v2 dropped the default-function export, so the DMG maker crashed with "sizeOf is not a function". Scope appdmg to ^1.2.1, which keeps the callback API and is still the patched release the override was added for. rebuildConfig: forge passes rebuildConfig straight into @electron/rebuild during packaging. Unfiltered, it rebuilds appdmg's macos-alias against Electron 44's headers, where nan no longer compiles. appdmg is a build-time DMG packager that runs under Node and survives pruning as an optional prod dependency, so it has to be excluded; better-sqlite3 is the only native module the app loads at runtime. Verified with a full `npm run make-local`: universal package built (lipo: x86_64 arm64), DMG passes hdiutil verify, zip and RELEASES.json produced. Signing and notarization are not exercised by make-local, since scripts/make.js only sets MACOS_RELEASE=true for publish. Co-Authored-By: Claude Opus 5 (1M context) --- forge.config.ts | 8 +++++- package-lock.json | 67 ++++++++++++++++++++++------------------------- package.json | 4 +++ 3 files changed, 43 insertions(+), 36 deletions(-) diff --git a/forge.config.ts b/forge.config.ts index b2991166..a087aa63 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -254,7 +254,13 @@ const config: ForgeConfig = { // eslint-disable-next-line @typescript-eslint/no-explicit-any osxNotarize: osxNotarize as any, }, - rebuildConfig: {}, + // Only better-sqlite3 is loaded inside Electron and needs rebuilding against + // Electron's headers. appdmg's native deps (macos-alias, fs-xattr) are + // build-time tools that run under Node, and macos-alias's nan does not + // compile against Electron 44's V8, which fails the whole package step. + rebuildConfig: { + onlyModules: ["better-sqlite3"], + }, makers: [ // Windows new MakerSquirrel({ diff --git a/package-lock.json b/package-lock.json index dff89c95..16d34fa5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -6024,6 +6024,22 @@ "node": ">=8.5" } }, + "node_modules/appdmg/node_modules/image-size": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/image-size/-/image-size-1.2.1.tgz", + "integrity": "sha512-rH+46sQJ2dlwfjfhCyNx5thzrv+dtmBIhPHk0zgRUukHzZ/kRueTJXoYYsclBaKcSMBWuGbOFXtioLpzTb5euw==", + "license": "MIT", + "optional": true, + "dependencies": { + "queue": "6.0.2" + }, + "bin": { + "image-size": "bin/image-size.js" + }, + "engines": { + "node": ">=16.x" + } + }, "node_modules/archiver": { "version": "8.0.0", "resolved": "https://registry.npmjs.org/archiver/-/archiver-8.0.0.tgz", @@ -8919,15 +8935,6 @@ "reusify": "^1.0.4" } }, - "node_modules/fd-slicer": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", - "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", - "license": "MIT", - "dependencies": { - "pend": "~1.2.0" - } - }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -9862,19 +9869,6 @@ "node": ">= 4" } }, - "node_modules/image-size": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.4.tgz", - "integrity": "sha512-QRUkFFsRV/6fuESxb9Vkq+a0LkSrgKXuc2NEqfikiXxxN/G3tjWt5EVUlMaImRBZRZK/jRBEbYvpPYZL8t08Zw==", - "license": "MIT", - "optional": true, - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=18" - } - }, "node_modules/imul": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/imul/-/imul-1.0.1.tgz", @@ -12617,6 +12611,16 @@ "dev": true, "license": "MIT" }, + "node_modules/queue": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/queue/-/queue-6.0.2.tgz", + "integrity": "sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "~2.0.3" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -15219,22 +15223,15 @@ } }, "node_modules/yauzl": { - "version": "2.10.0", - "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", - "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-3.4.0.tgz", + "integrity": "sha512-jIH9yLR9wqr0wOS0TpBvo/g/2UgZH5qePVbjgRliiF0BYvOZyaBknKsF+x9Iht0O6sqgnB93rCICdOZFecJuDw==", "license": "MIT", "dependencies": { - "buffer-crc32": "~0.2.3", - "fd-slicer": "~1.1.0" - } - }, - "node_modules/yauzl/node_modules/buffer-crc32": { - "version": "0.2.13", - "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", - "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", - "license": "MIT", + "pend": "~1.2.0" + }, "engines": { - "node": "*" + "node": ">=12" } }, "node_modules/yn": { diff --git a/package.json b/package.json index f919a98f..59fd9d5a 100644 --- a/package.json +++ b/package.json @@ -130,9 +130,13 @@ "macos-alias@0.2.12": true }, "overrides": { + "yauzl": "^3.4.0", "tar": "^7.5.22", "tmp": "^0.2.7", "image-size": "^2.0.4", + "appdmg": { + "image-size": "^1.2.1" + }, "mhtml2html": { "jsdom": "^29.1.1" }, From 4d28ec0f21459d7ae52c59ff3fdedd6984ba350a Mon Sep 17 00:00:00 2001 From: Micah Lee Date: Tue, 22 Sep 2026 11:43:38 -0700 Subject: [PATCH 3/3] Preflight release credentials and signing identity MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit @electron/packager defaults to continueOnError: true when signing (mac.js:402), so a missing Developer ID identity does not fail the build. It leaves the stock ad-hoc Electron signature in place, and the build dies minutes later inside @electron/notarize's pre-flight with "code object is not signed at all" — a message that describes the symptom and never names the cause. Check up front, before clean.mjs deletes the previous build, that publish has what it needs: the Spaces credentials on every platform, and on macOS that the signing identity is actually in the keychain plus the notarization credentials are set. Each failure names the missing piece and how to get it. forge.config.ts now reads the identity from MACOS_SIGNING_IDENTITY rather than hardcoding it, so the preflight cannot pass while signing uses some other identity. It throws if the variable is unset while MACOS_RELEASE is true, which only happens when forge is invoked outside scripts/make.js. Co-Authored-By: Claude Opus 5 (1M context) --- forge.config.ts | 14 +++++++++- scripts/make.js | 72 +++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 1 deletion(-) diff --git a/forge.config.ts b/forge.config.ts index a087aa63..155323c8 100644 --- a/forge.config.ts +++ b/forge.config.ts @@ -160,8 +160,20 @@ const mimeTypeScheme = blueskyOAuthSchemeHandlerMimeTypeForMode( let osxSign: OsxSignOptions | undefined; let osxNotarize: NotaryToolCredentials | undefined; if (process.env.MACOS_RELEASE === "true") { + // Set by scripts/make.js, which also preflights that this identity actually + // exists in the keychain. Signing must not fall back to a hardcoded string + // here, or the preflight could pass while signing uses a different identity. + const identity = process.env.MACOS_SIGNING_IDENTITY; + if (!identity) { + throw new Error( + "MACOS_SIGNING_IDENTITY is not set. Build macOS releases via " + + "`npm run publish-dev` / `npm run publish-prod` so scripts/make.js can " + + "set it and verify the identity is in the keychain.", + ); + } + osxSign = { - identity: "Developer ID Application: Lockdown Systems LLC (G762K6CH36)", + identity, optionsForFile: (filePath) => { const entitlementDefault = path.join( assetsPath, diff --git a/scripts/make.js b/scripts/make.js index 63316ee0..39632546 100644 --- a/scripts/make.js +++ b/scripts/make.js @@ -37,6 +37,78 @@ if (platform == "win32") { process.env.MACOS_RELEASE = command === "publish" ? "true" : "false"; } +// The Developer ID identity used to sign macOS releases. forge.config.ts reads +// this from the environment so the preflight check below and the actual signing +// can never drift apart. +const macosSigningIdentity = + "Developer ID Application: Lockdown Systems LLC (G762K6CH36)"; +process.env.MACOS_SIGNING_IDENTITY = macosSigningIdentity; + +// Preflight checks +// +// @electron/packager defaults to `continueOnError: true` when signing, so a +// missing identity does not fail the build. It leaves the stock ad-hoc Electron +// signature in place and the build dies minutes later inside notarization with +// "code object is not signed at all", which does not name the real cause. Fail +// up front instead. + +function fail(message, hint) { + console.error(`\nāŒ ${message}\n`); + console.error(` ${hint}\n`); + process.exit(1); +} + +function requireEnv(name, hint) { + if (!process.env[name]) { + fail(`${name} is not set, which ${command} requires.`, hint); + } +} + +if (command === "publish") { + requireEnv( + "DO_SPACES_KEY", + "Needed to upload the release to DigitalOcean Spaces.", + ); + requireEnv( + "DO_SPACES_SECRET", + "Needed to upload the release to DigitalOcean Spaces.", + ); + + if (platform === "darwin") { + let identities = ""; + try { + identities = execSync("security find-identity -v -p codesigning", { + encoding: "utf8", + }); + } catch { + fail( + "Could not read code signing identities from the keychain.", + "Try running: security find-identity -v -p codesigning", + ); + } + + if (!identities.includes(macosSigningIdentity)) { + fail( + `No code signing identity matching "${macosSigningIdentity}" in your keychain.`, + "Import the Developer ID .p12, or create a new certificate at " + + "https://developer.apple.com/account/resources/certificates (Account " + + "Holder only), then confirm with: security find-identity -v -p codesigning", + ); + } + + console.log(`āœ… Signing identity found: ${macosSigningIdentity}`); + + requireEnv( + "APPLE_ID", + "The Apple ID used to notarize. Needed by notarytool.", + ); + requireEnv( + "APPLE_PASSWORD", + "An app-specific password from https://appleid.apple.com, not your Apple ID password.", + ); + } +} + try { // Clean up previous builds and install dependencies execSync(`node ./scripts/clean.mjs`, { stdio: "inherit" });