@clerk/electron 0.0.1-snapshot.v20260617204522
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +254 -0
- package/dist/cjs/index.js +275 -0
- package/dist/cjs/index.js.map +1 -0
- package/dist/cjs/passkeys/index.js +267 -0
- package/dist/cjs/passkeys/index.js.map +1 -0
- package/dist/cjs/preload/index.js +65 -0
- package/dist/cjs/preload/index.js.map +1 -0
- package/dist/cjs/react/index.js +115 -0
- package/dist/cjs/react/index.js.map +1 -0
- package/dist/cjs/storage/index.js +133 -0
- package/dist/cjs/storage/index.js.map +1 -0
- package/dist/esm/chunk-ICCH5DQT.js +19 -0
- package/dist/esm/chunk-ICCH5DQT.js.map +1 -0
- package/dist/esm/index.js +254 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/passkeys/index.js +263 -0
- package/dist/esm/passkeys/index.js.map +1 -0
- package/dist/esm/preload/index.js +44 -0
- package/dist/esm/preload/index.js.map +1 -0
- package/dist/esm/react/index.js +107 -0
- package/dist/esm/react/index.js.map +1 -0
- package/dist/esm/storage/index.js +127 -0
- package/dist/esm/storage/index.js.map +1 -0
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/main/create-clerk-bridge.d.ts +10 -0
- package/dist/types/main/create-clerk-bridge.d.ts.map +1 -0
- package/dist/types/main/ipc-handlers.d.ts +3 -0
- package/dist/types/main/ipc-handlers.d.ts.map +1 -0
- package/dist/types/main/oauth-transport.d.ts +7 -0
- package/dist/types/main/oauth-transport.d.ts.map +1 -0
- package/dist/types/main/passkey-handlers.d.ts +4 -0
- package/dist/types/main/passkey-handlers.d.ts.map +1 -0
- package/dist/types/passkeys/index.d.ts +56 -0
- package/dist/types/passkeys/index.d.ts.map +1 -0
- package/dist/types/passkeys/preload.d.ts +3 -0
- package/dist/types/passkeys/preload.d.ts.map +1 -0
- package/dist/types/passkeys/renderer/native-bridge.d.ts +6 -0
- package/dist/types/passkeys/renderer/native-bridge.d.ts.map +1 -0
- package/dist/types/passkeys/renderer/strategy.d.ts +17 -0
- package/dist/types/passkeys/renderer/strategy.d.ts.map +1 -0
- package/dist/types/passkeys/shared/errors.d.ts +8 -0
- package/dist/types/passkeys/shared/errors.d.ts.map +1 -0
- package/dist/types/passkeys/shared/serialization.d.ts +11 -0
- package/dist/types/passkeys/shared/serialization.d.ts.map +1 -0
- package/dist/types/preload/index.d.ts +11 -0
- package/dist/types/preload/index.d.ts.map +1 -0
- package/dist/types/react/create-clerk-instance.d.ts +6 -0
- package/dist/types/react/create-clerk-instance.d.ts.map +1 -0
- package/dist/types/react/index.d.ts +25 -0
- package/dist/types/react/index.d.ts.map +1 -0
- package/dist/types/shared/ipc.d.ts +15 -0
- package/dist/types/shared/ipc.d.ts.map +1 -0
- package/dist/types/shared/types.d.ts +144 -0
- package/dist/types/shared/types.d.ts.map +1 -0
- package/dist/types/storage/index.d.ts +47 -0
- package/dist/types/storage/index.d.ts.map +1 -0
- package/package.json +137 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Clerk, Inc.
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,254 @@
|
|
|
1
|
+
<p align="center">
|
|
2
|
+
<a href="https://clerk.com?utm_source=github&utm_medium=clerk_electron" target="_blank" rel="noopener noreferrer">
|
|
3
|
+
<picture>
|
|
4
|
+
<source media="(prefers-color-scheme: dark)" srcset="https://images.clerk.com/static/logo-dark-mode-400x400.png">
|
|
5
|
+
<img src="https://images.clerk.com/static/logo-light-mode-400x400.png" height="64">
|
|
6
|
+
</picture>
|
|
7
|
+
</a>
|
|
8
|
+
<br />
|
|
9
|
+
<h1 align="center">@clerk/electron</h1>
|
|
10
|
+
</p>
|
|
11
|
+
|
|
12
|
+
<div align="center">
|
|
13
|
+
|
|
14
|
+
[](https://clerk.com/docs?utm_source=github&utm_medium=clerk_electron)
|
|
15
|
+
[](https://x.com/intent/follow?screen_name=clerk)
|
|
16
|
+
|
|
17
|
+
[Changelog](https://github.com/clerk/javascript/blob/main/packages/electron/CHANGELOG.md)
|
|
18
|
+
·
|
|
19
|
+
[Report a Bug](https://github.com/clerk/javascript/issues/new?assignees=&labels=needs-triage&projects=&template=BUG_REPORT.yml)
|
|
20
|
+
·
|
|
21
|
+
[Request a Feature](https://feedback.clerk.com/roadmap)
|
|
22
|
+
·
|
|
23
|
+
[Get help](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_electron)
|
|
24
|
+
|
|
25
|
+
</div>
|
|
26
|
+
|
|
27
|
+
## Getting Started
|
|
28
|
+
|
|
29
|
+
[Clerk](https://clerk.com/?utm_source=github&utm_medium=clerk_electron) is the easiest way to add authentication and user management to your Electron application.
|
|
30
|
+
|
|
31
|
+
> [!WARNING]
|
|
32
|
+
> `@clerk/electron` is under active development and is not yet ready for production use. The API is incomplete and subject to change.
|
|
33
|
+
|
|
34
|
+
This package exposes entrypoints for Electron's distinct runtime contexts:
|
|
35
|
+
|
|
36
|
+
- `@clerk/electron` — for use in the Electron **main** process.
|
|
37
|
+
- `@clerk/electron/preload` — for use in Electron **preload** scripts.
|
|
38
|
+
- `@clerk/electron/react` — for use in the Electron **renderer** process.
|
|
39
|
+
- `@clerk/electron/storage` — default token storage backed by `electron-store`.
|
|
40
|
+
- `@clerk/electron/passkeys` — passkey (WebAuthn) support for the **renderer** process.
|
|
41
|
+
|
|
42
|
+
```ts
|
|
43
|
+
// main.ts
|
|
44
|
+
import { app, BrowserWindow, net, protocol } from 'electron';
|
|
45
|
+
import { createClerkBridge } from '@clerk/electron';
|
|
46
|
+
import { storage } from '@clerk/electron/storage';
|
|
47
|
+
import { join } from 'node:path';
|
|
48
|
+
import { pathToFileURL } from 'node:url';
|
|
49
|
+
|
|
50
|
+
createClerkBridge({
|
|
51
|
+
storage: storage(),
|
|
52
|
+
renderer: {
|
|
53
|
+
scheme: 'my-app',
|
|
54
|
+
host: 'renderer',
|
|
55
|
+
},
|
|
56
|
+
passkeys: true,
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
app.whenReady().then(() => {
|
|
60
|
+
protocol.handle('my-app', request => {
|
|
61
|
+
const url = new URL(request.url);
|
|
62
|
+
const file = url.pathname === '/' ? 'index.html' : url.pathname;
|
|
63
|
+
|
|
64
|
+
return net.fetch(pathToFileURL(join(__dirname, '../renderer', file)).toString());
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const win = new BrowserWindow({
|
|
68
|
+
webPreferences: {
|
|
69
|
+
preload: join(__dirname, '../preload/index.js'),
|
|
70
|
+
},
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
win.loadURL('my-app://renderer/');
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
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.
|
|
78
|
+
|
|
79
|
+
```ts
|
|
80
|
+
// preload.ts
|
|
81
|
+
import { exposeClerkBridge } from '@clerk/electron/preload';
|
|
82
|
+
|
|
83
|
+
exposeClerkBridge({ passkeys: true });
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// renderer.tsx
|
|
88
|
+
import { ClerkProvider } from '@clerk/electron/react';
|
|
89
|
+
import { passkeys } from '@clerk/electron/passkeys';
|
|
90
|
+
|
|
91
|
+
<ClerkProvider
|
|
92
|
+
publishableKey={import.meta.env.VITE_CLERK_PUBLISHABLE_KEY}
|
|
93
|
+
passkeys={passkeys}
|
|
94
|
+
>
|
|
95
|
+
{/* ... */}
|
|
96
|
+
</ClerkProvider>;
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
## Content Security Policy
|
|
100
|
+
|
|
101
|
+
`@clerk/electron` loads Clerk's prebuilt UI from Clerk's CDN at runtime rather than bundling it, so your renderer's Content Security Policy must allow Clerk's Frontend API host. If it doesn't, the UI script fails to load and Clerk components never render.
|
|
102
|
+
|
|
103
|
+
Replace `{fapi_host}` below with your instance's **Frontend API** host, found in the [Clerk Dashboard](https://dashboard.clerk.com) under **API keys**. It includes a `clerk.` segment (for example, `clerk.your-app.com` in production or `your-slug.clerk.accounts.dev` in development). This is _not_ the Account Portal URL (`your-slug.accounts.dev`) — using that host blocks the UI script from loading.
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
default-src 'self';
|
|
107
|
+
script-src 'self' 'unsafe-inline' https://{fapi_host} https://challenges.cloudflare.com;
|
|
108
|
+
connect-src 'self' https://{fapi_host};
|
|
109
|
+
img-src 'self' https://img.clerk.com data:;
|
|
110
|
+
style-src 'self' 'unsafe-inline';
|
|
111
|
+
worker-src 'self' blob:;
|
|
112
|
+
frame-src 'self' https://challenges.cloudflare.com;
|
|
113
|
+
form-action 'self';
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
> [!NOTE]
|
|
117
|
+
> This covers sign-in/up and the hotloaded UI. If you use Clerk Billing (Stripe) or other features, you'll need to allow additional origins; see Clerk's [CSP guide](https://clerk.com/docs/guides/secure/best-practices/csp-headers) for the full list.
|
|
118
|
+
|
|
119
|
+
Apply it either with a `<meta>` tag in your renderer HTML:
|
|
120
|
+
|
|
121
|
+
```html
|
|
122
|
+
<meta
|
|
123
|
+
http-equiv="Content-Security-Policy"
|
|
124
|
+
content="default-src 'self'; script-src 'self' 'unsafe-inline' https://{fapi_host} https://challenges.cloudflare.com; connect-src 'self' https://{fapi_host}; img-src 'self' https://img.clerk.com data:; style-src 'self' 'unsafe-inline'; worker-src 'self' blob:; frame-src 'self' https://challenges.cloudflare.com; form-action 'self';"
|
|
125
|
+
/>
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
or, as [Electron's security guide](https://www.electronjs.org/docs/latest/tutorial/security#7-define-a-content-security-policy) recommends, as a response header from the main process:
|
|
129
|
+
|
|
130
|
+
```ts
|
|
131
|
+
// main.ts
|
|
132
|
+
import { app, session } from 'electron';
|
|
133
|
+
|
|
134
|
+
const fapiHost = 'clerk.your-app.com';
|
|
135
|
+
|
|
136
|
+
app.whenReady().then(() => {
|
|
137
|
+
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
|
138
|
+
callback({
|
|
139
|
+
responseHeaders: {
|
|
140
|
+
...details.responseHeaders,
|
|
141
|
+
'Content-Security-Policy': [
|
|
142
|
+
[
|
|
143
|
+
"default-src 'self'",
|
|
144
|
+
`script-src 'self' 'unsafe-inline' https://${fapiHost} https://challenges.cloudflare.com`,
|
|
145
|
+
`connect-src 'self' https://${fapiHost}`,
|
|
146
|
+
"img-src 'self' https://img.clerk.com data:",
|
|
147
|
+
"style-src 'self' 'unsafe-inline'",
|
|
148
|
+
"worker-src 'self' blob:",
|
|
149
|
+
"frame-src 'self' https://challenges.cloudflare.com",
|
|
150
|
+
"form-action 'self'",
|
|
151
|
+
].join('; '),
|
|
152
|
+
],
|
|
153
|
+
},
|
|
154
|
+
});
|
|
155
|
+
});
|
|
156
|
+
});
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
> [!NOTE]
|
|
160
|
+
> Loading the renderer from a dev server (such as Vite) requires looser rules for HMR: add `'unsafe-eval'` to `script-src` and your dev server's origin to `connect-src` (for example, `ws://localhost:<port> http://localhost:<port>`). Many apps skip CSP during development and apply it only to packaged builds.
|
|
161
|
+
|
|
162
|
+
## Passkeys
|
|
163
|
+
|
|
164
|
+
Passkey support works in two modes, selected automatically per request:
|
|
165
|
+
|
|
166
|
+
- **Renderer mode** — when your window loads content over `https://` from an origin that matches your passkey RP ID, the renderer's built-in Chromium WebAuthn is used. Credentials are synced by the OS/browser ecosystem (Windows Hello works out of the box; Touch ID on macOS requires Electron ≥ 42 and [`app.configureWebAuthn`](https://www.electronjs.org/docs/latest/api/app#appconfigurewebauthnoptions-macos)).
|
|
167
|
+
- **Native mode** — when your window loads a local bundle (`file://` or a custom protocol), WebAuthn's origin checks reject the request, so the ceremony is routed over IPC to the main process and serviced by the OS WebAuthn APIs (AuthenticationServices on macOS, `webauthn.dll` on Windows) via the optional [`@clerk/electron-passkeys`](https://github.com/clerk/javascript/tree/main/packages/electron-passkeys) native module.
|
|
168
|
+
|
|
169
|
+
### Setup
|
|
170
|
+
|
|
171
|
+
Native mode requires the optional native module:
|
|
172
|
+
|
|
173
|
+
```sh
|
|
174
|
+
pnpm add @clerk/electron-passkeys
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```ts
|
|
178
|
+
// main process
|
|
179
|
+
import { createClerkBridge } from '@clerk/electron';
|
|
180
|
+
import { storage } from '@clerk/electron/storage';
|
|
181
|
+
|
|
182
|
+
createClerkBridge({ storage: storage(), passkeys: true });
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
```ts
|
|
186
|
+
// preload script
|
|
187
|
+
import { exposeClerkBridge } from '@clerk/electron/preload';
|
|
188
|
+
|
|
189
|
+
exposeClerkBridge({ passkeys: true });
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
```tsx
|
|
193
|
+
// renderer process (React)
|
|
194
|
+
import { ClerkProvider } from '@clerk/electron/react';
|
|
195
|
+
import { passkeys } from '@clerk/electron/passkeys';
|
|
196
|
+
|
|
197
|
+
<ClerkProvider
|
|
198
|
+
publishableKey={publishableKey}
|
|
199
|
+
passkeys={passkeys}
|
|
200
|
+
>
|
|
201
|
+
{/* ... */}
|
|
202
|
+
</ClerkProvider>;
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Passkey code is only bundled and initialized when you pass the `passkeys` prop. If you manage the Clerk instance yourself instead of using `ClerkProvider`, wire it up before `clerk.load()`:
|
|
206
|
+
|
|
207
|
+
```ts
|
|
208
|
+
// renderer process (vanilla)
|
|
209
|
+
import { Clerk } from '@clerk/clerk-js';
|
|
210
|
+
import { createPasskeyProvider } from '@clerk/electron/passkeys';
|
|
211
|
+
|
|
212
|
+
const clerk = new Clerk(publishableKey);
|
|
213
|
+
createPasskeyProvider(clerk);
|
|
214
|
+
await clerk.load();
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
### macOS requirements for native mode
|
|
218
|
+
|
|
219
|
+
Like passkeys on iOS, the macOS platform APIs require a verified association between your app and your domain:
|
|
220
|
+
|
|
221
|
+
1. Serve an `apple-app-site-association` file from `https://<rp-domain>/.well-known/apple-app-site-association` (https, no redirect, `application/json`) listing your app: `{"webcredentials": {"apps": ["<TEAMID>.<bundle-id>"]}}`. The RP domain must be publicly reachable — Apple's CDN fetches it.
|
|
222
|
+
2. Sign your app with `com.apple.developer.associated-domains` containing `webcredentials:<rp-domain>`. This is a _restricted_ entitlement: the build must embed a provisioning profile with the Associated Domains capability for the bundle ID, and the entitlements must also include `com.apple.application-identifier` and `com.apple.developer.team-identifier` matching the profile.
|
|
223
|
+
|
|
224
|
+
Hard-won development-build checklist (each of these failure modes produces the same opaque "not associated with domain" error):
|
|
225
|
+
|
|
226
|
+
- Sign with an **Apple Development** identity (`mac.type: development` in electron-builder) and a **macOS App Development** profile that includes your Mac; also install the profile on the machine (`~/Library/Developer/Xcode/UserData/Provisioning Profiles/<UUID>.provisionprofile`).
|
|
227
|
+
- Copy `.app` bundles with `ditto`, never `cp -R` — `cp` breaks the bundle seal, and macOS silently ignores the entitlements of an app whose signature fails `codesign --verify --deep --strict`.
|
|
228
|
+
- The system registers the domain association via `swcd` when the app launches; verify with `sudo swcutil show`. If state gets stuck, `sudo swcutil reset` and relaunch.
|
|
229
|
+
- Prefer the default (production/CDN) association route. `?mode=developer` + `sudo swcutil developer-mode -e true` exists but is flaky in practice.
|
|
230
|
+
- The system log tells the truth: `log stream --predicate 'process == "swcd" OR composedMessage CONTAINS "your.domain"'` while launching, and look for `taskgated-helper: allowing entitlement(s) ... due to provisioning profile`.
|
|
231
|
+
|
|
232
|
+
Windows has no equivalent requirement. On Linux there is no native path; passkeys work in renderer mode only (including external security keys).
|
|
233
|
+
|
|
234
|
+
## Support
|
|
235
|
+
|
|
236
|
+
For help, visit our [support page](https://clerk.com/contact/support?utm_source=github&utm_medium=clerk_electron).
|
|
237
|
+
|
|
238
|
+
## Contributing
|
|
239
|
+
|
|
240
|
+
We're open to all community contributions! If you'd like to contribute in any way, please read [our contribution guidelines](https://github.com/clerk/javascript/blob/main/docs/CONTRIBUTING.md) and [code of conduct](https://github.com/clerk/javascript/blob/main/docs/CODE_OF_CONDUCT.md).
|
|
241
|
+
|
|
242
|
+
## Security
|
|
243
|
+
|
|
244
|
+
`@clerk/electron` follows good practices of security, but 100% security cannot be assured.
|
|
245
|
+
|
|
246
|
+
`@clerk/electron` is provided **"as is"** without any **warranty**. Use at your own risk.
|
|
247
|
+
|
|
248
|
+
_For more information and to report security issues, please refer to our [security documentation](https://github.com/clerk/javascript/blob/main/docs/SECURITY.md)._
|
|
249
|
+
|
|
250
|
+
## License
|
|
251
|
+
|
|
252
|
+
This project is licensed under the **MIT license**.
|
|
253
|
+
|
|
254
|
+
See [LICENSE](https://github.com/clerk/javascript/blob/main/packages/electron/LICENSE) for more information.
|
|
@@ -0,0 +1,275 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var electron = require('electron');
|
|
4
|
+
|
|
5
|
+
// src/main/create-clerk-bridge.ts
|
|
6
|
+
|
|
7
|
+
// src/shared/ipc.ts
|
|
8
|
+
var TOKEN_CACHE_CHANNELS = {
|
|
9
|
+
getToken: "clerk:token-cache:get",
|
|
10
|
+
saveToken: "clerk:token-cache:save",
|
|
11
|
+
clearToken: "clerk:token-cache:clear"
|
|
12
|
+
};
|
|
13
|
+
var OAUTH_TRANSPORT_CHANNELS = {
|
|
14
|
+
getRedirectUrl: "clerk:oauth-transport:get-redirect-url",
|
|
15
|
+
open: "clerk:oauth-transport:open"
|
|
16
|
+
};
|
|
17
|
+
var PASSKEY_CHANNELS = {
|
|
18
|
+
create: "clerk:passkeys:create",
|
|
19
|
+
get: "clerk:passkeys:get",
|
|
20
|
+
capabilities: "clerk:passkeys:capabilities"
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
// src/main/ipc-handlers.ts
|
|
24
|
+
function setupTokenCacheIpcHandlers(storage) {
|
|
25
|
+
electron.ipcMain.handle(TOKEN_CACHE_CHANNELS.getToken, (_event, key) => {
|
|
26
|
+
return storage.getItem(key);
|
|
27
|
+
});
|
|
28
|
+
electron.ipcMain.handle(TOKEN_CACHE_CHANNELS.saveToken, (_event, key, value) => {
|
|
29
|
+
return storage.setItem(key, value);
|
|
30
|
+
});
|
|
31
|
+
electron.ipcMain.handle(TOKEN_CACHE_CHANNELS.clearToken, (_event, key) => {
|
|
32
|
+
return storage.removeItem(key);
|
|
33
|
+
});
|
|
34
|
+
return () => {
|
|
35
|
+
electron.ipcMain.removeHandler(TOKEN_CACHE_CHANNELS.getToken);
|
|
36
|
+
electron.ipcMain.removeHandler(TOKEN_CACHE_CHANNELS.saveToken);
|
|
37
|
+
electron.ipcMain.removeHandler(TOKEN_CACHE_CHANNELS.clearToken);
|
|
38
|
+
};
|
|
39
|
+
}
|
|
40
|
+
var CALLBACK_TIMEOUT_MS = 3 * 60 * 1e3;
|
|
41
|
+
function buildRedirectUrl(options) {
|
|
42
|
+
return `${options.renderer.scheme}://${options.renderer.host}/`;
|
|
43
|
+
}
|
|
44
|
+
function isMatchingCallbackUrl(url, redirectUrl) {
|
|
45
|
+
try {
|
|
46
|
+
const callback = new URL(url);
|
|
47
|
+
const expected = new URL(redirectUrl);
|
|
48
|
+
return callback.protocol === expected.protocol && callback.host === expected.host && callback.pathname === expected.pathname;
|
|
49
|
+
} catch {
|
|
50
|
+
return false;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
function assertExternalOAuthUrl(url) {
|
|
54
|
+
const parsedUrl = new URL(url);
|
|
55
|
+
if (parsedUrl.protocol !== "http:" && parsedUrl.protocol !== "https:") {
|
|
56
|
+
throw new TypeError(`Clerk: refusing to open unsupported OAuth URL protocol: ${parsedUrl.protocol}`);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
function setupOAuthTransportIpcHandlers(options) {
|
|
60
|
+
const redirectUrl = buildRedirectUrl(options);
|
|
61
|
+
let pendingOAuthFlow = null;
|
|
62
|
+
const disposePendingOAuthFlow = (reason) => {
|
|
63
|
+
if (!pendingOAuthFlow) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
const pending = pendingOAuthFlow;
|
|
67
|
+
clearTimeout(pendingOAuthFlow.timeout);
|
|
68
|
+
pendingOAuthFlow = null;
|
|
69
|
+
if (reason) {
|
|
70
|
+
pending.reject(reason);
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
const handleCallbackUrl = (url) => {
|
|
74
|
+
if (!pendingOAuthFlow || !isMatchingCallbackUrl(url, redirectUrl)) {
|
|
75
|
+
return;
|
|
76
|
+
}
|
|
77
|
+
const pending = pendingOAuthFlow;
|
|
78
|
+
disposePendingOAuthFlow();
|
|
79
|
+
pending.resolve({ callbackUrl: url });
|
|
80
|
+
};
|
|
81
|
+
const openUrlListener = (event, url) => {
|
|
82
|
+
if (!isMatchingCallbackUrl(url, redirectUrl)) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
event.preventDefault();
|
|
86
|
+
handleCallbackUrl(url);
|
|
87
|
+
};
|
|
88
|
+
const secondInstanceListener = (_event, argv) => {
|
|
89
|
+
const callbackUrl = argv.find((url) => isMatchingCallbackUrl(url, redirectUrl));
|
|
90
|
+
if (callbackUrl) {
|
|
91
|
+
handleCallbackUrl(callbackUrl);
|
|
92
|
+
}
|
|
93
|
+
};
|
|
94
|
+
electron.app.setAsDefaultProtocolClient(options.renderer.scheme);
|
|
95
|
+
electron.app.on("open-url", openUrlListener);
|
|
96
|
+
electron.app.on("second-instance", secondInstanceListener);
|
|
97
|
+
electron.ipcMain.handle(OAUTH_TRANSPORT_CHANNELS.getRedirectUrl, () => {
|
|
98
|
+
return redirectUrl;
|
|
99
|
+
});
|
|
100
|
+
electron.ipcMain.handle(OAUTH_TRANSPORT_CHANNELS.open, async (_event, url) => {
|
|
101
|
+
if (pendingOAuthFlow) {
|
|
102
|
+
throw new Error("Clerk: an OAuth flow is already pending.");
|
|
103
|
+
}
|
|
104
|
+
assertExternalOAuthUrl(url);
|
|
105
|
+
const callbackPromise = new Promise((resolve, reject) => {
|
|
106
|
+
const timeout = setTimeout(() => {
|
|
107
|
+
disposePendingOAuthFlow();
|
|
108
|
+
reject(new Error("Clerk: OAuth callback timed out."));
|
|
109
|
+
}, CALLBACK_TIMEOUT_MS);
|
|
110
|
+
pendingOAuthFlow = { resolve, reject, timeout };
|
|
111
|
+
});
|
|
112
|
+
try {
|
|
113
|
+
await electron.shell.openExternal(url);
|
|
114
|
+
} catch (err) {
|
|
115
|
+
disposePendingOAuthFlow(err instanceof Error ? err : new Error(String(err)));
|
|
116
|
+
}
|
|
117
|
+
return callbackPromise;
|
|
118
|
+
});
|
|
119
|
+
return () => {
|
|
120
|
+
disposePendingOAuthFlow(new Error("Clerk: OAuth flow was cancelled."));
|
|
121
|
+
electron.app.removeListener("open-url", openUrlListener);
|
|
122
|
+
electron.app.removeListener("second-instance", secondInstanceListener);
|
|
123
|
+
electron.ipcMain.removeHandler(OAUTH_TRANSPORT_CHANNELS.getRedirectUrl);
|
|
124
|
+
electron.ipcMain.removeHandler(OAUTH_TRANSPORT_CHANNELS.open);
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
var nativeModulePromise;
|
|
128
|
+
function loadNativeModule() {
|
|
129
|
+
nativeModulePromise != null ? nativeModulePromise : nativeModulePromise = import('@clerk/electron-passkeys').then(
|
|
130
|
+
(module) => {
|
|
131
|
+
var _a;
|
|
132
|
+
return (_a = module.default) != null ? _a : module;
|
|
133
|
+
},
|
|
134
|
+
(error) => {
|
|
135
|
+
nativeModulePromise = void 0;
|
|
136
|
+
throw new Error(
|
|
137
|
+
"Clerk: createClerkBridge({ passkeys: true }) requires the optional @clerk/electron-passkeys package. Install it with your package manager to enable native passkey support.",
|
|
138
|
+
{ cause: error }
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
);
|
|
142
|
+
return nativeModulePromise;
|
|
143
|
+
}
|
|
144
|
+
var NATIVE_ERROR_CODES = ["cancelled", "invalid_rp", "not_supported", "timeout", "unknown"];
|
|
145
|
+
function isPasskeyIpcResult(value) {
|
|
146
|
+
var _a;
|
|
147
|
+
if (!value || typeof value !== "object" || typeof value.ok !== "boolean") {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
const result = value;
|
|
151
|
+
return result.ok ? result.credential !== void 0 : NATIVE_ERROR_CODES.includes((_a = result.error) == null ? void 0 : _a.code);
|
|
152
|
+
}
|
|
153
|
+
async function invokeNative(method, event, options) {
|
|
154
|
+
let native;
|
|
155
|
+
try {
|
|
156
|
+
native = await loadNativeModule();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
return {
|
|
159
|
+
ok: false,
|
|
160
|
+
error: { code: "not_supported", message: error instanceof Error ? error.message : String(error) }
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
if (!native.isAvailable()) {
|
|
164
|
+
return {
|
|
165
|
+
ok: false,
|
|
166
|
+
error: { code: "not_supported", message: "Native passkeys are not supported on this platform." }
|
|
167
|
+
};
|
|
168
|
+
}
|
|
169
|
+
if (!event.senderFrame || event.senderFrame !== event.sender.mainFrame) {
|
|
170
|
+
return {
|
|
171
|
+
ok: false,
|
|
172
|
+
error: { code: "unknown", message: "The passkey request did not originate from a window's main frame." }
|
|
173
|
+
};
|
|
174
|
+
}
|
|
175
|
+
const window = electron.BrowserWindow.fromWebContents(event.sender);
|
|
176
|
+
if (!window) {
|
|
177
|
+
return {
|
|
178
|
+
ok: false,
|
|
179
|
+
error: { code: "unknown", message: "The passkey request did not originate from a visible window." }
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
try {
|
|
183
|
+
const resultJson = await native[method](window.getNativeWindowHandle(), JSON.stringify(options));
|
|
184
|
+
const result = JSON.parse(resultJson);
|
|
185
|
+
if (!isPasskeyIpcResult(result)) {
|
|
186
|
+
return { ok: false, error: { code: "unknown", message: "The native module returned an unexpected result." } };
|
|
187
|
+
}
|
|
188
|
+
return result;
|
|
189
|
+
} catch (error) {
|
|
190
|
+
return { ok: false, error: { code: "unknown", message: error instanceof Error ? error.message : String(error) } };
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
function setupPasskeysMain() {
|
|
194
|
+
loadNativeModule().catch((error) => console.warn(error.message));
|
|
195
|
+
electron.ipcMain.handle(
|
|
196
|
+
PASSKEY_CHANNELS.create,
|
|
197
|
+
(event, options) => invokeNative("createCredential", event, options)
|
|
198
|
+
);
|
|
199
|
+
electron.ipcMain.handle(
|
|
200
|
+
PASSKEY_CHANNELS.get,
|
|
201
|
+
(event, options) => invokeNative("getCredential", event, options)
|
|
202
|
+
);
|
|
203
|
+
electron.ipcMain.handle(PASSKEY_CHANNELS.capabilities, async () => {
|
|
204
|
+
try {
|
|
205
|
+
const native = await loadNativeModule();
|
|
206
|
+
if (!native.isAvailable()) {
|
|
207
|
+
return { available: false, platformAuthenticator: false, securityKeys: false };
|
|
208
|
+
}
|
|
209
|
+
return { available: true, ...native.capabilities() };
|
|
210
|
+
} catch {
|
|
211
|
+
return { available: false, platformAuthenticator: false, securityKeys: false };
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
return {
|
|
215
|
+
cleanup() {
|
|
216
|
+
electron.ipcMain.removeHandler(PASSKEY_CHANNELS.create);
|
|
217
|
+
electron.ipcMain.removeHandler(PASSKEY_CHANNELS.get);
|
|
218
|
+
electron.ipcMain.removeHandler(PASSKEY_CHANNELS.capabilities);
|
|
219
|
+
}
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// src/main/create-clerk-bridge.ts
|
|
224
|
+
function assertValidRendererOriginConfig(renderer) {
|
|
225
|
+
if (renderer.scheme.includes(":") || renderer.scheme.includes("/")) {
|
|
226
|
+
throw new Error(
|
|
227
|
+
'Clerk: renderer.scheme must be a scheme name like "my-app", not a URL or protocol like "my-app://".'
|
|
228
|
+
);
|
|
229
|
+
}
|
|
230
|
+
if (renderer.host.includes(":") || renderer.host.includes("/")) {
|
|
231
|
+
throw new Error(
|
|
232
|
+
'Clerk: renderer.host must be a host name like "renderer", not a URL or origin like "my-app://renderer".'
|
|
233
|
+
);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function createClerkBridge(options) {
|
|
237
|
+
if (!options.storage) {
|
|
238
|
+
throw new Error(
|
|
239
|
+
"Clerk: createClerkBridge requires a storage adapter. Pass createClerkBridge({ storage: storage() }) from @clerk/electron/storage, or provide a custom storage adapter."
|
|
240
|
+
);
|
|
241
|
+
}
|
|
242
|
+
const cleanupTokenPersistence = setupTokenCacheIpcHandlers(options.storage);
|
|
243
|
+
let cleanupOAuthTransport;
|
|
244
|
+
const passkeys = options.passkeys ? setupPasskeysMain() : null;
|
|
245
|
+
if (options.renderer) {
|
|
246
|
+
assertValidRendererOriginConfig(options.renderer);
|
|
247
|
+
electron.protocol.registerSchemesAsPrivileged([
|
|
248
|
+
{
|
|
249
|
+
scheme: options.renderer.scheme,
|
|
250
|
+
privileges: {
|
|
251
|
+
standard: true,
|
|
252
|
+
secure: true,
|
|
253
|
+
supportFetchAPI: true,
|
|
254
|
+
corsEnabled: true,
|
|
255
|
+
stream: true
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
]);
|
|
259
|
+
cleanupOAuthTransport = setupOAuthTransportIpcHandlers({
|
|
260
|
+
renderer: options.renderer
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
return {
|
|
264
|
+
cleanup() {
|
|
265
|
+
cleanupTokenPersistence();
|
|
266
|
+
cleanupOAuthTransport == null ? void 0 : cleanupOAuthTransport();
|
|
267
|
+
passkeys == null ? void 0 : passkeys.cleanup();
|
|
268
|
+
}
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
exports.createClerkBridge = createClerkBridge;
|
|
273
|
+
exports.setupPasskeysMain = setupPasskeysMain;
|
|
274
|
+
//# sourceMappingURL=index.js.map
|
|
275
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/shared/ipc.ts","../../src/main/ipc-handlers.ts","../../src/main/oauth-transport.ts","../../src/main/passkey-handlers.ts","../../src/main/create-clerk-bridge.ts"],"names":["ipcMain","app","shell","BrowserWindow","protocol"],"mappings":"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","file":"index.js","sourcesContent":["export const TOKEN_CACHE_CHANNELS = {\n getToken: 'clerk:token-cache:get',\n saveToken: 'clerk:token-cache:save',\n clearToken: 'clerk:token-cache:clear',\n} as const;\n\nexport const OAUTH_TRANSPORT_CHANNELS = {\n getRedirectUrl: 'clerk:oauth-transport:get-redirect-url',\n open: 'clerk:oauth-transport:open',\n} as const;\n\nexport const PASSKEY_CHANNELS = {\n create: 'clerk:passkeys:create',\n get: 'clerk:passkeys:get',\n capabilities: 'clerk:passkeys:capabilities',\n} as const;\n","import { ipcMain } from 'electron';\n\nimport { TOKEN_CACHE_CHANNELS } from '../shared/ipc';\nimport type { TokenStorage } from '../shared/types';\n\nexport function setupTokenCacheIpcHandlers(storage: TokenStorage): () => void {\n ipcMain.handle(TOKEN_CACHE_CHANNELS.getToken, (_event, key: string) => {\n return storage.getItem(key);\n });\n\n ipcMain.handle(TOKEN_CACHE_CHANNELS.saveToken, (_event, key: string, value: string) => {\n return storage.setItem(key, value);\n });\n\n ipcMain.handle(TOKEN_CACHE_CHANNELS.clearToken, (_event, key: string) => {\n return storage.removeItem(key);\n });\n\n return () => {\n ipcMain.removeHandler(TOKEN_CACHE_CHANNELS.getToken);\n ipcMain.removeHandler(TOKEN_CACHE_CHANNELS.saveToken);\n ipcMain.removeHandler(TOKEN_CACHE_CHANNELS.clearToken);\n };\n}\n","import { app, ipcMain, shell } from 'electron';\n\nimport { OAUTH_TRANSPORT_CHANNELS } from '../shared/ipc';\nimport type { RendererSchemeOptions } from '../shared/types';\n\nconst CALLBACK_TIMEOUT_MS = 3 * 60 * 1000;\n\ntype PendingOAuthFlow = {\n resolve: (value: { callbackUrl: string }) => void;\n reject: (reason: Error) => void;\n timeout: NodeJS.Timeout;\n};\n\ntype OAuthTransportOptions = {\n renderer: RendererSchemeOptions;\n};\n\nfunction buildRedirectUrl(options: OAuthTransportOptions): string {\n return `${options.renderer.scheme}://${options.renderer.host}/`;\n}\n\nfunction isMatchingCallbackUrl(url: string, redirectUrl: string): boolean {\n try {\n const callback = new URL(url);\n const expected = new URL(redirectUrl);\n\n return (\n callback.protocol === expected.protocol &&\n callback.host === expected.host &&\n callback.pathname === expected.pathname\n );\n } catch {\n return false;\n }\n}\n\nfunction assertExternalOAuthUrl(url: string): void {\n const parsedUrl = new URL(url);\n\n if (parsedUrl.protocol !== 'http:' && parsedUrl.protocol !== 'https:') {\n throw new TypeError(`Clerk: refusing to open unsupported OAuth URL protocol: ${parsedUrl.protocol}`);\n }\n}\n\nexport function setupOAuthTransportIpcHandlers(options: OAuthTransportOptions): () => void {\n const redirectUrl = buildRedirectUrl(options);\n let pendingOAuthFlow: PendingOAuthFlow | null = null;\n\n const disposePendingOAuthFlow = (reason?: Error): void => {\n if (!pendingOAuthFlow) {\n return;\n }\n\n const pending = pendingOAuthFlow;\n clearTimeout(pendingOAuthFlow.timeout);\n pendingOAuthFlow = null;\n\n if (reason) {\n pending.reject(reason);\n }\n };\n\n const handleCallbackUrl = (url: string): void => {\n if (!pendingOAuthFlow || !isMatchingCallbackUrl(url, redirectUrl)) {\n return;\n }\n\n const pending = pendingOAuthFlow;\n disposePendingOAuthFlow();\n pending.resolve({ callbackUrl: url });\n };\n\n const openUrlListener = (event: Electron.Event, url: string): void => {\n if (!isMatchingCallbackUrl(url, redirectUrl)) {\n return;\n }\n\n event.preventDefault();\n handleCallbackUrl(url);\n };\n\n const secondInstanceListener = (_event: Electron.Event, argv: string[]): void => {\n const callbackUrl = argv.find(url => isMatchingCallbackUrl(url, redirectUrl));\n\n if (callbackUrl) {\n handleCallbackUrl(callbackUrl);\n }\n };\n\n app.setAsDefaultProtocolClient(options.renderer.scheme);\n app.on('open-url', openUrlListener);\n app.on('second-instance', secondInstanceListener);\n\n ipcMain.handle(OAUTH_TRANSPORT_CHANNELS.getRedirectUrl, () => {\n return redirectUrl;\n });\n\n ipcMain.handle(OAUTH_TRANSPORT_CHANNELS.open, async (_event, url: string) => {\n if (pendingOAuthFlow) {\n throw new Error('Clerk: an OAuth flow is already pending.');\n }\n\n assertExternalOAuthUrl(url);\n\n const callbackPromise = new Promise<{ callbackUrl: string }>((resolve, reject) => {\n const timeout = setTimeout(() => {\n disposePendingOAuthFlow();\n reject(new Error('Clerk: OAuth callback timed out.'));\n }, CALLBACK_TIMEOUT_MS);\n\n pendingOAuthFlow = { resolve, reject, timeout };\n });\n\n try {\n await shell.openExternal(url);\n } catch (err) {\n disposePendingOAuthFlow(err instanceof Error ? err : new Error(String(err)));\n }\n\n return callbackPromise;\n });\n\n return () => {\n disposePendingOAuthFlow(new Error('Clerk: OAuth flow was cancelled.'));\n app.removeListener('open-url', openUrlListener);\n app.removeListener('second-instance', secondInstanceListener);\n ipcMain.removeHandler(OAUTH_TRANSPORT_CHANNELS.getRedirectUrl);\n ipcMain.removeHandler(OAUTH_TRANSPORT_CHANNELS.open);\n };\n}\n","import type { IpcMainInvokeEvent } from 'electron';\nimport { BrowserWindow, ipcMain } from 'electron';\n\nimport { PASSKEY_CHANNELS } from '../shared/ipc';\nimport type {\n AuthenticationResponseJSON,\n PasskeyCapabilities,\n PasskeyIpcResult,\n PasskeyNativeErrorCode,\n RegistrationResponseJSON,\n SerializedPublicKeyCredentialCreationOptions,\n SerializedPublicKeyCredentialRequestOptions,\n SetupPasskeysMainReturn,\n} from '../shared/types';\n\n/**\n * Optional native module. Ceremony failures resolve as JSON envelopes so error\n * codes survive both the FFI and Electron IPC boundaries.\n */\ntype NativePasskeysModule = {\n isAvailable: () => boolean;\n capabilities: () => Omit<PasskeyCapabilities, 'available'>;\n createCredential: (windowHandle: Buffer, optionsJson: string) => Promise<string>;\n getCredential: (windowHandle: Buffer, optionsJson: string) => Promise<string>;\n};\n\nlet nativeModulePromise: Promise<NativePasskeysModule> | undefined;\n\nfunction loadNativeModule(): Promise<NativePasskeysModule> {\n // Keep the native module optional for apps that do not use passkeys.\n nativeModulePromise ??= import('@clerk/electron-passkeys').then(\n (module: { default?: NativePasskeysModule } & NativePasskeysModule) => module.default ?? module,\n error => {\n nativeModulePromise = undefined;\n throw new Error(\n 'Clerk: createClerkBridge({ passkeys: true }) requires the optional @clerk/electron-passkeys package. Install it with your package manager to enable native passkey support.',\n { cause: error },\n );\n },\n );\n return nativeModulePromise;\n}\n\nconst NATIVE_ERROR_CODES: PasskeyNativeErrorCode[] = ['cancelled', 'invalid_rp', 'not_supported', 'timeout', 'unknown'];\n\nfunction isPasskeyIpcResult<T>(value: unknown): value is PasskeyIpcResult<T> {\n if (!value || typeof value !== 'object' || typeof (value as { ok?: unknown }).ok !== 'boolean') {\n return false;\n }\n const result = value as { ok: boolean; credential?: unknown; error?: { code?: unknown } };\n return result.ok\n ? result.credential !== undefined\n : NATIVE_ERROR_CODES.includes(result.error?.code as PasskeyNativeErrorCode);\n}\n\nasync function invokeNative<T>(\n method: 'createCredential' | 'getCredential',\n event: IpcMainInvokeEvent,\n options: SerializedPublicKeyCredentialCreationOptions | SerializedPublicKeyCredentialRequestOptions,\n): Promise<PasskeyIpcResult<T>> {\n let native: NativePasskeysModule;\n try {\n native = await loadNativeModule();\n } catch (error) {\n return {\n ok: false,\n error: { code: 'not_supported', message: error instanceof Error ? error.message : String(error) },\n };\n }\n\n if (!native.isAvailable()) {\n return {\n ok: false,\n error: { code: 'not_supported', message: 'Native passkeys are not supported on this platform.' },\n };\n }\n\n // Subframes and webviews can host third-party content that must not be able\n // to run credential ceremonies for the app's RP ID.\n if (!event.senderFrame || event.senderFrame !== event.sender.mainFrame) {\n return {\n ok: false,\n error: { code: 'unknown', message: \"The passkey request did not originate from a window's main frame.\" },\n };\n }\n\n const window = BrowserWindow.fromWebContents(event.sender);\n if (!window) {\n return {\n ok: false,\n error: { code: 'unknown', message: 'The passkey request did not originate from a visible window.' },\n };\n }\n\n try {\n const resultJson = await native[method](window.getNativeWindowHandle(), JSON.stringify(options));\n const result: unknown = JSON.parse(resultJson);\n if (!isPasskeyIpcResult<T>(result)) {\n return { ok: false, error: { code: 'unknown', message: 'The native module returned an unexpected result.' } };\n }\n return result;\n } catch (error) {\n return { ok: false, error: { code: 'unknown', message: error instanceof Error ? error.message : String(error) } };\n }\n}\n\n/** Registers IPC handlers for native platform WebAuthn. */\nexport function setupPasskeysMain(): SetupPasskeysMainReturn {\n // Surface a missing optional dependency during setup, before the first ceremony.\n loadNativeModule().catch((error: Error) => console.warn(error.message));\n\n ipcMain.handle(\n PASSKEY_CHANNELS.create,\n (\n event,\n options: SerializedPublicKeyCredentialCreationOptions,\n ): Promise<PasskeyIpcResult<RegistrationResponseJSON>> => invokeNative('createCredential', event, options),\n );\n\n ipcMain.handle(\n PASSKEY_CHANNELS.get,\n (\n event,\n options: SerializedPublicKeyCredentialRequestOptions,\n ): Promise<PasskeyIpcResult<AuthenticationResponseJSON>> => invokeNative('getCredential', event, options),\n );\n\n ipcMain.handle(PASSKEY_CHANNELS.capabilities, async (): Promise<PasskeyCapabilities> => {\n try {\n const native = await loadNativeModule();\n if (!native.isAvailable()) {\n return { available: false, platformAuthenticator: false, securityKeys: false };\n }\n return { available: true, ...native.capabilities() };\n } catch {\n return { available: false, platformAuthenticator: false, securityKeys: false };\n }\n });\n\n return {\n cleanup() {\n ipcMain.removeHandler(PASSKEY_CHANNELS.create);\n ipcMain.removeHandler(PASSKEY_CHANNELS.get);\n ipcMain.removeHandler(PASSKEY_CHANNELS.capabilities);\n },\n };\n}\n","import { protocol } from 'electron';\n\nimport type { ClerkBridge, CreateClerkBridgeOptions } from '../shared/types';\nimport { setupTokenCacheIpcHandlers } from './ipc-handlers';\nimport { setupOAuthTransportIpcHandlers } from './oauth-transport';\nimport { setupPasskeysMain } from './passkey-handlers';\n\nfunction assertValidRendererOriginConfig(renderer: NonNullable<CreateClerkBridgeOptions['renderer']>): void {\n if (renderer.scheme.includes(':') || renderer.scheme.includes('/')) {\n throw new Error(\n 'Clerk: renderer.scheme must be a scheme name like \"my-app\", not a URL or protocol like \"my-app://\".',\n );\n }\n\n if (renderer.host.includes(':') || renderer.host.includes('/')) {\n throw new Error(\n 'Clerk: renderer.host must be a host name like \"renderer\", not a URL or origin like \"my-app://renderer\".',\n );\n }\n}\n\n/**\n * Creates the Clerk bridge for Electron's main process.\n *\n * The bridge owns Clerk's main-process IPC handlers, token persistence, and OAuth deep-link\n * transport. Call this before creating renderer windows, and call the returned `cleanup` method\n * when tearing down the app or test environment.\n */\nexport function createClerkBridge(options: CreateClerkBridgeOptions): ClerkBridge {\n if (!options.storage) {\n throw new Error(\n 'Clerk: createClerkBridge requires a storage adapter. Pass createClerkBridge({ storage: storage() }) from @clerk/electron/storage, or provide a custom storage adapter.',\n );\n }\n\n const cleanupTokenPersistence = setupTokenCacheIpcHandlers(options.storage);\n let cleanupOAuthTransport: (() => void) | undefined;\n const passkeys = options.passkeys ? setupPasskeysMain() : null;\n\n if (options.renderer) {\n assertValidRendererOriginConfig(options.renderer);\n\n protocol.registerSchemesAsPrivileged([\n {\n scheme: options.renderer.scheme,\n privileges: {\n standard: true,\n secure: true,\n supportFetchAPI: true,\n corsEnabled: true,\n stream: true,\n },\n },\n ]);\n\n cleanupOAuthTransport = setupOAuthTransportIpcHandlers({\n renderer: options.renderer,\n });\n }\n\n return {\n cleanup() {\n cleanupTokenPersistence();\n cleanupOAuthTransport?.();\n passkeys?.cleanup();\n },\n };\n}\n"]}
|