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";
33import * as NodePath from "node:path";
44
55import * as NodeServices from "@effect/platform-node/NodeServices";
6import { extractFile } from "@electron/asar";
67import { assert, it } from "@effect/vitest";
78import * as ConfigProvider from "effect/ConfigProvider";
89import * as FileSystem from "effect/FileSystem";
......@@ -197,7 +198,7 @@ const makeWindowsPayloadFixture = Effect.fn("test.makeWindowsPayloadFixture")(fu
197198 yield* fs.makeDirectory(path.dirname(serverEntryPath), { recursive: true });
198199 yield* fs.makeDirectory(path.dirname(nativePath), { recursive: true });
199200 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
202203 const generatedAsarPath = path.join(tempDir, WINDOWS_SERVER_ASAR_RESOURCE);
203204 yield* packWindowsServerAsar({ sourceDir, asarPath: generatedAsarPath, arch: "x64" });
......@@ -888,7 +889,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
888889 ]);
889890 });
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", () =>
892893 Effect.scoped(
893894 Effect.gen(function* () {
894895 const fs = yield* FileSystem.FileSystem;
......@@ -902,12 +903,14 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
902903 "node_modules/node-pty/prebuilds/win32-arm64/conpty/OpenConsole.exe",
903904 "node_modules/node-pty/third_party/conpty/1.0.0/win10-x64/OpenConsole.exe",
904905 "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",
905908 ];
906909
907910 for (const nativeFile of nativeFiles) {
908911 const nativePath = path.join(sourceDir, nativeFile);
909912 yield* fs.makeDirectory(path.dirname(nativePath), { recursive: true });
910 yield* fs.writeFileString(nativePath, "native");
913 yield* fs.writeFileString(nativePath, nativeFile.includes("linux") ? "\x7fELF" : "MZ");
911914 }
912915
913916 const asarPath = path.join(tempDir, "server.asar");
......@@ -922,6 +925,22 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
922925 ),
923926 ),
924927 );
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 );
925944 assert.isFalse(
926945 yield* fs.exists(path.join(unpackedRoot, "node_modules/node-pty/prebuilds/win32-arm64")),
927946 );
......@@ -2126,7 +2145,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
21262145 }).pipe(Effect.provide(ConfigProvider.layer(ConfigProvider.fromEnv({ env: {} })))),
21272146 );
21282147
2129 it.effect("keeps executable resource editing enabled for unsigned Windows builds", () =>
2148 it.effect("configures Windows executable metadata and native signing extensions", () =>
21302149 Effect.gen(function* () {
21312150 const config = yield* createBuildConfig(
21322151 "win",
......@@ -2140,6 +2159,7 @@ it.layer(NodeServices.layer)("build-desktop-artifact", (it) => {
21402159
21412160 const win = config.win as Record<string, unknown>;
21422161 assert.equal(win.icon, "icon.ico");
2162 assert.deepStrictEqual(win.signExts, [".node", ".dll"]);
21432163 assert.equal(win.signAndEditExecutable, true);
21442164 assert.notProperty(win, "azureSignOptions");
21452165 }).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";
10281028// asar redirect convention). Everything else stays packed.
10291029export const WINDOWS_NATIVE_ASAR_UNPACK_GLOB =
10301030 "{**/*.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;
10311039// Mirrors DESKTOP_FILE_EXCLUSIONS for the hand-packed sidecar: the Claude SDK
10321040// platform packages are dead weight (see above), and node_modules/.bin shims
10331041// are never spawned at runtime (and are symlinks on POSIX build hosts, which
......@@ -2939,6 +2947,10 @@ export const createBuildConfig = Effect.fn("createBuildConfig")(function* (
29392947 const winConfig: Record<string, unknown> = {
29402948 target: [target],
29412949 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"],
29422954 // Resource editing applies the product metadata and icon independently
29432955 // of code signing. Disabling it for local unsigned builds leaves the
29442956 // packaged executable with Electron's stock icon.
......@@ -3027,11 +3039,33 @@ export const packWindowsServerAsar = Effect.fn("packWindowsServerAsar")(function
30273039 readonly arch: typeof BuildArch.Type;
30283040}) {
30293041 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 }
30303064 yield* Effect.tryPromise({
30313065 try: () =>
30323066 createPackageWithOptions(input.sourceDir, input.asarPath, {
30333067 dot: true,
3034 unpack: WINDOWS_NATIVE_ASAR_UNPACK_GLOB,
3068 unpack: `{${unpackGlobs.join(",")}}`,
30353069 // glob 13 (via @electron/asar 4) matches `ignore` relative to `cwd`,
30363070 // not against the absolute paths it crawls, so anchor it at the source.
30373071 globOptions: {