@better-auth/electron 1.5.0-beta.13 → 1.5.0-beta.15

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.
@@ -1,3 +1,4 @@
1
+ import { n as ElectronSharedOptions } from "./options-DTTv7_dq.mjs";
1
2
  import * as z from "zod";
2
3
  import { BetterFetch } from "@better-fetch/fetch";
3
4
 
@@ -6,13 +7,7 @@ interface Storage {
6
7
  getItem: (name: string) => unknown | null;
7
8
  setItem: (name: string, value: unknown) => void;
8
9
  }
9
- interface ElectronClientOptions {
10
- /**
11
- * The URL to redirect to for authentication.
12
- *
13
- * @example "http://localhost:3000/sign-in"
14
- */
15
- signInURL: string | URL;
10
+ interface ElectronSharedClientOptions extends ElectronSharedOptions {
16
11
  /**
17
12
  * The protocol scheme to use for deep linking in Electron.
18
13
  *
@@ -23,7 +18,6 @@ interface ElectronClientOptions {
23
18
  */
24
19
  protocol: string | {
25
20
  scheme: string;
26
- privileges?: Electron.Privileges | undefined;
27
21
  };
28
22
  /**
29
23
  * The callback path to use for authentication redirects.
@@ -31,14 +25,27 @@ interface ElectronClientOptions {
31
25
  * @default "/auth/callback"
32
26
  */
33
27
  callbackPath?: string;
28
+ }
29
+ interface ElectronClientOptions extends ElectronSharedClientOptions {
34
30
  /**
35
- * An instance of a storage solution (e.g., `electron-store`)
31
+ * The URL to redirect to for authentication.
32
+ *
33
+ * @example "http://localhost:3000/sign-in"
34
+ */
35
+ signInURL: string | URL;
36
+ protocol: string | {
37
+ scheme: string;
38
+ privileges?: Electron.Privileges | undefined;
39
+ };
40
+ /**
41
+ * An instance of a storage solution (e.g., `conf`)
36
42
  * to store session and cookie data.
37
43
  *
38
44
  * @example
39
45
  * ```ts
46
+ * import { storage } from "@better-auth/electron/storage";
40
47
  * electronClient({
41
- * storage: window.localStorage,
48
+ * storage: storage(),
42
49
  * });
43
50
  * ```
44
51
  */
@@ -66,12 +73,6 @@ interface ElectronClientOptions {
66
73
  * @default "better-auth"
67
74
  */
68
75
  channelPrefix?: string | undefined;
69
- /**
70
- * Client ID to use for identifying the Electron client during authorization.
71
- *
72
- * @default "electron"
73
- */
74
- clientID?: string | undefined;
75
76
  /**
76
77
  * Whether to disable caching the session data locally.
77
78
  *
@@ -79,32 +80,7 @@ interface ElectronClientOptions {
79
80
  */
80
81
  disableCache?: boolean | undefined;
81
82
  }
82
- interface ElectronProxyClientOptions {
83
- /**
84
- * The protocol scheme to use for deep linking in Electron.
85
- *
86
- * Should follow the reverse domain name notation to ensure uniqueness.
87
- *
88
- * Note that this must match the protocol scheme registered in the server plugin.
89
- *
90
- * @see {@link https://datatracker.ietf.org/doc/html/rfc8252#section-7.1}
91
- * @example "com.example.app"
92
- */
93
- protocol: string | {
94
- scheme: string;
95
- };
96
- /**
97
- * The callback path to use for authentication redirects.
98
- *
99
- * @default "/auth/callback"
100
- */
101
- callbackPath?: string;
102
- /**
103
- * Client ID to use for identifying the Electron client during authorization.
104
- *
105
- * @default "electron"
106
- */
107
- clientID?: string | undefined;
83
+ interface ElectronProxyClientOptions extends ElectronSharedClientOptions {
108
84
  /**
109
85
  * The prefix to use for cookies set by the plugin.
110
86
  *
@@ -126,4 +102,4 @@ declare const requestAuthOptionsSchema: z.ZodObject<{
126
102
  }, z.core.$strip>;
127
103
  type ElectronRequestAuthOptions = z.infer<typeof requestAuthOptionsSchema>;
128
104
  //#endregion
129
- export { Storage as i, ElectronClientOptions as n, ElectronProxyClientOptions as r, ElectronRequestAuthOptions as t };
105
+ export { Storage as a, ElectronSharedClientOptions as i, ElectronClientOptions as n, ElectronProxyClientOptions as r, ElectronRequestAuthOptions as t };
@@ -0,0 +1,57 @@
1
+ import { n as ElectronClientOptions, t as ElectronRequestAuthOptions } from "./authenticate-DifQcrCY.mjs";
2
+ import { BetterFetch, BetterFetchError } from "@better-fetch/fetch";
3
+ import electron from "electron";
4
+ import { User } from "@better-auth/core/db";
5
+
6
+ //#region src/preload.d.ts
7
+ type ExposedBridges = ReturnType<typeof exposeBridges>["$InferBridges"];
8
+ /**
9
+ * Exposes IPC bridges to the renderer process.
10
+ */
11
+ declare function exposeBridges(opts: SetupRendererConfig): {
12
+ $InferBridges: {
13
+ getUser: () => Promise<{
14
+ id: string;
15
+ createdAt: Date;
16
+ updatedAt: Date;
17
+ email: string;
18
+ emailVerified: boolean;
19
+ name: string;
20
+ image?: string | null | undefined;
21
+ } & Record<string, any>>;
22
+ requestAuth: (options?: ElectronRequestAuthOptions) => Promise<void>;
23
+ signOut: () => Promise<void>;
24
+ onAuthenticated: (callback: (user: User & Record<string, any>) => unknown) => () => void;
25
+ onUserUpdated: (callback: (user: (User & Record<string, any>) | null) => unknown) => () => void;
26
+ onAuthError: (callback: (context: BetterFetchError & {
27
+ path: string;
28
+ }) => unknown) => () => void;
29
+ };
30
+ };
31
+ interface SetupRendererConfig {
32
+ channelPrefix?: ElectronClientOptions["channelPrefix"] | undefined;
33
+ }
34
+ //#endregion
35
+ //#region src/browser.d.ts
36
+ type SetupMainConfig = {
37
+ getWindow?: () => electron.BrowserWindow | null | undefined;
38
+ csp?: boolean | undefined;
39
+ bridges?: boolean | undefined;
40
+ scheme?: boolean | undefined;
41
+ };
42
+ /**
43
+ * Handles the deep link URL for authentication.
44
+ */
45
+ declare function handleDeepLink({
46
+ $fetch,
47
+ options,
48
+ url,
49
+ getWindow
50
+ }: {
51
+ $fetch: BetterFetch;
52
+ options: ElectronClientOptions;
53
+ url: string;
54
+ getWindow?: SetupMainConfig["getWindow"] | undefined;
55
+ }): Promise<void>;
56
+ //#endregion
57
+ export { ExposedBridges as n, handleDeepLink as t };
package/dist/client.d.mts CHANGED
@@ -1,3 +1,172 @@
1
- import { i as Storage, n as ElectronClientOptions, r as ElectronProxyClientOptions, t as ElectronRequestAuthOptions } from "./authenticate-CWAVJ4W8.mjs";
2
- import { n as handleDeepLink, t as electronClient } from "./client-BBp9yCmE.mjs";
3
- export { ElectronClientOptions, ElectronProxyClientOptions, ElectronRequestAuthOptions, Storage, electronClient, handleDeepLink };
1
+ import { n as ElectronSharedOptions } from "./options-DTTv7_dq.mjs";
2
+ import { a as Storage, i as ElectronSharedClientOptions, n as ElectronClientOptions, r as ElectronProxyClientOptions, t as ElectronRequestAuthOptions } from "./authenticate-DifQcrCY.mjs";
3
+ import { n as ExposedBridges, t as handleDeepLink } from "./browser-jzFaG_9T.mjs";
4
+ import * as better_auth0 from "better-auth";
5
+ import { ClientStore } from "better-auth";
6
+ import * as _better_fetch_fetch0 from "@better-fetch/fetch";
7
+ import electron from "electron";
8
+
9
+ //#region src/client.d.ts
10
+ declare const electronClient: (options: ElectronClientOptions) => {
11
+ id: "electron";
12
+ fetchPlugins: {
13
+ id: string;
14
+ name: string;
15
+ init(url: string, options: ({
16
+ cache?: RequestCache | undefined;
17
+ credentials?: RequestCredentials | undefined;
18
+ headers?: (HeadersInit & (HeadersInit | {
19
+ accept: "application/json" | "text/plain" | "application/octet-stream";
20
+ "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
21
+ authorization: "Bearer" | "Basic";
22
+ })) | undefined;
23
+ integrity?: string | undefined;
24
+ keepalive?: boolean | undefined;
25
+ method?: string | undefined;
26
+ mode?: RequestMode | undefined;
27
+ priority?: RequestPriority | undefined;
28
+ redirect?: RequestRedirect | undefined;
29
+ referrer?: string | undefined;
30
+ referrerPolicy?: ReferrerPolicy | undefined;
31
+ signal?: (AbortSignal | null) | undefined;
32
+ window?: null | undefined;
33
+ onRequest?: (<T extends Record<string, any>>(context: _better_fetch_fetch0.RequestContext<T>) => Promise<_better_fetch_fetch0.RequestContext | void> | _better_fetch_fetch0.RequestContext | void) | undefined;
34
+ onResponse?: ((context: _better_fetch_fetch0.ResponseContext) => Promise<Response | void | _better_fetch_fetch0.ResponseContext> | Response | _better_fetch_fetch0.ResponseContext | void) | undefined;
35
+ onSuccess?: ((context: _better_fetch_fetch0.SuccessContext<any>) => Promise<void> | void) | undefined;
36
+ onError?: ((context: _better_fetch_fetch0.ErrorContext) => Promise<void> | void) | undefined;
37
+ onRetry?: ((response: _better_fetch_fetch0.ResponseContext) => Promise<void> | void) | undefined;
38
+ hookOptions?: {
39
+ cloneResponse?: boolean;
40
+ } | undefined;
41
+ timeout?: number | undefined;
42
+ customFetchImpl?: _better_fetch_fetch0.FetchEsque | undefined;
43
+ plugins?: _better_fetch_fetch0.BetterFetchPlugin[] | undefined;
44
+ baseURL?: string | undefined;
45
+ throw?: boolean | undefined;
46
+ auth?: ({
47
+ type: "Bearer";
48
+ token: string | Promise<string | undefined> | (() => string | Promise<string | undefined> | undefined) | undefined;
49
+ } | {
50
+ type: "Basic";
51
+ username: string | (() => string | undefined) | undefined;
52
+ password: string | (() => string | undefined) | undefined;
53
+ } | {
54
+ type: "Custom";
55
+ prefix: string | (() => string | undefined) | undefined;
56
+ value: string | (() => string | undefined) | undefined;
57
+ }) | undefined;
58
+ body?: any;
59
+ query?: any;
60
+ params?: any;
61
+ duplex?: "full" | "half" | undefined;
62
+ jsonParser?: ((text: string) => Promise<any> | any) | undefined;
63
+ retry?: _better_fetch_fetch0.RetryOptions | undefined;
64
+ retryAttempt?: number | undefined;
65
+ output?: (_better_fetch_fetch0.StandardSchemaV1 | typeof Blob | typeof File) | undefined;
66
+ errorSchema?: _better_fetch_fetch0.StandardSchemaV1 | undefined;
67
+ disableValidation?: boolean | undefined;
68
+ } & Record<string, any>) | undefined): Promise<{
69
+ url: string;
70
+ options: {
71
+ cache?: RequestCache | undefined;
72
+ credentials?: RequestCredentials | undefined;
73
+ headers?: (HeadersInit & (HeadersInit | {
74
+ accept: "application/json" | "text/plain" | "application/octet-stream";
75
+ "content-type": "application/json" | "text/plain" | "application/x-www-form-urlencoded" | "multipart/form-data" | "application/octet-stream";
76
+ authorization: "Bearer" | "Basic";
77
+ })) | undefined;
78
+ integrity?: string | undefined;
79
+ keepalive?: boolean | undefined;
80
+ method?: string | undefined;
81
+ mode?: RequestMode | undefined;
82
+ priority?: RequestPriority | undefined;
83
+ redirect?: RequestRedirect | undefined;
84
+ referrer?: string | undefined;
85
+ referrerPolicy?: ReferrerPolicy | undefined;
86
+ signal?: (AbortSignal | null) | undefined;
87
+ window?: null | undefined;
88
+ onRequest?: (<T extends Record<string, any>>(context: _better_fetch_fetch0.RequestContext<T>) => Promise<_better_fetch_fetch0.RequestContext | void> | _better_fetch_fetch0.RequestContext | void) | undefined;
89
+ onResponse?: ((context: _better_fetch_fetch0.ResponseContext) => Promise<Response | void | _better_fetch_fetch0.ResponseContext> | Response | _better_fetch_fetch0.ResponseContext | void) | undefined;
90
+ onSuccess?: ((context: _better_fetch_fetch0.SuccessContext<any>) => Promise<void> | void) | undefined;
91
+ onError?: ((context: _better_fetch_fetch0.ErrorContext) => Promise<void> | void) | undefined;
92
+ onRetry?: ((response: _better_fetch_fetch0.ResponseContext) => Promise<void> | void) | undefined;
93
+ hookOptions?: {
94
+ cloneResponse?: boolean;
95
+ } | undefined;
96
+ timeout?: number | undefined;
97
+ customFetchImpl?: _better_fetch_fetch0.FetchEsque | undefined;
98
+ plugins?: _better_fetch_fetch0.BetterFetchPlugin[] | undefined;
99
+ baseURL?: string | undefined;
100
+ throw?: boolean | undefined;
101
+ auth?: ({
102
+ type: "Bearer";
103
+ token: string | Promise<string | undefined> | (() => string | Promise<string | undefined> | undefined) | undefined;
104
+ } | {
105
+ type: "Basic";
106
+ username: string | (() => string | undefined) | undefined;
107
+ password: string | (() => string | undefined) | undefined;
108
+ } | {
109
+ type: "Custom";
110
+ prefix: string | (() => string | undefined) | undefined;
111
+ value: string | (() => string | undefined) | undefined;
112
+ }) | undefined;
113
+ body?: any;
114
+ query?: any;
115
+ params?: any;
116
+ duplex?: "full" | "half" | undefined;
117
+ jsonParser?: ((text: string) => Promise<any> | any) | undefined;
118
+ retry?: _better_fetch_fetch0.RetryOptions | undefined;
119
+ retryAttempt?: number | undefined;
120
+ output?: (_better_fetch_fetch0.StandardSchemaV1 | typeof Blob | typeof File) | undefined;
121
+ errorSchema?: _better_fetch_fetch0.StandardSchemaV1 | undefined;
122
+ disableValidation?: boolean | undefined;
123
+ } & Record<string, any>;
124
+ }>;
125
+ hooks: {
126
+ onSuccess: (context: _better_fetch_fetch0.SuccessContext<any>) => Promise<void>;
127
+ onError: (context: _better_fetch_fetch0.ErrorContext) => Promise<void>;
128
+ };
129
+ }[];
130
+ getActions: ($fetch: _better_fetch_fetch0.BetterFetch, $store: ClientStore, clientOptions: better_auth0.BetterAuthClientOptions | undefined) => {
131
+ /**
132
+ * Gets the stored cookie.
133
+ *
134
+ * You can use this to get the cookie stored in
135
+ * the device and use it in your fetch requests.
136
+ *
137
+ * @example
138
+ * ```ts
139
+ * const cookie = client.getCookie();
140
+ * await fetch("https://api.example.com", {
141
+ * headers: {
142
+ * cookie,
143
+ * },
144
+ * });
145
+ * ```
146
+ */
147
+ getCookie: () => string;
148
+ /**
149
+ * Initiates the authentication process.
150
+ * Opens the system's default browser for user authentication.
151
+ */
152
+ requestAuth: (options?: ElectronRequestAuthOptions | undefined) => Promise<void>;
153
+ /**
154
+ * Sets up the main process.
155
+ *
156
+ * - Registers custom protocol scheme.
157
+ * - Registers IPC bridge handlers.
158
+ * - Handles content security policy if needed.
159
+ */
160
+ setupMain: (cfg?: {
161
+ csp?: boolean | undefined;
162
+ bridges?: boolean | undefined;
163
+ scheme?: boolean | undefined;
164
+ getWindow?: () => electron.BrowserWindow | null | undefined;
165
+ }) => void;
166
+ $Infer: {
167
+ Bridges: ExposedBridges;
168
+ };
169
+ };
170
+ };
171
+ //#endregion
172
+ export { ElectronClientOptions, ElectronProxyClientOptions, ElectronRequestAuthOptions, ElectronSharedClientOptions, ElectronSharedOptions, Storage, electronClient, handleDeepLink };
package/dist/client.mjs CHANGED
@@ -1,4 +1,4 @@
1
- import { n as parseProtocolScheme, t as isProcessType } from "./utils-C3fLmbAT.mjs";
1
+ import { n as isProcessType, r as parseProtocolScheme, t as getChannelPrefixWithDelimiter } from "./utils-DecLU66o.mjs";
2
2
  import { BetterAuthError } from "@better-auth/core/error";
3
3
  import { APIError as APIError$1, getBaseURL, isDevelopment, isTest } from "better-auth";
4
4
  import { generateRandomString } from "better-auth/crypto";
@@ -11,84 +11,6 @@ import { parseSetCookieHeader } from "better-auth/cookies";
11
11
  import electron, { shell } from "electron";
12
12
  import { resolve } from "node:path";
13
13
 
14
- //#region src/bridges.ts
15
- const { ipcRenderer, ipcMain, contextBridge, webContents: webContents$1 } = electron;
16
- function getChannelPrefixWithDelimiter(ns = "better-auth") {
17
- return ns.length > 0 ? ns + ":" : ns;
18
- }
19
- function listenerFactory(channel, listener) {
20
- ipcRenderer.on(channel, listener);
21
- return () => {
22
- ipcRenderer.off(channel, listener);
23
- };
24
- }
25
- /**
26
- * Exposes IPC bridges to the renderer process.
27
- */
28
- function exposeBridges(opts) {
29
- if (!process.contextIsolated) throw new BetterAuthError("Context isolation must be enabled to use IPC bridges securely.");
30
- const prefix = getChannelPrefixWithDelimiter(opts.channelPrefix);
31
- const bridges = {
32
- getUser: async () => {
33
- return await ipcRenderer.invoke(`${prefix}getUser`);
34
- },
35
- requestAuth: async (options) => {
36
- await ipcRenderer.invoke(`${prefix}requestAuth`, options);
37
- },
38
- signOut: async () => {
39
- await ipcRenderer.invoke(`${prefix}signOut`);
40
- },
41
- onAuthenticated: (callback) => {
42
- return listenerFactory(`${prefix}authenticated`, async (_evt, user) => {
43
- await callback(user);
44
- });
45
- },
46
- onUserUpdated: (callback) => {
47
- return listenerFactory(`${prefix}user-updated`, async (_evt, user) => {
48
- await callback(user);
49
- });
50
- },
51
- onAuthError: (callback) => {
52
- return listenerFactory(`${prefix}error`, async (_evt, context) => {
53
- await callback(context);
54
- });
55
- }
56
- };
57
- for (const [key, value] of Object.entries(bridges)) contextBridge.exposeInMainWorld(key, value);
58
- return {};
59
- }
60
- /**
61
- * Sets up IPC bridges in the main process.
62
- */
63
- function setupBridges(ctx, opts, clientOptions) {
64
- const prefix = getChannelPrefixWithDelimiter(opts.channelPrefix);
65
- ctx.$store?.atoms.session?.subscribe((state) => {
66
- if (state.isPending === true) return;
67
- webContents$1.getFocusedWebContents()?.send(`${prefix}user-updated`, state?.data?.user ?? null);
68
- });
69
- ipcMain.handle(`${prefix}getUser`, async () => {
70
- return (await ctx.$fetch("/get-session", {
71
- method: "GET",
72
- headers: {
73
- cookie: ctx.getCookie(),
74
- "content-type": "application/json"
75
- }
76
- })).data?.user ?? null;
77
- });
78
- ipcMain.handle(`${prefix}requestAuth`, (_evt, options) => requestAuth(clientOptions, opts, options));
79
- ipcMain.handle(`${prefix}signOut`, async () => {
80
- await ctx.$fetch("/sign-out", {
81
- method: "POST",
82
- body: "{}",
83
- headers: {
84
- cookie: ctx.getCookie(),
85
- "content-type": "application/json"
86
- }
87
- });
88
- });
89
- }
90
-
91
- //#endregion
92
14
  //#region src/authenticate.ts
93
15
  const kCodeVerifier = Symbol.for("better-auth:code_verifier");
94
16
  const kState = Symbol.for("better-auth:state");
@@ -155,104 +77,14 @@ async function authenticate($fetch, options, body, getWindow) {
155
77
  }
156
78
 
157
79
  //#endregion
158
- //#region src/cookies.ts
159
- function getSetCookie(header, prevCookie) {
160
- const parsed = parseSetCookieHeader(header);
161
- let toSetCookie = {};
162
- parsed.forEach((cookie, key) => {
163
- const expiresAt = cookie["expires"];
164
- const maxAge = cookie["max-age"];
165
- const expires = maxAge ? new Date(Date.now() + Number(maxAge) * 1e3) : expiresAt ? new Date(String(expiresAt)) : null;
166
- toSetCookie[key] = {
167
- value: cookie["value"],
168
- expires: expires ? expires.toISOString() : null
169
- };
170
- });
171
- if (prevCookie) try {
172
- toSetCookie = {
173
- ...JSON.parse(prevCookie),
174
- ...toSetCookie
175
- };
176
- } catch {}
177
- return JSON.stringify(toSetCookie);
178
- }
179
- function getCookie(cookie) {
180
- let parsed = {};
181
- try {
182
- parsed = JSON.parse(cookie);
183
- } catch (_e) {}
184
- return Object.entries(parsed).reduce((acc, [key, value]) => {
185
- if (value.expires && new Date(value.expires) < /* @__PURE__ */ new Date()) return acc;
186
- return `${acc}; ${key}=${value.value}`;
187
- }, "");
188
- }
189
- /**
190
- * Compare if session cookies have actually changed by comparing their values.
191
- * Ignores expiry timestamps that naturally change on each request.
192
- *
193
- * @param prevCookie - Previous cookie JSON string
194
- * @param newCookie - New cookie JSON string
195
- * @returns true if session cookies have changed, false otherwise
196
- */
197
- function hasSessionCookieChanged(prevCookie, newCookie) {
198
- if (!prevCookie) return true;
199
- try {
200
- const prev = JSON.parse(prevCookie);
201
- const next = JSON.parse(newCookie);
202
- const sessionKeys = /* @__PURE__ */ new Set();
203
- Object.keys(prev).forEach((key) => {
204
- if (key.includes("session_token") || key.includes("session_data")) sessionKeys.add(key);
205
- });
206
- Object.keys(next).forEach((key) => {
207
- if (key.includes("session_token") || key.includes("session_data")) sessionKeys.add(key);
208
- });
209
- for (const key of sessionKeys) if (prev[key]?.value !== next[key]?.value) return true;
210
- return false;
211
- } catch {
212
- return true;
213
- }
214
- }
215
- /**
216
- * Check if the Set-Cookie header contains better-auth cookies.
217
- * This prevents infinite refetching when non-better-auth cookies (like third-party cookies) change.
218
- *
219
- * Supports multiple cookie naming patterns:
220
- * - Default: "better-auth.session_token", "better-auth-passkey", "__Secure-better-auth.session_token"
221
- * - Custom prefix: "myapp.session_token", "myapp-passkey", "__Secure-myapp.session_token"
222
- * - Custom full names: "my_custom_session_token", "custom_session_data"
223
- * - No prefix (cookiePrefix=""): matches any cookie with known suffixes
224
- * - Multiple prefixes: ["better-auth", "my-app"] matches cookies starting with any of the prefixes
225
- *
226
- * @param setCookieHeader - The Set-Cookie header value
227
- * @param cookiePrefix - The cookie prefix(es) to check for. Can be a string, array of strings, or empty string.
228
- * @returns true if the header contains better-auth cookies, false otherwise
229
- */
230
- function hasBetterAuthCookies(setCookieHeader, cookiePrefix) {
231
- const cookies = parseSetCookieHeader(setCookieHeader);
232
- const cookieSuffixes = ["session_token", "session_data"];
233
- const prefixes = Array.isArray(cookiePrefix) ? cookiePrefix : [cookiePrefix];
234
- for (const name of cookies.keys()) {
235
- const nameWithoutSecure = name.startsWith("__Secure-") ? name.slice(9) : name;
236
- for (const prefix of prefixes) if (prefix) {
237
- if (nameWithoutSecure.startsWith(prefix)) return true;
238
- } else for (const suffix of cookieSuffixes) if (nameWithoutSecure.endsWith(suffix)) return true;
239
- }
240
- return false;
241
- }
242
-
243
- //#endregion
244
- //#region src/setup.ts
245
- const { app: app$1, session, protocol, BrowserWindow } = electron;
80
+ //#region src/browser.ts
81
+ const { app: app$1, session, protocol, BrowserWindow, ipcMain, webContents: webContents$1 } = electron;
246
82
  function withGetWindowFallback(win) {
247
83
  return win ?? (() => {
248
84
  const allWindows = BrowserWindow.getAllWindows();
249
85
  return allWindows.length > 0 ? allWindows[0] : null;
250
86
  });
251
87
  }
252
- function setupRenderer(opts) {
253
- if (!isProcessType("renderer")) throw new BetterAuthError("setupRenderer can only be called in the renderer process.");
254
- exposeBridges(opts);
255
- }
256
88
  function setupMain($fetch, $store, getCookie, opts, clientOptions, cfg) {
257
89
  if (!isProcessType("browser")) throw new BetterAuthError("setupMain can only be called in the main process.");
258
90
  if (!cfg || cfg.csp === true) setupCSP(clientOptions);
@@ -367,6 +199,122 @@ function setupCSP(clientOptions) {
367
199
  });
368
200
  });
369
201
  }
202
+ /**
203
+ * Sets up IPC bridges in the main process.
204
+ */
205
+ function setupBridges(ctx, opts, clientOptions) {
206
+ const prefix = getChannelPrefixWithDelimiter(opts.channelPrefix);
207
+ ctx.$store?.atoms.session?.subscribe((state) => {
208
+ if (state.isPending === true) return;
209
+ webContents$1.getFocusedWebContents()?.send(`${prefix}user-updated`, state?.data?.user ?? null);
210
+ });
211
+ ipcMain.handle(`${prefix}getUser`, async () => {
212
+ return (await ctx.$fetch("/get-session", {
213
+ method: "GET",
214
+ headers: {
215
+ cookie: ctx.getCookie(),
216
+ "content-type": "application/json"
217
+ }
218
+ })).data?.user ?? null;
219
+ });
220
+ ipcMain.handle(`${prefix}requestAuth`, (_evt, options) => requestAuth(clientOptions, opts, options));
221
+ ipcMain.handle(`${prefix}signOut`, async () => {
222
+ await ctx.$fetch("/sign-out", {
223
+ method: "POST",
224
+ body: "{}",
225
+ headers: {
226
+ cookie: ctx.getCookie(),
227
+ "content-type": "application/json"
228
+ }
229
+ });
230
+ });
231
+ }
232
+
233
+ //#endregion
234
+ //#region src/cookies.ts
235
+ function getSetCookie(header, prevCookie) {
236
+ const parsed = parseSetCookieHeader(header);
237
+ let toSetCookie = {};
238
+ parsed.forEach((cookie, key) => {
239
+ const expiresAt = cookie["expires"];
240
+ const maxAge = cookie["max-age"];
241
+ const expires = maxAge ? new Date(Date.now() + Number(maxAge) * 1e3) : expiresAt ? new Date(String(expiresAt)) : null;
242
+ toSetCookie[key] = {
243
+ value: cookie["value"],
244
+ expires: expires ? expires.toISOString() : null
245
+ };
246
+ });
247
+ if (prevCookie) try {
248
+ toSetCookie = {
249
+ ...JSON.parse(prevCookie),
250
+ ...toSetCookie
251
+ };
252
+ } catch {}
253
+ return JSON.stringify(toSetCookie);
254
+ }
255
+ function getCookie(cookie) {
256
+ let parsed = {};
257
+ try {
258
+ parsed = JSON.parse(cookie);
259
+ } catch (_e) {}
260
+ return Object.entries(parsed).reduce((acc, [key, value]) => {
261
+ if (value.expires && new Date(value.expires) < /* @__PURE__ */ new Date()) return acc;
262
+ return `${acc}; ${key}=${value.value}`;
263
+ }, "");
264
+ }
265
+ /**
266
+ * Compare if session cookies have actually changed by comparing their values.
267
+ * Ignores expiry timestamps that naturally change on each request.
268
+ *
269
+ * @param prevCookie - Previous cookie JSON string
270
+ * @param newCookie - New cookie JSON string
271
+ * @returns true if session cookies have changed, false otherwise
272
+ */
273
+ function hasSessionCookieChanged(prevCookie, newCookie) {
274
+ if (!prevCookie) return true;
275
+ try {
276
+ const prev = JSON.parse(prevCookie);
277
+ const next = JSON.parse(newCookie);
278
+ const sessionKeys = /* @__PURE__ */ new Set();
279
+ Object.keys(prev).forEach((key) => {
280
+ if (key.includes("session_token") || key.includes("session_data")) sessionKeys.add(key);
281
+ });
282
+ Object.keys(next).forEach((key) => {
283
+ if (key.includes("session_token") || key.includes("session_data")) sessionKeys.add(key);
284
+ });
285
+ for (const key of sessionKeys) if (prev[key]?.value !== next[key]?.value) return true;
286
+ return false;
287
+ } catch {
288
+ return true;
289
+ }
290
+ }
291
+ /**
292
+ * Check if the Set-Cookie header contains better-auth cookies.
293
+ * This prevents infinite refetching when non-better-auth cookies (like third-party cookies) change.
294
+ *
295
+ * Supports multiple cookie naming patterns:
296
+ * - Default: "better-auth.session_token", "better-auth-passkey", "__Secure-better-auth.session_token"
297
+ * - Custom prefix: "myapp.session_token", "myapp-passkey", "__Secure-myapp.session_token"
298
+ * - Custom full names: "my_custom_session_token", "custom_session_data"
299
+ * - No prefix (cookiePrefix=""): matches any cookie with known suffixes
300
+ * - Multiple prefixes: ["better-auth", "my-app"] matches cookies starting with any of the prefixes
301
+ *
302
+ * @param setCookieHeader - The Set-Cookie header value
303
+ * @param cookiePrefix - The cookie prefix(es) to check for. Can be a string, array of strings, or empty string.
304
+ * @returns true if the header contains better-auth cookies, false otherwise
305
+ */
306
+ function hasBetterAuthCookies(setCookieHeader, cookiePrefix) {
307
+ const cookies = parseSetCookieHeader(setCookieHeader);
308
+ const cookieSuffixes = ["session_token", "session_data"];
309
+ const prefixes = Array.isArray(cookiePrefix) ? cookiePrefix : [cookiePrefix];
310
+ for (const name of cookies.keys()) {
311
+ const nameWithoutSecure = name.startsWith("__Secure-") ? name.slice(9) : name;
312
+ for (const prefix of prefixes) if (prefix) {
313
+ if (nameWithoutSecure.startsWith(prefix)) return true;
314
+ } else for (const suffix of cookieSuffixes) if (nameWithoutSecure.endsWith(suffix)) return true;
315
+ }
316
+ return false;
317
+ }
370
318
 
371
319
  //#endregion
372
320
  //#region src/client.ts
@@ -464,7 +412,6 @@ const electronClient = (options) => {
464
412
  return {
465
413
  getCookie: getCookieFn,
466
414
  requestAuth: (options) => requestAuth(clientOptions, opts, options),
467
- setupRenderer: () => setupRenderer(opts),
468
415
  setupMain: (cfg) => setupMain($fetch, store, getCookieFn, opts, clientOptions, cfg),
469
416
  $Infer: {}
470
417
  };