Skip to content

Commit 61ccd5d

Browse files
committed
chore(electron): Add initial main and preload scripts (#8785)
1 parent 48a5484 commit 61ccd5d

15 files changed

Lines changed: 758 additions & 7 deletions

File tree

‎packages/electron/package.json‎

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,16 @@
4444
"default": "./dist/cjs/preload/index.js"
4545
}
4646
},
47+
"./storage": {
48+
"import": {
49+
"types": "./dist/types/storage/index.d.ts",
50+
"default": "./dist/esm/storage/index.js"
51+
},
52+
"require": {
53+
"types": "./dist/types/storage/index.d.ts",
54+
"default": "./dist/cjs/storage/index.js"
55+
}
56+
},
4757
"./package.json": "./package.json"
4858
},
4959
"main": "./dist/cjs/index.js",
@@ -71,7 +81,18 @@
7181
"tslib": "catalog:repo"
7282
},
7383
"devDependencies": {
74-
"@types/node": "^22.19.17"
84+
"@types/node": "^22.19.17",
85+
"electron": "^39.2.6",
86+
"electron-store": "^8.2.0"
87+
},
88+
"peerDependencies": {
89+
"electron": ">=28",
90+
"electron-store": "^8.2.0"
91+
},
92+
"peerDependenciesMeta": {
93+
"electron-store": {
94+
"optional": true
95+
}
7596
},
7697
"engines": {
7798
"node": ">=20.9.0"

‎packages/electron/src/global.d.ts‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
import type { TokenCache } from './shared/types';
2+
13
declare const PACKAGE_NAME: string;
24
declare const PACKAGE_VERSION: string;
35
declare const __DEV__: boolean;
6+
7+
declare global {
8+
interface Window {
9+
__clerk_internal_electron?: {
10+
tokenCache: TokenCache;
11+
};
12+
}
13+
}

‎packages/electron/src/index.ts‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
1-
// TODO: Implement the Clerk SDK for the Electron main process.
2-
export const SDK_NAME = PACKAGE_NAME;
1+
export { setupMain } from './main/setup-main';
2+
export type { TokenStorage } from './shared/types';
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
import { ipcMain } from 'electron';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
import { TOKEN_CACHE_CHANNELS } from '../../shared/ipc';
5+
import type { TokenStorage } from '../../shared/types';
6+
import { setupTokenCacheIpcHandlers } from '../ipc-handlers';
7+
8+
const ipcEvent = {} as Electron.IpcMainInvokeEvent;
9+
10+
vi.mock('electron', () => ({
11+
ipcMain: {
12+
handle: vi.fn(),
13+
removeHandler: vi.fn(),
14+
},
15+
}));
16+
17+
describe('setupTokenCacheIpcHandlers', () => {
18+
const storage: TokenStorage = {
19+
getItem: vi.fn(),
20+
setItem: vi.fn(),
21+
removeItem: vi.fn(),
22+
};
23+
24+
beforeEach(() => {
25+
vi.clearAllMocks();
26+
});
27+
28+
it('registers token cache IPC handlers', () => {
29+
setupTokenCacheIpcHandlers(storage);
30+
31+
expect(ipcMain.handle).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.getToken, expect.any(Function));
32+
expect(ipcMain.handle).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.saveToken, expect.any(Function));
33+
expect(ipcMain.handle).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.clearToken, expect.any(Function));
34+
});
35+
36+
it('delegates token operations to the storage adapter', async () => {
37+
vi.mocked(storage.getItem).mockResolvedValue('jwt');
38+
39+
setupTokenCacheIpcHandlers(storage);
40+
41+
const getTokenHandler = vi.mocked(ipcMain.handle).mock.calls[0][1];
42+
const saveTokenHandler = vi.mocked(ipcMain.handle).mock.calls[1][1];
43+
const clearTokenHandler = vi.mocked(ipcMain.handle).mock.calls[2][1];
44+
45+
await expect(getTokenHandler(ipcEvent, 'token-key')).resolves.toBe('jwt');
46+
await saveTokenHandler(ipcEvent, 'token-key', 'jwt');
47+
await clearTokenHandler(ipcEvent, 'token-key');
48+
49+
expect(storage.getItem).toHaveBeenCalledWith('token-key');
50+
expect(storage.setItem).toHaveBeenCalledWith('token-key', 'jwt');
51+
expect(storage.removeItem).toHaveBeenCalledWith('token-key');
52+
});
53+
54+
it('removes registered handlers on cleanup', () => {
55+
const cleanup = setupTokenCacheIpcHandlers(storage);
56+
57+
cleanup();
58+
59+
expect(ipcMain.removeHandler).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.getToken);
60+
expect(ipcMain.removeHandler).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.saveToken);
61+
expect(ipcMain.removeHandler).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.clearToken);
62+
});
63+
});
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
import { ipcMain } from 'electron';
2+
import { beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
import type { TokenStorage } from '../../shared/types';
5+
import { setupMain } from '../setup-main';
6+
7+
vi.mock('electron', () => ({
8+
ipcMain: {
9+
handle: vi.fn(),
10+
removeHandler: vi.fn(),
11+
},
12+
}));
13+
14+
describe('setupMain', () => {
15+
const missingStorage = {} as Parameters<typeof setupMain>[0];
16+
const storage: TokenStorage = {
17+
getItem: vi.fn(),
18+
setItem: vi.fn(),
19+
removeItem: vi.fn(),
20+
};
21+
22+
beforeEach(() => {
23+
vi.clearAllMocks();
24+
});
25+
26+
it('requires a storage adapter', () => {
27+
expect(() => setupMain(missingStorage)).toThrow('setupMain requires a storage adapter');
28+
});
29+
30+
it('sets up token persistence IPC handlers with the provided storage', () => {
31+
setupMain({ storage });
32+
33+
expect(ipcMain.handle).toHaveBeenCalledTimes(3);
34+
});
35+
36+
it('returns a cleanup function for registered handlers', () => {
37+
const clerk = setupMain({ storage });
38+
39+
clerk.cleanup();
40+
41+
expect(ipcMain.removeHandler).toHaveBeenCalledTimes(3);
42+
});
43+
});
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { ipcMain } from 'electron';
2+
3+
import { TOKEN_CACHE_CHANNELS } from '../shared/ipc';
4+
import type { TokenStorage } from '../shared/types';
5+
6+
export function setupTokenCacheIpcHandlers(storage: TokenStorage): () => void {
7+
ipcMain.handle(TOKEN_CACHE_CHANNELS.getToken, (_event, key: string) => {
8+
return storage.getItem(key);
9+
});
10+
11+
ipcMain.handle(TOKEN_CACHE_CHANNELS.saveToken, (_event, key: string, value: string) => {
12+
return storage.setItem(key, value);
13+
});
14+
15+
ipcMain.handle(TOKEN_CACHE_CHANNELS.clearToken, (_event, key: string) => {
16+
return storage.removeItem(key);
17+
});
18+
19+
return () => {
20+
ipcMain.removeHandler(TOKEN_CACHE_CHANNELS.getToken);
21+
ipcMain.removeHandler(TOKEN_CACHE_CHANNELS.saveToken);
22+
ipcMain.removeHandler(TOKEN_CACHE_CHANNELS.clearToken);
23+
};
24+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
import type { SetupMainOptions, SetupMainReturn } from '../shared/types';
2+
import { setupTokenCacheIpcHandlers } from './ipc-handlers';
3+
4+
export function setupMain(options: SetupMainOptions): SetupMainReturn {
5+
if (!options.storage) {
6+
throw new Error(
7+
'Clerk: setupMain requires a storage adapter. Pass setupMain({ storage: storage() }) from @clerk/electron/storage, or provide a custom storage adapter.',
8+
);
9+
}
10+
11+
const cleanupTokenPersistence = setupTokenCacheIpcHandlers(options.storage);
12+
13+
return {
14+
cleanup() {
15+
cleanupTokenPersistence();
16+
},
17+
};
18+
}
Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
import { contextBridge, ipcRenderer } from 'electron';
2+
import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
3+
4+
import { TOKEN_CACHE_CHANNELS } from '../../shared/ipc';
5+
import { setupPreload } from '../index';
6+
7+
vi.mock('electron', () => ({
8+
contextBridge: {
9+
exposeInMainWorld: vi.fn(),
10+
},
11+
ipcRenderer: {
12+
invoke: vi.fn(),
13+
},
14+
}));
15+
16+
describe('setupPreload', () => {
17+
const originalContextIsolated = process.contextIsolated;
18+
19+
beforeEach(() => {
20+
vi.clearAllMocks();
21+
Object.defineProperty(process, 'contextIsolated', { configurable: true, value: true });
22+
vi.stubGlobal('window', {});
23+
});
24+
25+
afterEach(() => {
26+
vi.unstubAllGlobals();
27+
});
28+
29+
afterAll(() => {
30+
Object.defineProperty(process, 'contextIsolated', { configurable: true, value: originalContextIsolated });
31+
});
32+
33+
it('exposes the Clerk Electron bridge through contextBridge when context isolation is enabled', () => {
34+
setupPreload();
35+
36+
expect(contextBridge.exposeInMainWorld).toHaveBeenCalledWith('__clerk_internal_electron', {
37+
tokenCache: {
38+
getToken: expect.any(Function),
39+
saveToken: expect.any(Function),
40+
clearToken: expect.any(Function),
41+
},
42+
});
43+
});
44+
45+
it('exposes the Clerk Electron bridge on window when context isolation is disabled', () => {
46+
Object.defineProperty(process, 'contextIsolated', { configurable: true, value: false });
47+
48+
setupPreload();
49+
50+
expect(window.__clerk_internal_electron?.tokenCache).toEqual({
51+
getToken: expect.any(Function),
52+
saveToken: expect.any(Function),
53+
clearToken: expect.any(Function),
54+
});
55+
});
56+
57+
it('forwards token cache calls over IPC', async () => {
58+
setupPreload();
59+
60+
const bridge = vi.mocked(contextBridge.exposeInMainWorld).mock
61+
.calls[0][1] as typeof window.__clerk_internal_electron;
62+
63+
await bridge?.tokenCache.getToken('token-key');
64+
await bridge?.tokenCache.saveToken('token-key', 'jwt');
65+
await bridge?.tokenCache.clearToken('token-key');
66+
67+
expect(ipcRenderer.invoke).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.getToken, 'token-key');
68+
expect(ipcRenderer.invoke).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.saveToken, 'token-key', 'jwt');
69+
expect(ipcRenderer.invoke).toHaveBeenCalledWith(TOKEN_CACHE_CHANNELS.clearToken, 'token-key');
70+
});
71+
});
Lines changed: 18 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,18 @@
1-
// TODO: Implement the Clerk preload bridge for the Electron preload context.
2-
export const SDK_NAME = PACKAGE_NAME;
1+
import { contextBridge, ipcRenderer } from 'electron';
2+
3+
import { TOKEN_CACHE_CHANNELS } from '../shared/ipc';
4+
import type { TokenCache } from '../shared/types';
5+
6+
export function setupPreload(): void {
7+
const tokenCache: TokenCache = {
8+
getToken: key => ipcRenderer.invoke(TOKEN_CACHE_CHANNELS.getToken, key),
9+
saveToken: (key, value) => ipcRenderer.invoke(TOKEN_CACHE_CHANNELS.saveToken, key, value),
10+
clearToken: key => ipcRenderer.invoke(TOKEN_CACHE_CHANNELS.clearToken, key),
11+
};
12+
13+
if (process.contextIsolated) {
14+
contextBridge.exposeInMainWorld('__clerk_internal_electron', { tokenCache });
15+
} else {
16+
window.__clerk_internal_electron = { tokenCache };
17+
}
18+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
export const TOKEN_CACHE_CHANNELS = {
2+
getToken: 'clerk:token-cache:get',
3+
saveToken: 'clerk:token-cache:save',
4+
clearToken: 'clerk:token-cache:clear',
5+
} as const;

0 commit comments

Comments
 (0)