1import * as NodeEvents from "node:events";
2import * as NodeFS from "node:fs";
3import * as NodeFSP from "node:fs/promises";
4import * as NodeTimers from "node:timers";
5import * as NodeURL from "node:url";
6import * as NodeVM from "node:vm";
7import { parse } from "acorn";
8
9const expectedDesktopBridgeApis = [
10 "getClientPlatform",
11 "getLocalEnvironmentBootstraps",
12 "getPathForFile",
13 "pickFolder",
14];
15const clerkPasskeysGlobal = "__clerk_internal_electron_passkeys";
16const preloadExecutionTimeoutMs = 1_000;
17const desktopPackage = JSON.parse(
18 NodeFS.readFileSync(new URL("../package.json", import.meta.url), "utf8"),
19);
20const electronVersion = desktopPackage.dependencies.electron;
21
22const isSyntaxNode = (value) =>
23 typeof value === "object" && value !== null && "type" in value && typeof value.type === "string";
24
25const inspectBundle = (source) => {
26 const runtimeImports = [];
27 const visit = (node) => {
28 if (node.type === "ImportExpression") {
29 throw new Error("Desktop preload bundle contains a dynamic import() call");
30 }
31
32 if (node.type === "CallExpression" && node.callee.type === "Identifier") {
33 if (node.callee.name === "require") {
34 const [argument] = node.arguments;
35 if (node.arguments.length !== 1 || argument?.type !== "Literal") {
36 throw new Error("Desktop preload bundle contains a dynamic require() call");
37 }
38 if (typeof argument.value !== "string") {
39 throw new Error("Desktop preload bundle contains a dynamic require() call");
40 }
41 runtimeImports.push(argument.value);
42 }
43 }
44
45 for (const child of Object.values(node)) {
46 if (Array.isArray(child)) {
47 for (const item of child) {
48 if (isSyntaxNode(item)) visit(item);
49 }
50 } else if (isSyntaxNode(child)) {
51 visit(child);
52 }
53 }
54 };
55
56 visit(parse(source, { ecmaVersion: "latest", sourceType: "script" }));
57 return runtimeImports;
58};
59
60const createSandboxModules = (exposedGlobals) => {
61 const ipcRenderer = {
62 invoke: () => Promise.resolve(undefined),
63 on: () => undefined,
64 removeListener: () => undefined,
65 sendSync: () => undefined,
66 };
67 const electron = {
68 contextBridge: {
69 exposeInMainWorld: (name, api) => exposedGlobals.set(name, api),
70 },
71 ipcRenderer,
72 webUtils: {
73 getPathForFile: () => "",
74 },
75 };
76
77 return new Map([
78 ["electron", electron],
79 ["electron/common", electron],
80 ["electron/renderer", electron],
81 ["events", NodeEvents.default],
82 ["node:events", NodeEvents.default],
83 ["timers", NodeTimers.default],
84 ["node:timers", NodeTimers.default],
85 ["url", NodeURL.default],
86 ["node:url", NodeURL.default],
87 ]);
88};
89
90const executeBundle = (source, sandboxModules) => {
91 const sandboxProcess = {
92 contextIsolated: true,
93 // oxlint-disable-next-line t3code/no-global-process-runtime -- This standalone CI verifier supplies the preload's host platform without loading Effect.
94 platform: process.platform,
95 versions: { electron: electronVersion },
96 };
97 const requireSandboxModule = (moduleName) => {
98 if (!sandboxModules.has(moduleName)) {
99 throw new Error(
100 `Unsupported sandbox module requested during preload execution: ${moduleName}`,
101 );
102 }
103 return sandboxModules.get(moduleName);
104 };
105
106 NodeVM.runInNewContext(
107 source,
108 {
109 process: sandboxProcess,
110 require: requireSandboxModule,
111 window: { addEventListener: () => undefined },
112 },
113 {
114 filename: "desktop-preload.cjs",
115 timeout: preloadExecutionTimeoutMs,
116 },
117 );
118};
119
120export const verifyPreloadBundle = (source) => {
121 const runtimeImports = inspectBundle(source);
122 const exposedGlobals = new Map();
123 const sandboxModules = createSandboxModules(exposedGlobals);
124 const unsupportedImports = [...new Set(runtimeImports)]
125 .filter((moduleName) => !sandboxModules.has(moduleName))
126 .toSorted();
127
128 if (unsupportedImports.length > 0) {
129 throw new Error(
130 `Desktop preload bundle contains unsupported sandbox imports: ${unsupportedImports.join(", ")}`,
131 );
132 }
133
134 executeBundle(source, sandboxModules);
135
136 const desktopBridge = exposedGlobals.get("desktopBridge");
137 const missingApis = expectedDesktopBridgeApis.filter(
138 (api) => typeof desktopBridge?.[api] !== "function",
139 );
140 if (!exposedGlobals.has("desktopBridge")) missingApis.unshift("desktopBridge exposure");
141 if (!exposedGlobals.has(clerkPasskeysGlobal)) missingApis.push(`${clerkPasskeysGlobal} exposure`);
142
143 if (missingApis.length > 0) {
144 throw new Error(`Desktop preload bundle is missing executable APIs: ${missingApis.join(", ")}`);
145 }
146};
147
148if (process.argv[1] && NodeURL.pathToFileURL(process.argv[1]).href === import.meta.url) {
149 const preloadUrl = new URL("../dist-electron/preload.cjs", import.meta.url);
150 const source = await NodeFSP.readFile(preloadUrl, "utf8");
151 verifyPreloadBundle(source);
152}