authorgravatar for diego23lu@hotmail.comDiego Lumbreras <diego23lu@hotmail.com> 2026-10-09 22:24:26-06:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-10-10 01:24:26-03:00
logfa9f936ef5cbbe4bd4f9890b638766fc92f45075
tree675cf199629624404b7fee3b3a72b7b188b975ac
parentd02e292610ea5eff171d388211ba3ede2f0467a0
signature Signed by PGP key B5690EEEBB952194

fix(desktop): sign Windows native addons (#8206)

Co-authored-by: maria-rcks <maria@kuuro.net>

2 files changed, 59 insertions(+), 5 deletions(-)

scripts/build-desktop-artifact.test.ts+24-4
...@@ -3,6 +3,7 @@ import * as NodeCrypto from "node:crypto";...@@ -3,6 +3,7 @@ import * as NodeCrypto from "node:crypto";
3import * as NodePath from "node:path";3import * as NodePath from "node:path";
44
5import * as NodeServices from "@effect/platform-node/NodeServices";5import * as NodeServices from "@effect/platform-node/NodeServices";
6import { extractFile } from "@electron/asar";
6import { assert, it } from "@effect/vitest";7import { assert, it } from "@effect/vitest";
7import * as ConfigProvider from "effect/ConfigProvider";8import * as ConfigProvider from "effect/ConfigProvider";
8import * as FileSystem from "effect/FileSystem";9import * as FileSystem from "effect/FileSystem";
...@@ -197,7 +198,7 @@ const makeWindowsPayloadFixture = Effect.fn("test.makeWindowsPayloadFixture")(fu...@@ -197,7 +198,7 @@ const makeWindowsPayloadFixture = Effect.fn("test.makeWindowsPayloadFixture")(fu
197 yield* fs.makeDirectory(path.dirname(serverEntryPath), { recursive: true });198 yield* fs.makeDirectory(path.dirname(serverEntryPath), { recursive: true });
198 yield* fs.makeDirectory(path.dirname(nativePath), { recursive: true });199 yield* fs.makeDirectory(path.dirname(nativePath), { recursive: true });
199 yield* fs.writeFileString(serverEntryPath, input.serverEntrySource ?? "console.log('server');\n");200 yield* fs.writeFileString(serverEntryPath, input.serverEntrySource ?? "console.log('server');\n");
200 yield* fs.writeFileString(nativePath, "native-binary");201 yield* fs.writeFileString(nativePath, "MZ native-binary");
201202
202 const generatedAsarPath = path.join(tempDir, WINDOWS_SERVER_ASAR_RESOURCE);203 const generatedAsarPath = path.join(tempDir, WINDOWS_SERVER_ASAR_RESOURCE);
203 yield* packWindowsServerAsar({ sourceDir, asarPath: generatedAsarPath, arch: "x64" });204 yield* packWindowsServerAsar({ sourceDir, asarPath: generatedAsarPath, arch: "x64" });
...@@ -888,7 +889,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -888,7 +889,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
888 ]);889 ]);
889 });890 });
890891
891 it.effect("keeps target native files while excluding the other Windows architecture", () =>892 it.effect("unpacks target Windows natives while excluding the other architecture", () =>
892 Effect.scoped(893 Effect.scoped(
893 Effect.gen(function* () {894 Effect.gen(function* () {
894 const fs = yield* FileSystem.FileSystem;895 const fs = yield* FileSystem.FileSystem;
...@@ -902,12 +903,14 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -902,12 +903,14 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
902 "node_modules/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe",903 "node_modules/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe",
903 "node_modules/node-pty/third_party/conpty/1.0.0/win10-x64/OpenConsole.exe",904 "node_modules/node-pty/third_party/conpty/1.0.0/win10-x64/OpenConsole.exe",
904 "node_modules/node-pty/third_party/conpty/1.0.0/win10-arm64/OpenConsole.exe",905 "node_modules/node-pty/third_party/conpty/1.0.0/win10-arm64/OpenConsole.exe",
906 "node_modules/node-pty/prebuilds/win32-x64/pty.node",
907 "node_modules/node-pty/prebuilds/linux-x64/pty.node",
905 ];908 ];
906909
907 for (const nativeFile of nativeFiles) {910 for (const nativeFile of nativeFiles) {
908 const nativePath = path.join(sourceDir, nativeFile);911 const nativePath = path.join(sourceDir, nativeFile);
909 yield* fs.makeDirectory(path.dirname(nativePath), { recursive: true });912 yield* fs.makeDirectory(path.dirname(nativePath), { recursive: true });
910 yield* fs.writeFileString(nativePath, "native");913 yield* fs.writeFileString(nativePath, nativeFile.includes("linux") ? "\x7fELF" : "MZ");
911 }914 }
912915
913 const asarPath = path.join(tempDir, "server.asar");916 const asarPath = path.join(tempDir, "server.asar");
...@@ -922,6 +925,22 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -922,6 +925,22 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
922 ),925 ),
923 ),926 ),
924 );927 );
928 assert.isTrue(
929 yield* fs.exists(
930 path.join(unpackedRoot, "node_modules/node-pty/prebuilds/win32-x64/pty.node"),
931 ),
932 );
933 // Signed builds sign every unpacked .node, so the ELF prebuild stays packed.
934 assert.isFalse(
935 yield* fs.exists(path.join(unpackedRoot, "node_modules/node-pty/prebuilds/linux-x64")),
936 );
937 assert.equal(
938 extractFile(
939 asarPath,
940 path.join("node_modules", "node-pty", "prebuilds", "linux-x64", "pty.node"),
941 ).toString(),
942 "\x7fELF",
943 );
925 assert.isFalse(944 assert.isFalse(
926 yield* fs.exists(path.join(unpackedRoot, "node_modules/node-pty/prebuilds/win32-arm64")),945 yield* fs.exists(path.join(unpackedRoot, "node_modules/node-pty/prebuilds/win32-arm64")),
927 );946 );
...@@ -2126,7 +2145,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -2126,7 +2145,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
2126 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),2145 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
2127 );2146 );
21282147
2129 it.effect("keeps executable resource editing enabled for unsigned Windows builds", () =>2148 it.effect("configures Windows executable metadata and native signing extensions", () =>
2130 Effect.gen(function* () {2149 Effect.gen(function* () {
2131 const config = yield* createBuildConfig(2150 const config = yield* createBuildConfig(
2132 "win",2151 "win",
...@@ -2140,6 +2159,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {...@@ -2140,6 +2159,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
21402159
2141 const win = config.win as Record<string, unknown>;2160 const win = config.win as Record<string, unknown>;
2142 assert.equal(win.icon, "icon.ico");2161 assert.equal(win.icon, "icon.ico");
2162 assert.deepStrictEqual(win.signExts, [".node", ".dll"]);
2143 assert.equal(win.signAndEditExecutable, true);2163 assert.equal(win.signAndEditExecutable, true);
2144 assert.notProperty(win, "azureSignOptions");2164 assert.notProperty(win, "azureSignOptions");
2145 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),2165 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
scripts/build-desktop-artifact.ts+35-1
...@@ -1028,6 +1028,14 @@ export const WINDOWS_SERVER_ASAR_RESOURCE = "server.asar";...@@ -1028,6 +1028,14 @@ export const WINDOWS_SERVER_ASAR_RESOURCE = "server.asar";
1028// asar redirect convention). Everything else stays packed.1028// asar redirect convention). Everything else stays packed.
1029export const WINDOWS_NATIVE_ASAR_UNPACK_GLOB =1029export const WINDOWS_NATIVE_ASAR_UNPACK_GLOB =
1030 "{**/*.node,**/*.dll,**/*.exe,**/*.so,**/*.so.*,**/*.dylib}";1030 "{**/*.node,**/*.dll,**/*.exe,**/*.so,**/*.so.*,**/*.dylib}";
1031// The server sidecar unpacks .node files separately, keeping only Windows ones.
1032export const WINDOWS_SERVER_ASAR_UNPACK_GLOBS = [
1033 "**/*.dll",
1034 "**/*.exe",
1035 "**/*.so",
1036 "**/*.so.*",
1037 "**/*.dylib",
1038] as const;
1031// Mirrors DESKTOP_FILE_EXCLUSIONS for the hand-packed sidecar: the Claude SDK1039// Mirrors DESKTOP_FILE_EXCLUSIONS for the hand-packed sidecar: the Claude SDK
1032// platform packages are dead weight (see above), and node_modules/.bin shims1040// platform packages are dead weight (see above), and node_modules/.bin shims
1033// are never spawned at runtime (and are symlinks on POSIX build hosts, which1041// are never spawned at runtime (and are symlinks on POSIX build hosts, which
...@@ -2939,6 +2947,10 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (...@@ -2939,6 +2947,10 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
2939 const winConfig: Record<string, unknown> = {2947 const winConfig: Record<string, unknown> = {
2940 target: [target],2948 target: [target],
2941 icon: "icon.ico",2949 icon: "icon.ico",
2950 // Smart App Control validates unpacked native addons independently from
2951 // the signed desktop executable. Extend release signing to every native
2952 // Windows library that Electron or the server sidecar loads at runtime.
2953 signExts: [".node", ".dll"],
2942 // Resource editing applies the product metadata and icon independently2954 // Resource editing applies the product metadata and icon independently
2943 // of code signing. Disabling it for local unsigned builds leaves the2955 // of code signing. Disabling it for local unsigned builds leaves the
2944 // packaged executable with Electron's stock icon.2956 // packaged executable with Electron's stock icon.
...@@ -3027,11 +3039,33 @@ export const packWindowsServerAsar = Effect.fn("packWindowsServerAsar")(function...@@ -3027,11 +3039,33 @@ export const packWindowsServerAsar = Effect.fn("packWindowsServerAsar")(function
3027 readonly arch: typeof BuildArch.Type;3039 readonly arch: typeof BuildArch.Type;
3028}) {3040}) {
3029 const fs = yield* FileSystem.FileSystem;3041 const fs = yield* FileSystem.FileSystem;
3042 const path = yield* Path.Path;
3043 // Native addons are unpacked only when they are Windows PE ("MZ") binaries.
3044 // Signed builds Authenticode-sign every unpacked .node file, which fails on
3045 // the darwin and linux prebuilds some packages (node-pty) ship alongside
3046 // their win32 ones. The Windows primary never loads those, so they stay packed.
3047 const unpackGlobs: string[] = [...WINDOWS_SERVER_ASAR_UNPACK_GLOBS];
3048 for (const entry of yield* fs.readDirectory(input.sourceDir, { recursive: true })) {
3049 if (!entry.endsWith(".node")) continue;
3050 const addonPath = path.join(input.sourceDir, entry);
3051 if ((yield* fs.stat(addonPath)).type !== "File") continue;
3052 const bytes = yield* fs.readFile(addonPath);
3053 if (bytes[0] !== 0x4d || bytes[1] !== 0x5a) continue;
3054 const posixPath = entry.split(path.sep).join("/");
3055 // Backslash escapes are path separators to minimatch on Windows.
3056 if (/[\\*?[\]{}(),]/.test(posixPath)) {
3057 return yield* new WindowsServerSidecarPackError({
3058 asarPath: input.asarPath,
3059 cause: new Error(`native addon path contains glob syntax: ${posixPath}`),
3060 });
3061 }
3062 unpackGlobs.push(`**/${posixPath}`);
3063 }
3030 yield* Effect.tryPromise({3064 yield* Effect.tryPromise({
3031 try: () =>3065 try: () =>
3032 createPackageWithOptions(input.sourceDir, input.asarPath, {3066 createPackageWithOptions(input.sourceDir, input.asarPath, {
3033 dot: true,3067 dot: true,
3034 unpack: WINDOWS_NATIVE_ASAR_UNPACK_GLOB,3068 unpack: `{${unpackGlobs.join(",")}}`,
3035 // glob 13 (via @electron/asar 4) matches `ignore` relative to `cwd`,3069 // glob 13 (via @electron/asar 4) matches `ignore` relative to `cwd`,
3036 // not against the absolute paths it crawls, so anchor it at the source.3070 // not against the absolute paths it crawls, so anchor it at the source.
3037 globOptions: {3071 globOptions: {