Skip to content

Commit b8f0bfb

Browse files
committed
chore: Add custom renderer origin support (#8816)
1 parent e49adbc commit b8f0bfb

5 files changed

Lines changed: 125 additions & 2 deletions

File tree

‎packages/electron/README.md‎

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,14 +40,40 @@ This package exposes entrypoints for Electron's distinct runtime contexts:
4040

4141
```ts
4242
// main.ts
43+
import { app, BrowserWindow, net, protocol } from 'electron';
4344
import { setupMain } from '@clerk/electron';
4445
import { storage } from '@clerk/electron/storage';
46+
import { join } from 'node:path';
47+
import { pathToFileURL } from 'node:url';
4548

4649
setupMain({
4750
storage: storage(),
51+
renderer: {
52+
scheme: 'my-app',
53+
host: 'renderer',
54+
},
55+
});
56+
57+
app.whenReady().then(() => {
58+
protocol.handle('my-app', request => {
59+
const url = new URL(request.url);
60+
const file = url.pathname === '/' ? 'index.html' : url.pathname;
61+
62+
return net.fetch(pathToFileURL(join(__dirname, '../renderer', file)).toString());
63+
});
64+
65+
const win = new BrowserWindow({
66+
webPreferences: {
67+
preload: join(__dirname, '../preload/index.js'),
68+
},
69+
});
70+
71+
win.loadURL('my-app://renderer/');
4872
});
4973
```
5074

75+
In `my-app://renderer/sign-in`, `my-app` is the scheme, `renderer` is the host, `my-app://renderer` is the origin, and `/sign-in` is the path. If your renderer uses path-based routing, serve every route from the same origin and fall back to your renderer entrypoint as needed.
76+
5177
```tsx
5278
// renderer.tsx
5379
import { ClerkProvider } from '@clerk/electron/react';

‎packages/electron/src/index.ts‎

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1 @@
11
export { setupMain } from './main/setup-main';
2-
export type { TokenStorage } from './shared/types';

‎packages/electron/src/main/__tests__/setup-main.test.ts‎

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { ipcMain } from 'electron';
1+
import { ipcMain, protocol } from 'electron';
22
import { beforeEach, describe, expect, it, vi } from 'vitest';
33

44
import type { TokenStorage } from '../../shared/types';
@@ -9,6 +9,9 @@ vi.mock('electron', () => ({
99
handle: vi.fn(),
1010
removeHandler: vi.fn(),
1111
},
12+
protocol: {
13+
registerSchemesAsPrivileged: vi.fn(),
14+
},
1215
}));
1316

1417
describe('setupMain', () => {
@@ -33,6 +36,53 @@ describe('setupMain', () => {
3336
expect(ipcMain.handle).toHaveBeenCalledTimes(3);
3437
});
3538

39+
it('registers the configured renderer scheme as privileged before app ready', () => {
40+
setupMain({
41+
storage,
42+
renderer: {
43+
host: 'renderer',
44+
scheme: 'my-app',
45+
},
46+
});
47+
48+
expect(protocol.registerSchemesAsPrivileged).toHaveBeenCalledWith([
49+
{
50+
scheme: 'my-app',
51+
privileges: {
52+
corsEnabled: true,
53+
secure: true,
54+
standard: true,
55+
stream: true,
56+
supportFetchAPI: true,
57+
},
58+
},
59+
]);
60+
});
61+
62+
it('requires renderer.scheme to be a scheme name, not a URL', () => {
63+
expect(() =>
64+
setupMain({
65+
storage,
66+
renderer: {
67+
host: 'renderer',
68+
scheme: 'my-app://',
69+
},
70+
}),
71+
).toThrow('renderer.scheme must be a scheme name');
72+
});
73+
74+
it('requires renderer.host to be a host name, not an origin', () => {
75+
expect(() =>
76+
setupMain({
77+
storage,
78+
renderer: {
79+
host: 'my-app://renderer',
80+
scheme: 'my-app',
81+
},
82+
}),
83+
).toThrow('renderer.host must be a host name');
84+
});
85+
3686
it('returns a cleanup function for registered handlers', () => {
3787
const clerk = setupMain({ storage });
3888

‎packages/electron/src/main/setup-main.ts‎

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,22 @@
1+
import { protocol } from 'electron';
2+
13
import type { SetupMainOptions, SetupMainReturn } from '../shared/types';
24
import { setupTokenCacheIpcHandlers } from './ipc-handlers';
35

6+
function assertValidRendererOriginConfig(renderer: NonNullable<SetupMainOptions['renderer']>): void {
7+
if (renderer.scheme.includes(':') || renderer.scheme.includes('/')) {
8+
throw new Error(
9+
'Clerk: renderer.scheme must be a scheme name like "my-app", not a URL or protocol like "my-app://".',
10+
);
11+
}
12+
13+
if (renderer.host.includes(':') || renderer.host.includes('/')) {
14+
throw new Error(
15+
'Clerk: renderer.host must be a host name like "renderer", not a URL or origin like "my-app://renderer".',
16+
);
17+
}
18+
}
19+
420
export function setupMain(options: SetupMainOptions): SetupMainReturn {
521
if (!options.storage) {
622
throw new Error(
@@ -10,6 +26,23 @@ export function setupMain(options: SetupMainOptions): SetupMainReturn {
1026

1127
const cleanupTokenPersistence = setupTokenCacheIpcHandlers(options.storage);
1228

29+
if (options.renderer) {
30+
assertValidRendererOriginConfig(options.renderer);
31+
32+
protocol.registerSchemesAsPrivileged([
33+
{
34+
scheme: options.renderer.scheme,
35+
privileges: {
36+
standard: true,
37+
secure: true,
38+
supportFetchAPI: true,
39+
corsEnabled: true,
40+
stream: true,
41+
},
42+
},
43+
]);
44+
}
45+
1346
return {
1447
cleanup() {
1548
cleanupTokenPersistence();

‎packages/electron/src/shared/types.ts‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,27 @@ export type TokenStorage = {
88

99
export type SetupMainOptions = {
1010
storage: TokenStorage;
11+
/**
12+
* Registers the custom scheme used to serve the Electron renderer from a stable origin.
13+
*/
14+
renderer?: RendererSchemeOptions;
1115
};
1216

1317
export type SetupMainReturn = {
1418
cleanup: () => void;
1519
};
1620

21+
export type RendererSchemeOptions = {
22+
/**
23+
* Custom scheme used for the renderer origin.
24+
*/
25+
scheme: string;
26+
/**
27+
* Custom host used for the renderer origin.
28+
*/
29+
host: string;
30+
};
31+
1732
export type TokenCache = {
1833
getToken: (key: string) => Promise<string | null>;
1934
saveToken: (key: string, value: string) => Promise<void>;

0 commit comments

Comments
 (0)