@soorya-u/better-auth-desktop 0.1.0
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/README.md +257 -0
- package/dist/adapters/electrobun.d.mts +49 -0
- package/dist/adapters/electrobun.mjs +166 -0
- package/dist/adapters/electron.d.mts +55 -0
- package/dist/adapters/electron.mjs +205 -0
- package/dist/client-De_zQzE1.d.mts +34 -0
- package/dist/client-DhRcoap2.mjs +157 -0
- package/dist/client.d.mts +3 -0
- package/dist/client.mjs +2 -0
- package/dist/core/index.d.mts +6 -0
- package/dist/core/index.mjs +5 -0
- package/dist/exchange-DCZ0j_T5.mjs +217 -0
- package/dist/index-CP_2Wlut.d.mts +61 -0
- package/dist/index.d.mts +10 -0
- package/dist/index.mjs +7 -0
- package/dist/loopback-ClkAoS2u.d.mts +13 -0
- package/dist/loopback-DTQgg1Za.mjs +35 -0
- package/dist/rpc/webview.d.mts +26 -0
- package/dist/rpc/webview.mjs +325 -0
- package/dist/server/index.d.mts +60 -0
- package/dist/server/index.mjs +213 -0
- package/dist/storage-BcwDmS-W.mjs +30 -0
- package/dist/storage-DtgX_vmE.d.mts +10 -0
- package/dist/types-h9AEfSnq.d.mts +75 -0
- package/dist/version-1hEssvcU.mjs +5 -0
- package/dist/web/index.d.mts +32 -0
- package/dist/web/index.mjs +41 -0
- package/package.json +153 -0
package/README.md
ADDED
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
# @soorya-u/better-auth-desktop
|
|
2
|
+
|
|
3
|
+
Loopback-based desktop authentication for [Better Auth](https://better-auth.com).
|
|
4
|
+
|
|
5
|
+
OAuth completes in the **system browser** and the resulting session is handed
|
|
6
|
+
back to the desktop app through a **`127.0.0.1` loopback** the app briefly
|
|
7
|
+
listens on — reached by a **top-level browser navigation**, not a custom URL
|
|
8
|
+
scheme. That works on **macOS, Linux, and Windows** with **no OS scheme
|
|
9
|
+
registration**, and avoids the CORS / Private-Network-Access / mixed-content
|
|
10
|
+
problems a `fetch`-based hand-off would hit.
|
|
11
|
+
|
|
12
|
+
A framework-agnostic **server plugin** and **core** drive the flow; thin
|
|
13
|
+
adapters bind it to a runtime. **Electrobun** ships today; **Electron** is a
|
|
14
|
+
drop-in (it never loads any Electrobun code).
|
|
15
|
+
|
|
16
|
+
## How it works
|
|
17
|
+
|
|
18
|
+
```
|
|
19
|
+
WebView (renderer) Desktop main process Better Auth server
|
|
20
|
+
│ requestAuth(github) ─────▶│ │
|
|
21
|
+
│ │ bind 127.0.0.1:<port> │
|
|
22
|
+
│ │ openExternal( │
|
|
23
|
+
│ │ /desktop/init-oauth-proxy │
|
|
24
|
+
│ │ ?provider&pkce&callbackURL=http://127.0.0.1:<port>/callback?nonce=N)
|
|
25
|
+
│ │ │
|
|
26
|
+
[ system browser ] ── OAuth ──▶ provider ──▶ server callback ──▶ session established
|
|
27
|
+
│ │ /desktop/oauth-complete:
|
|
28
|
+
│ │ mint one-time code; 302 →
|
|
29
|
+
│ │ http://127.0.0.1:<port>/callback?nonce=N&token=T
|
|
30
|
+
[ system browser ] ── navigates to 127.0.0.1:<port>/callback?token=T&nonce=N
|
|
31
|
+
│ │ loopback handler: │
|
|
32
|
+
│ │ verify nonce │
|
|
33
|
+
│ │ exchange T ────────────────────▶│ /desktop/token (PKCE verify)
|
|
34
|
+
│ │ ◀── session ────────────────────│
|
|
35
|
+
│ │ store session (keychain) │
|
|
36
|
+
│◀── onAuthenticated ───────┤ "you can close this tab" │
|
|
37
|
+
│ navigate("/app") │ close loopback │
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
The one-time code in the loopback URL is **single-use** and **PKCE-bound** (the
|
|
41
|
+
verifier never leaves the desktop), the listener binds **`127.0.0.1` only**, the
|
|
42
|
+
request must carry the matching **nonce**, and the loopback closes after success
|
|
43
|
+
or a timeout. Works in dev (with `oAuthProxy` bouncing through
|
|
44
|
+
`/oauth-proxy-callback`) and prod (direct callback) alike.
|
|
45
|
+
|
|
46
|
+
## Installation
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
bun add better-auth @soorya-u/better-auth-desktop
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
## Server
|
|
53
|
+
|
|
54
|
+
Add the plugin to your Better Auth server. Compose it with `oAuthProxy` if your
|
|
55
|
+
desktop/web origins differ from the OAuth-registered callback origin.
|
|
56
|
+
|
|
57
|
+
```ts
|
|
58
|
+
// server/auth.ts
|
|
59
|
+
import { betterAuth } from "better-auth";
|
|
60
|
+
import { betterAuthDesktop } from "@soorya-u/better-auth-desktop/server";
|
|
61
|
+
import { oAuthProxy } from "better-auth/plugins";
|
|
62
|
+
|
|
63
|
+
export const auth = betterAuth({
|
|
64
|
+
socialProviders: {
|
|
65
|
+
github: {
|
|
66
|
+
clientId: process.env.OAUTH_GITHUB_CLIENT_ID!,
|
|
67
|
+
clientSecret: process.env.OAUTH_GITHUB_CLIENT_SECRET!,
|
|
68
|
+
},
|
|
69
|
+
},
|
|
70
|
+
plugins: [
|
|
71
|
+
betterAuthDesktop({ clientID: "my-desktop-app" }),
|
|
72
|
+
oAuthProxy(),
|
|
73
|
+
],
|
|
74
|
+
});
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
### Server options
|
|
78
|
+
|
|
79
|
+
| Option | Default | Description |
|
|
80
|
+
| --- | --- | --- |
|
|
81
|
+
| `clientID` | `"desktop"` | Must match the desktop adapter's `clientID`. |
|
|
82
|
+
| `codeExpiresIn` | `300` | One-time-code TTL (seconds). |
|
|
83
|
+
| `hashKey` | `"token"` | Name of the one-time-code parameter. |
|
|
84
|
+
| `webCallbackUrl` | — | Optional branded callback page (see below). Omit for the default direct-to-loopback redirect. |
|
|
85
|
+
| `allowedLoopbackPorts` | — | Optional hardening: `number[]` or `{ min, max }`. Restricts which loopback port the server will redirect to. |
|
|
86
|
+
| `disableOriginOverride` | `false` | Disable rewriting the request origin from the `desktop-origin` header. |
|
|
87
|
+
|
|
88
|
+
By **default** the server redirects the browser straight to the desktop
|
|
89
|
+
loopback. No web callback page is involved.
|
|
90
|
+
|
|
91
|
+
## Electrobun (desktop)
|
|
92
|
+
|
|
93
|
+
`electrobunDesktop` is a standard Better Auth client plugin. Use it with
|
|
94
|
+
`createAuthClient` in the Bun **main** process, pass `createBunRPC()` to your
|
|
95
|
+
`BrowserWindow`, and call `setupMain()`.
|
|
96
|
+
|
|
97
|
+
`storage` is **required** — pass `keychainStorage()` (Bun.secrets) or your own
|
|
98
|
+
keychain-backed `Storage`. The package never picks a default, so it can't
|
|
99
|
+
silently land on a runtime-inappropriate one.
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
// desktop/bun/auth.ts
|
|
103
|
+
import { createAuthClient } from "better-auth/client";
|
|
104
|
+
import {
|
|
105
|
+
electrobunDesktop,
|
|
106
|
+
keychainStorage,
|
|
107
|
+
} from "@soorya-u/better-auth-desktop/electrobun";
|
|
108
|
+
|
|
109
|
+
export const authClient = createAuthClient({
|
|
110
|
+
baseURL: process.env.SERVER_URL!,
|
|
111
|
+
plugins: [
|
|
112
|
+
electrobunDesktop({
|
|
113
|
+
clientID: "my-desktop-app",
|
|
114
|
+
storage: await keychainStorage(),
|
|
115
|
+
// loopbackPort: 51789, // optional; omit to bind 127.0.0.1:0 (OS-assigned)
|
|
116
|
+
}),
|
|
117
|
+
],
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
// Bun-side RPC the WebView calls; pass to `new BrowserWindow({ rpc })`.
|
|
121
|
+
export const authBunRpc = authClient.createBunRPC();
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
```ts
|
|
125
|
+
// desktop/bun/index.ts
|
|
126
|
+
import { BrowserWindow } from "electrobun/bun";
|
|
127
|
+
import { authClient, authBunRpc } from "./auth";
|
|
128
|
+
|
|
129
|
+
new BrowserWindow({ title: "My App", url, rpc: authBunRpc });
|
|
130
|
+
await authClient.setupMain();
|
|
131
|
+
```
|
|
132
|
+
|
|
133
|
+
### Renderer (WebView)
|
|
134
|
+
|
|
135
|
+
```ts
|
|
136
|
+
// renderer entry
|
|
137
|
+
import { defineAuthWebviewRPC } from "@soorya-u/better-auth-desktop/rpc/webview";
|
|
138
|
+
|
|
139
|
+
export const auth = defineAuthWebviewRPC();
|
|
140
|
+
|
|
141
|
+
auth.onAuthenticated((user) => navigate("/app"));
|
|
142
|
+
await auth.requestAuth({ provider: "github" });
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
The bridge exposes `requestAuth`, `getUser`, `signOut`, `getUserImage`, and the
|
|
146
|
+
`onAuthenticated` / `onUserUpdated` / `onAuthError` subscriptions.
|
|
147
|
+
|
|
148
|
+
No `urlSchemes` / `protocol` registration is needed in `electrobun.config.ts`.
|
|
149
|
+
|
|
150
|
+
## Electron (desktop)
|
|
151
|
+
|
|
152
|
+
The Electron adapter shares the same core; it pulls in **no** Electrobun code.
|
|
153
|
+
`storage` is **required**. The package ships `electronStorage()` — persistent
|
|
154
|
+
via [`electron-store`](https://github.com/sindresorhus/electron-store) (install
|
|
155
|
+
it: `bun add electron-store`) with values encrypted by Electron `safeStorage`
|
|
156
|
+
(OS keychain) when available — or you can supply your own `Storage`.
|
|
157
|
+
|
|
158
|
+
```ts
|
|
159
|
+
// main process
|
|
160
|
+
import { createAuthClient } from "better-auth/client";
|
|
161
|
+
import {
|
|
162
|
+
electronDesktop,
|
|
163
|
+
electronStorage,
|
|
164
|
+
} from "@soorya-u/better-auth-desktop/electron";
|
|
165
|
+
|
|
166
|
+
const authClient = createAuthClient({
|
|
167
|
+
baseURL: process.env.SERVER_URL!,
|
|
168
|
+
plugins: [
|
|
169
|
+
electronDesktop({
|
|
170
|
+
clientID: "my-desktop-app",
|
|
171
|
+
getWindow: () => mainWindow,
|
|
172
|
+
storage: await electronStorage(),
|
|
173
|
+
}),
|
|
174
|
+
],
|
|
175
|
+
});
|
|
176
|
+
await authClient.setupMain();
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The renderer talks to it over IPC; the channel names are exported as
|
|
180
|
+
`ELECTRON_AUTH_CHANNELS` from `@soorya-u/better-auth-desktop/electron`.
|
|
181
|
+
|
|
182
|
+
## Optional: branded web callback page
|
|
183
|
+
|
|
184
|
+
If you set `webCallbackUrl` on the server plugin, `oauth-complete` redirects the
|
|
185
|
+
browser to your page (`#token=…&loopback=…`) instead of straight to the
|
|
186
|
+
loopback. Your page calls `forwardToDesktop()`, which performs the top-level
|
|
187
|
+
navigation to `127.0.0.1` — still no CORS / PNA / mixed-content.
|
|
188
|
+
|
|
189
|
+
It's available both as a standalone function and as a Better Auth client plugin:
|
|
190
|
+
|
|
191
|
+
```ts
|
|
192
|
+
// standalone
|
|
193
|
+
import { forwardToDesktop } from "@soorya-u/better-auth-desktop/web";
|
|
194
|
+
forwardToDesktop(); // safe no-op outside a desktop sign-in
|
|
195
|
+
|
|
196
|
+
// or as a plugin
|
|
197
|
+
import { webDesktop } from "@soorya-u/better-auth-desktop/web";
|
|
198
|
+
const authClient = createAuthClient({ plugins: [webDesktop()] });
|
|
199
|
+
authClient.forwardToDesktop();
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
### Customizing the loopback success page
|
|
203
|
+
|
|
204
|
+
By default the loopback serves a minimal "you can close this tab" page. Override
|
|
205
|
+
it via `loopbackSuccess` on the desktop plugin — a string is used as the HTML
|
|
206
|
+
body, or `{ redirectTo }` bounces the browser to your own page:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
electrobunDesktop({
|
|
210
|
+
storage: await keychainStorage(),
|
|
211
|
+
loopbackSuccess: { redirectTo: "https://app.example.com/signed-in" },
|
|
212
|
+
});
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
## Custom adapters
|
|
216
|
+
|
|
217
|
+
`@soorya-u/better-auth-desktop/core` exposes the framework-agnostic pieces —
|
|
218
|
+
the `DesktopAdapter` interface, `startAuthFlow`, `exchangeToken`, the loopback
|
|
219
|
+
helpers, and `desktopClient` — so you can target another runtime. An adapter
|
|
220
|
+
only has to:
|
|
221
|
+
|
|
222
|
+
```ts
|
|
223
|
+
type DesktopAdapter = {
|
|
224
|
+
openExternal(url: string): void | Promise<void>;
|
|
225
|
+
serveLoopback(
|
|
226
|
+
onRequest: (req: LoopbackRequest) => Promise<LoopbackResponse>,
|
|
227
|
+
opts?: { port?: number },
|
|
228
|
+
): Promise<{ port: number; close(): void }>;
|
|
229
|
+
notifyRenderer(event: AuthEvent): void;
|
|
230
|
+
storage: Storage;
|
|
231
|
+
};
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
## Exports
|
|
235
|
+
|
|
236
|
+
| Subpath | Purpose |
|
|
237
|
+
| --- | --- |
|
|
238
|
+
| `@soorya-u/better-auth-desktop` | re-exports `core` (types & utilities) |
|
|
239
|
+
| `@soorya-u/better-auth-desktop/server` | `betterAuthDesktop()` server plugin |
|
|
240
|
+
| `@soorya-u/better-auth-desktop/electrobun` | `electrobunDesktop()` plugin + `keychainStorage()` (Bun main process) |
|
|
241
|
+
| `@soorya-u/better-auth-desktop/rpc/webview` | `defineAuthWebviewRPC()` (Electrobun renderer) |
|
|
242
|
+
| `@soorya-u/better-auth-desktop/electron` | `electronDesktop()` plugin + `electronStorage()` (Electron main process) |
|
|
243
|
+
| `@soorya-u/better-auth-desktop/web` | `forwardToDesktop()` + `webDesktop()` plugin (optional branded page) |
|
|
244
|
+
| `@soorya-u/better-auth-desktop/client` | `desktopClient()` Better Auth client plugin |
|
|
245
|
+
| `@soorya-u/better-auth-desktop/core` | shared types & utilities for custom adapters |
|
|
246
|
+
|
|
247
|
+
## Security notes
|
|
248
|
+
|
|
249
|
+
- The loopback binds `127.0.0.1` only and is unreachable off-box.
|
|
250
|
+
- The token in the loopback URL is a one-time, short-TTL, PKCE-bound code; the
|
|
251
|
+
verifier never leaves the desktop, so interception/replay is useless.
|
|
252
|
+
- The nonce stops a different local app from consuming the redirect.
|
|
253
|
+
- The loopback closes immediately after a successful exchange (or on timeout).
|
|
254
|
+
|
|
255
|
+
## License
|
|
256
|
+
|
|
257
|
+
MIT
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { n as AuthUser } from "../types-h9AEfSnq.mjs";
|
|
2
|
+
import { n as DesktopClientPluginOptions } from "../client-De_zQzE1.mjs";
|
|
3
|
+
import { n as keychainStorage, t as KeychainStorageOptions } from "../storage-DtgX_vmE.mjs";
|
|
4
|
+
import * as _$_better_fetch_fetch0 from "@better-fetch/fetch";
|
|
5
|
+
import { BetterFetch } from "@better-fetch/fetch";
|
|
6
|
+
import { BetterAuthClientOptions, ClientStore } from "@better-auth/core";
|
|
7
|
+
|
|
8
|
+
//#region src/adapters/electrobun.d.ts
|
|
9
|
+
type AuthSender = {
|
|
10
|
+
onAuthenticated(user: AuthUser): void;
|
|
11
|
+
onUserUpdated(user: AuthUser | null): void;
|
|
12
|
+
onAuthError(payload: {
|
|
13
|
+
error: {
|
|
14
|
+
message: string;
|
|
15
|
+
};
|
|
16
|
+
path?: string;
|
|
17
|
+
}): void;
|
|
18
|
+
};
|
|
19
|
+
type ElectrobunDesktopRPC = {
|
|
20
|
+
send: AuthSender;
|
|
21
|
+
setTransport(transport: unknown): void;
|
|
22
|
+
};
|
|
23
|
+
type ElectrobunDesktopOptions = DesktopClientPluginOptions;
|
|
24
|
+
/**
|
|
25
|
+
* Better Auth client plugin for Electrobun (Bun main process). Use it with
|
|
26
|
+
* `createAuthClient`, then pass `createBunRPC()` to your `BrowserWindow` and
|
|
27
|
+
* call `setupMain()`:
|
|
28
|
+
*
|
|
29
|
+
* ```ts
|
|
30
|
+
* const authClient = createAuthClient({
|
|
31
|
+
* baseURL,
|
|
32
|
+
* plugins: [electrobunDesktop({ clientID, storage: await keychainStorage() })],
|
|
33
|
+
* });
|
|
34
|
+
* new BrowserWindow({ rpc: authClient.createBunRPC(), url });
|
|
35
|
+
* await authClient.setupMain();
|
|
36
|
+
* ```
|
|
37
|
+
*/
|
|
38
|
+
declare const electrobunDesktop: (options: ElectrobunDesktopOptions) => {
|
|
39
|
+
id: "desktop";
|
|
40
|
+
version: string;
|
|
41
|
+
fetchPlugins: _$_better_fetch_fetch0.BetterFetchPlugin[];
|
|
42
|
+
getActions: ($fetch: BetterFetch, $store: ClientStore, clientOptions: BetterAuthClientOptions | undefined) => {
|
|
43
|
+
getCookie: () => string;
|
|
44
|
+
createBunRPC: () => ElectrobunDesktopRPC;
|
|
45
|
+
setupMain: () => Promise<() => void | undefined>;
|
|
46
|
+
};
|
|
47
|
+
};
|
|
48
|
+
//#endregion
|
|
49
|
+
export { ElectrobunDesktopOptions, ElectrobunDesktopRPC, type KeychainStorageOptions, electrobunDesktop, keychainStorage };
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import { t as PACKAGE_VERSION } from "../version-1hEssvcU.mjs";
|
|
2
|
+
import { t as createDesktopCookieLayer } from "../client-DhRcoap2.mjs";
|
|
3
|
+
import { n as startAuthFlow, r as fetchUserImage } from "../exchange-DCZ0j_T5.mjs";
|
|
4
|
+
import { t as keychainStorage } from "../storage-BcwDmS-W.mjs";
|
|
5
|
+
import { BrowserView, Utils } from "electrobun/bun";
|
|
6
|
+
//#region src/adapters/electrobun.ts
|
|
7
|
+
function createLoopbackAdapter(getSender, storage) {
|
|
8
|
+
return {
|
|
9
|
+
openExternal(url) {
|
|
10
|
+
if (!Utils.openExternal(url)) throw new Error(`Failed to open the system browser: ${url}`);
|
|
11
|
+
},
|
|
12
|
+
async serveLoopback(onRequest, opts) {
|
|
13
|
+
const server = Bun.serve({
|
|
14
|
+
hostname: "127.0.0.1",
|
|
15
|
+
port: opts?.port ?? 0,
|
|
16
|
+
async fetch(req) {
|
|
17
|
+
const url = new URL(req.url);
|
|
18
|
+
const query = {};
|
|
19
|
+
url.searchParams.forEach((value, key) => {
|
|
20
|
+
query[key] = value;
|
|
21
|
+
});
|
|
22
|
+
const res = await onRequest({
|
|
23
|
+
path: url.pathname,
|
|
24
|
+
query
|
|
25
|
+
});
|
|
26
|
+
return new Response(res.body, {
|
|
27
|
+
status: res.status,
|
|
28
|
+
headers: res.headers
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
return {
|
|
33
|
+
port: server.port ?? 0,
|
|
34
|
+
close: () => void server.stop()
|
|
35
|
+
};
|
|
36
|
+
},
|
|
37
|
+
notifyRenderer(event) {
|
|
38
|
+
const send = getSender();
|
|
39
|
+
if (!send) return;
|
|
40
|
+
switch (event.type) {
|
|
41
|
+
case "authenticated":
|
|
42
|
+
send.onAuthenticated(event.user);
|
|
43
|
+
break;
|
|
44
|
+
case "user-updated":
|
|
45
|
+
send.onUserUpdated(event.user);
|
|
46
|
+
break;
|
|
47
|
+
case "error":
|
|
48
|
+
send.onAuthError({
|
|
49
|
+
error: { message: event.error.message },
|
|
50
|
+
path: event.path
|
|
51
|
+
});
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
storage
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Better Auth client plugin for Electrobun (Bun main process). Use it with
|
|
60
|
+
* `createAuthClient`, then pass `createBunRPC()` to your `BrowserWindow` and
|
|
61
|
+
* call `setupMain()`:
|
|
62
|
+
*
|
|
63
|
+
* ```ts
|
|
64
|
+
* const authClient = createAuthClient({
|
|
65
|
+
* baseURL,
|
|
66
|
+
* plugins: [electrobunDesktop({ clientID, storage: await keychainStorage() })],
|
|
67
|
+
* });
|
|
68
|
+
* new BrowserWindow({ rpc: authClient.createBunRPC(), url });
|
|
69
|
+
* await authClient.setupMain();
|
|
70
|
+
* ```
|
|
71
|
+
*/
|
|
72
|
+
const electrobunDesktop = (options) => {
|
|
73
|
+
const layer = createDesktopCookieLayer(options);
|
|
74
|
+
let rpc = null;
|
|
75
|
+
const adapter = createLoopbackAdapter(() => rpc?.send ?? null, options.storage);
|
|
76
|
+
const sanitizeUser = async (user) => {
|
|
77
|
+
if (user !== null && typeof options.sanitizeUser === "function") try {
|
|
78
|
+
return await options.sanitizeUser(user);
|
|
79
|
+
} catch (error) {
|
|
80
|
+
console.error("Error while sanitizing user", error);
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
return user;
|
|
84
|
+
};
|
|
85
|
+
return {
|
|
86
|
+
id: "desktop",
|
|
87
|
+
version: PACKAGE_VERSION,
|
|
88
|
+
fetchPlugins: [layer.fetchPlugin],
|
|
89
|
+
getActions: ($fetch, $store, clientOptions) => {
|
|
90
|
+
layer.bindStore($store);
|
|
91
|
+
layer.bindServerOrigin(clientOptions?.baseURL);
|
|
92
|
+
const getCookie = layer.getCookie;
|
|
93
|
+
const requests = {
|
|
94
|
+
requestAuth: async ({ options: cfg }) => {
|
|
95
|
+
await startAuthFlow({
|
|
96
|
+
adapter,
|
|
97
|
+
$fetch,
|
|
98
|
+
clientOptions,
|
|
99
|
+
options,
|
|
100
|
+
cfg,
|
|
101
|
+
onAuthenticated: (user) => adapter.notifyRenderer({
|
|
102
|
+
type: "authenticated",
|
|
103
|
+
user
|
|
104
|
+
}),
|
|
105
|
+
onError: (error) => adapter.notifyRenderer({
|
|
106
|
+
type: "error",
|
|
107
|
+
error: error instanceof Error ? error : new Error(String(error)),
|
|
108
|
+
path: "/desktop/init-oauth-proxy"
|
|
109
|
+
})
|
|
110
|
+
});
|
|
111
|
+
},
|
|
112
|
+
getUser: async () => {
|
|
113
|
+
return await sanitizeUser((await $fetch("/get-session", {
|
|
114
|
+
method: "GET",
|
|
115
|
+
headers: {
|
|
116
|
+
cookie: getCookie(),
|
|
117
|
+
"content-type": "application/json"
|
|
118
|
+
}
|
|
119
|
+
})).data?.user ?? null);
|
|
120
|
+
},
|
|
121
|
+
signOut: async () => {
|
|
122
|
+
await $fetch("/sign-out", {
|
|
123
|
+
method: "POST",
|
|
124
|
+
body: "{}",
|
|
125
|
+
headers: {
|
|
126
|
+
cookie: getCookie(),
|
|
127
|
+
"content-type": "application/json"
|
|
128
|
+
}
|
|
129
|
+
});
|
|
130
|
+
},
|
|
131
|
+
getUserImage: async ({ url }) => {
|
|
132
|
+
const result = await fetchUserImage(clientOptions?.baseURL, url);
|
|
133
|
+
if (!result) return { dataUrl: null };
|
|
134
|
+
const { base64 } = await import("@better-auth/utils/base64");
|
|
135
|
+
return { dataUrl: `data:${result.mimeType};base64,${base64.encode(result.bytes)}` };
|
|
136
|
+
}
|
|
137
|
+
};
|
|
138
|
+
return {
|
|
139
|
+
getCookie,
|
|
140
|
+
createBunRPC: () => {
|
|
141
|
+
rpc = BrowserView.defineRPC({
|
|
142
|
+
maxRequestTime: 3e4,
|
|
143
|
+
handlers: {
|
|
144
|
+
requests,
|
|
145
|
+
messages: {}
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
return rpc;
|
|
149
|
+
},
|
|
150
|
+
setupMain: async () => {
|
|
151
|
+
const unsub = $store.atoms.session?.subscribe(async (state) => {
|
|
152
|
+
if (state.isPending === true) return;
|
|
153
|
+
const user = await sanitizeUser(state.data?.user ?? null);
|
|
154
|
+
adapter.notifyRenderer({
|
|
155
|
+
type: "user-updated",
|
|
156
|
+
user
|
|
157
|
+
});
|
|
158
|
+
});
|
|
159
|
+
return () => unsub?.();
|
|
160
|
+
}
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
};
|
|
164
|
+
};
|
|
165
|
+
//#endregion
|
|
166
|
+
export { electrobunDesktop, keychainStorage };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { l as Storage } from "../types-h9AEfSnq.mjs";
|
|
2
|
+
import { n as DesktopClientPluginOptions } from "../client-De_zQzE1.mjs";
|
|
3
|
+
import * as _$_better_fetch_fetch0 from "@better-fetch/fetch";
|
|
4
|
+
import { BetterFetch } from "@better-fetch/fetch";
|
|
5
|
+
import { BetterAuthClientOptions, ClientStore } from "@better-auth/core";
|
|
6
|
+
|
|
7
|
+
//#region src/adapters/electron.d.ts
|
|
8
|
+
declare const ELECTRON_AUTH_CHANNELS: {
|
|
9
|
+
readonly requestAuth: "desktop:requestAuth";
|
|
10
|
+
readonly getUser: "desktop:getUser";
|
|
11
|
+
readonly signOut: "desktop:signOut";
|
|
12
|
+
readonly getUserImage: "desktop:getUserImage";
|
|
13
|
+
readonly onAuthenticated: "desktop:onAuthenticated";
|
|
14
|
+
readonly onUserUpdated: "desktop:onUserUpdated";
|
|
15
|
+
readonly onAuthError: "desktop:onAuthError";
|
|
16
|
+
};
|
|
17
|
+
type WebContents = {
|
|
18
|
+
send(channel: string, payload: unknown): void;
|
|
19
|
+
};
|
|
20
|
+
type ElectronWindowLike = {
|
|
21
|
+
webContents: WebContents;
|
|
22
|
+
} | null | undefined;
|
|
23
|
+
type ElectronStorageOptions = {
|
|
24
|
+
/** electron-store file name (without extension). @default "better-auth-desktop" */name?: string; /** Encrypt values with Electron `safeStorage` (OS keychain) when available. @default true */
|
|
25
|
+
encrypt?: boolean;
|
|
26
|
+
};
|
|
27
|
+
declare function electronStorage(opts?: ElectronStorageOptions): Promise<Storage>;
|
|
28
|
+
type ElectronDesktopOptions = DesktopClientPluginOptions & {
|
|
29
|
+
/** Returns the focused window whose renderer should receive auth events. */getWindow: () => ElectronWindowLike;
|
|
30
|
+
};
|
|
31
|
+
/**
|
|
32
|
+
* Better Auth client plugin for Electron (main process). Shares the same core
|
|
33
|
+
* as the Electrobun plugin; pulls in **no** Electrobun code. Use it with
|
|
34
|
+
* `createAuthClient`, then call `setupMain()` (which registers the IPC handlers
|
|
35
|
+
* and the session subscription, and returns a cleanup function):
|
|
36
|
+
*
|
|
37
|
+
* ```ts
|
|
38
|
+
* const authClient = createAuthClient({
|
|
39
|
+
* baseURL,
|
|
40
|
+
* plugins: [electronDesktop({ clientID, storage: await electronStorage(), getWindow })],
|
|
41
|
+
* });
|
|
42
|
+
* await authClient.setupMain();
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
declare const electronDesktop: (options: ElectronDesktopOptions) => {
|
|
46
|
+
id: "desktop";
|
|
47
|
+
version: string;
|
|
48
|
+
fetchPlugins: _$_better_fetch_fetch0.BetterFetchPlugin[];
|
|
49
|
+
getActions: ($fetch: BetterFetch, $store: ClientStore, clientOptions: BetterAuthClientOptions | undefined) => {
|
|
50
|
+
getCookie: () => string;
|
|
51
|
+
setupMain: () => Promise<() => void>;
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
//#endregion
|
|
55
|
+
export { ELECTRON_AUTH_CHANNELS, ElectronDesktopOptions, ElectronStorageOptions, electronDesktop, electronStorage };
|