@better-auth/electron 1.5.0-beta.12
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.md +20 -0
- package/README.md +14 -0
- package/dist/authenticate-CWAVJ4W8.d.mts +129 -0
- package/dist/client-BBp9yCmE.d.mts +224 -0
- package/dist/client.d.mts +3 -0
- package/dist/client.mjs +476 -0
- package/dist/index.d.mts +239 -0
- package/dist/index.mjs +201 -0
- package/dist/proxy.d.mts +27 -0
- package/dist/proxy.mjs +39 -0
- package/dist/storage.d.mts +8 -0
- package/dist/storage.mjs +28 -0
- package/dist/utils-C3fLmbAT.mjs +17 -0
- package/package.json +104 -0
package/dist/client.mjs
ADDED
|
@@ -0,0 +1,476 @@
|
|
|
1
|
+
import { n as parseProtocolScheme, t as isProcessType } from "./utils-C3fLmbAT.mjs";
|
|
2
|
+
import { BetterAuthError } from "@better-auth/core/error";
|
|
3
|
+
import { APIError as APIError$1, getBaseURL, isDevelopment, isTest } from "better-auth";
|
|
4
|
+
import { generateRandomString } from "better-auth/crypto";
|
|
5
|
+
import * as z from "zod";
|
|
6
|
+
import { Buffer } from "node:buffer";
|
|
7
|
+
import { base64, base64Url } from "@better-auth/utils/base64";
|
|
8
|
+
import { createHash } from "@better-auth/utils/hash";
|
|
9
|
+
import { signInSocial } from "better-auth/api";
|
|
10
|
+
import { parseSetCookieHeader } from "better-auth/cookies";
|
|
11
|
+
import electron, { shell } from "electron";
|
|
12
|
+
import { resolve } from "node:path";
|
|
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
|
+
//#region src/authenticate.ts
|
|
93
|
+
const kCodeVerifier = Symbol.for("better-auth:code_verifier");
|
|
94
|
+
const kState = Symbol.for("better-auth:state");
|
|
95
|
+
(() => {
|
|
96
|
+
const { provider, idToken, loginHint, ...signInSocialBody } = signInSocial().options.body.shape;
|
|
97
|
+
return z.object({
|
|
98
|
+
...signInSocialBody,
|
|
99
|
+
provider: z.string().nonempty().optional()
|
|
100
|
+
});
|
|
101
|
+
})();
|
|
102
|
+
/**
|
|
103
|
+
* Opens the system browser to request user authentication.
|
|
104
|
+
*/
|
|
105
|
+
async function requestAuth(clientOptions, options, cfg) {
|
|
106
|
+
if (!isProcessType("browser")) throw new BetterAuthError("`requestAuth` can only be called in the main process");
|
|
107
|
+
const { randomBytes } = await import("node:crypto");
|
|
108
|
+
const state = generateRandomString(16, "A-Z", "a-z", "0-9");
|
|
109
|
+
const codeVerifier = base64Url.encode(randomBytes(32));
|
|
110
|
+
const codeChallenge = base64Url.encode(await createHash("SHA-256").digest(codeVerifier));
|
|
111
|
+
globalThis[kCodeVerifier] = codeVerifier;
|
|
112
|
+
globalThis[kState] = state;
|
|
113
|
+
let url = null;
|
|
114
|
+
if (cfg?.provider) {
|
|
115
|
+
const baseURL = getBaseURL(clientOptions?.baseURL, clientOptions?.basePath, void 0, true);
|
|
116
|
+
if (!baseURL) {
|
|
117
|
+
console.log("No base URL found in client options");
|
|
118
|
+
throw APIError$1.from("INTERNAL_SERVER_ERROR", {
|
|
119
|
+
code: "NO_BASE_URL",
|
|
120
|
+
message: "Base URL is required to use provider-based sign-in."
|
|
121
|
+
});
|
|
122
|
+
}
|
|
123
|
+
url = new URL(`${baseURL}/electron/init-oauth-proxy`);
|
|
124
|
+
for (const [key, value] of Object.entries(cfg)) url.searchParams.set(key, typeof value === "string" ? value : JSON.stringify(value));
|
|
125
|
+
} else url = new URL(options.signInURL);
|
|
126
|
+
url.searchParams.set("client_id", options.clientID || "electron");
|
|
127
|
+
url.searchParams.set("code_challenge", codeChallenge);
|
|
128
|
+
url.searchParams.set("code_challenge_method", "S256");
|
|
129
|
+
url.searchParams.set("state", state);
|
|
130
|
+
if (url === null) throw new Error("Failed to construct sign-in URL.");
|
|
131
|
+
await shell.openExternal(url.toString(), { activate: true });
|
|
132
|
+
}
|
|
133
|
+
/**
|
|
134
|
+
* Exchanges the authorization code for a session.
|
|
135
|
+
*/
|
|
136
|
+
async function authenticate($fetch, options, body, getWindow) {
|
|
137
|
+
const codeVerifier = globalThis[kCodeVerifier];
|
|
138
|
+
const state = globalThis[kState];
|
|
139
|
+
globalThis[kCodeVerifier] = void 0;
|
|
140
|
+
globalThis[kState] = void 0;
|
|
141
|
+
if (!codeVerifier) throw new Error("Code verifier not found.");
|
|
142
|
+
if (!state) throw new Error("State not found.");
|
|
143
|
+
await $fetch("/electron/token", {
|
|
144
|
+
method: "POST",
|
|
145
|
+
body: {
|
|
146
|
+
...body,
|
|
147
|
+
state,
|
|
148
|
+
code_verifier: codeVerifier
|
|
149
|
+
},
|
|
150
|
+
onSuccess: (ctx) => {
|
|
151
|
+
getWindow()?.webContents.send(`${getChannelPrefixWithDelimiter(options.channelPrefix)}authenticated`, ctx.data.user);
|
|
152
|
+
},
|
|
153
|
+
throw: true
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
//#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;
|
|
246
|
+
function withGetWindowFallback(win) {
|
|
247
|
+
return win ?? (() => {
|
|
248
|
+
const allWindows = BrowserWindow.getAllWindows();
|
|
249
|
+
return allWindows.length > 0 ? allWindows[0] : null;
|
|
250
|
+
});
|
|
251
|
+
}
|
|
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
|
+
function setupMain($fetch, $store, getCookie, opts, clientOptions, cfg) {
|
|
257
|
+
if (!isProcessType("browser")) throw new BetterAuthError("setupMain can only be called in the main process.");
|
|
258
|
+
if (!cfg || cfg.csp === true) setupCSP(clientOptions);
|
|
259
|
+
if (!cfg || cfg.scheme === true) registerProtocolScheme($fetch, opts, withGetWindowFallback(cfg?.getWindow));
|
|
260
|
+
if (!cfg || cfg.bridges === true) setupBridges({
|
|
261
|
+
$fetch,
|
|
262
|
+
$store,
|
|
263
|
+
getCookie
|
|
264
|
+
}, opts, clientOptions);
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Handles the deep link URL for authentication.
|
|
268
|
+
*/
|
|
269
|
+
async function handleDeepLink({ $fetch, options, url, getWindow }) {
|
|
270
|
+
if (!isProcessType("browser")) throw new BetterAuthError("`handleDeepLink` can only be called in the main process.");
|
|
271
|
+
let parsedURL = null;
|
|
272
|
+
try {
|
|
273
|
+
parsedURL = new URL(url);
|
|
274
|
+
} catch {}
|
|
275
|
+
if (!parsedURL) return;
|
|
276
|
+
const { scheme } = parseProtocolScheme(options.protocol);
|
|
277
|
+
if (!url.startsWith(`${scheme}:/`)) return;
|
|
278
|
+
const { protocol, pathname, hostname, hash } = parsedURL;
|
|
279
|
+
if (protocol !== `${scheme}:`) return;
|
|
280
|
+
if ("/" + hostname + pathname !== (options.callbackPath || "/auth/callback")) return;
|
|
281
|
+
if (!hash.startsWith("#token=")) return;
|
|
282
|
+
await authenticate($fetch, options, { token: hash.substring(7) }, withGetWindowFallback(getWindow));
|
|
283
|
+
}
|
|
284
|
+
function registerProtocolScheme($fetch, options, getWindow) {
|
|
285
|
+
const { scheme, privileges = {} } = typeof options.protocol === "string" ? { scheme: options.protocol } : options.protocol;
|
|
286
|
+
protocol.registerSchemesAsPrivileged([{
|
|
287
|
+
scheme,
|
|
288
|
+
privileges: {
|
|
289
|
+
standard: false,
|
|
290
|
+
secure: true,
|
|
291
|
+
...privileges
|
|
292
|
+
}
|
|
293
|
+
}]);
|
|
294
|
+
let hasSetupProtocolClient = false;
|
|
295
|
+
if (process?.defaultApp) {
|
|
296
|
+
if (process.argv.length >= 2 && typeof process.argv[1] === "string") hasSetupProtocolClient = app$1.setAsDefaultProtocolClient(scheme, process.execPath, [resolve(process.argv[1])]);
|
|
297
|
+
} else hasSetupProtocolClient = app$1.setAsDefaultProtocolClient(scheme);
|
|
298
|
+
if (!hasSetupProtocolClient) console.error(`Failed to register protocol ${scheme} as default protocol client.`);
|
|
299
|
+
if (!app$1.requestSingleInstanceLock()) app$1.quit();
|
|
300
|
+
else {
|
|
301
|
+
app$1.on("second-instance", async (_event, commandLine, _workingDir, url) => {
|
|
302
|
+
const win = getWindow();
|
|
303
|
+
if (win) {
|
|
304
|
+
if (win.isMinimized()) win.restore();
|
|
305
|
+
win.focus();
|
|
306
|
+
}
|
|
307
|
+
if (!url) {
|
|
308
|
+
const maybeURL = commandLine.pop();
|
|
309
|
+
if (typeof maybeURL === "string" && maybeURL.trim() !== "") try {
|
|
310
|
+
url = new URL(maybeURL).toString();
|
|
311
|
+
} catch {}
|
|
312
|
+
}
|
|
313
|
+
if (process?.platform !== "darwin" && typeof url === "string") await handleDeepLink({
|
|
314
|
+
$fetch,
|
|
315
|
+
options,
|
|
316
|
+
url,
|
|
317
|
+
getWindow
|
|
318
|
+
});
|
|
319
|
+
});
|
|
320
|
+
app$1.on("open-url", async (_event, url) => {
|
|
321
|
+
if (process?.platform === "darwin") await handleDeepLink({
|
|
322
|
+
$fetch,
|
|
323
|
+
options,
|
|
324
|
+
url,
|
|
325
|
+
getWindow
|
|
326
|
+
});
|
|
327
|
+
});
|
|
328
|
+
app$1.whenReady().then(async () => {
|
|
329
|
+
if (process?.platform !== "darwin" && typeof process.argv[1] === "string") await handleDeepLink({
|
|
330
|
+
$fetch,
|
|
331
|
+
options,
|
|
332
|
+
url: process.argv[1],
|
|
333
|
+
getWindow
|
|
334
|
+
});
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
function setupCSP(clientOptions) {
|
|
339
|
+
app$1.whenReady().then(() => {
|
|
340
|
+
session.defaultSession.webRequest.onHeadersReceived((details, callback) => {
|
|
341
|
+
const origin = new URL(clientOptions?.baseURL || "", "http://localhost").origin;
|
|
342
|
+
const cspKey = Object.keys(details.responseHeaders || {}).find((k) => k.toLowerCase() === "content-security-policy");
|
|
343
|
+
if (!cspKey) return callback({ responseHeaders: {
|
|
344
|
+
...details.responseHeaders || {},
|
|
345
|
+
"content-security-policy": `connect-src 'self' ${origin}`
|
|
346
|
+
} });
|
|
347
|
+
const policy = details.responseHeaders?.[cspKey]?.toString() || "";
|
|
348
|
+
const csp = /* @__PURE__ */ new Map();
|
|
349
|
+
for (let token of policy.split(";")) {
|
|
350
|
+
token = token.trim();
|
|
351
|
+
if (!token || !/^[\x00-\x7f]*$/.test(token)) continue;
|
|
352
|
+
const [rawDirectiveName, ...directiveValue] = token.split(/\s+/);
|
|
353
|
+
const directiveName = rawDirectiveName?.toLowerCase();
|
|
354
|
+
if (!directiveName) continue;
|
|
355
|
+
if (csp.has(directiveName)) continue;
|
|
356
|
+
csp.set(directiveName, directiveValue);
|
|
357
|
+
}
|
|
358
|
+
if (csp.has("connect-src")) {
|
|
359
|
+
const values = csp.get("connect-src") || [];
|
|
360
|
+
if (!values.includes(origin)) values.push(origin);
|
|
361
|
+
csp.set("connect-src", values);
|
|
362
|
+
} else csp.set("connect-src", ["'self'", origin]);
|
|
363
|
+
callback({ responseHeaders: {
|
|
364
|
+
...details.responseHeaders,
|
|
365
|
+
"content-security-policy": Array.from(csp.entries()).map(([k, v]) => `${k} ${v.join(" ")}`).join("; ")
|
|
366
|
+
} });
|
|
367
|
+
});
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
//#endregion
|
|
372
|
+
//#region src/client.ts
|
|
373
|
+
const { app, safeStorage, webContents } = electron;
|
|
374
|
+
const storageAdapter = (storage) => {
|
|
375
|
+
return {
|
|
376
|
+
...storage,
|
|
377
|
+
getDecrypted: (name) => {
|
|
378
|
+
const item = storage.getItem(name);
|
|
379
|
+
if (!item || typeof item !== "string") return null;
|
|
380
|
+
return safeStorage.decryptString(Buffer.from(base64.decode(item)));
|
|
381
|
+
},
|
|
382
|
+
setEncrypted: (name, value) => {
|
|
383
|
+
return storage.setItem(name, base64.encode(safeStorage.encryptString(value)));
|
|
384
|
+
}
|
|
385
|
+
};
|
|
386
|
+
};
|
|
387
|
+
const electronClient = (options) => {
|
|
388
|
+
const opts = {
|
|
389
|
+
storagePrefix: "better-auth",
|
|
390
|
+
cookiePrefix: "better-auth",
|
|
391
|
+
channelPrefix: "better-auth",
|
|
392
|
+
callbackPath: "/auth/callback",
|
|
393
|
+
...options
|
|
394
|
+
};
|
|
395
|
+
const { scheme } = parseProtocolScheme(opts.protocol);
|
|
396
|
+
let store = null;
|
|
397
|
+
const cookieName = `${opts.storagePrefix}.cookie`;
|
|
398
|
+
const localCacheName = `${opts.storagePrefix}.local_cache`;
|
|
399
|
+
const { getDecrypted, setEncrypted } = storageAdapter(opts.storage);
|
|
400
|
+
if ((isDevelopment() || isTest()) && /^(?!\.)(?!.*\.\.)(?!.*\.$)[^.]+\.[^.]+$/.test(scheme)) console.warn("The provided scheme does not follow the reverse domain name notation. For example: `app.example.com` -> `com.example.app`.");
|
|
401
|
+
return {
|
|
402
|
+
id: "electron",
|
|
403
|
+
fetchPlugins: [{
|
|
404
|
+
id: "electron",
|
|
405
|
+
name: "Electron",
|
|
406
|
+
async init(url, options) {
|
|
407
|
+
if (!isProcessType("browser")) throw new Error("Requests must be made from the Electron main process");
|
|
408
|
+
const cookie = getCookie(getDecrypted(cookieName) || "{}");
|
|
409
|
+
options ||= {};
|
|
410
|
+
options.credentials = "omit";
|
|
411
|
+
options.headers = {
|
|
412
|
+
...options.headers,
|
|
413
|
+
cookie,
|
|
414
|
+
"user-agent": app.userAgentFallback,
|
|
415
|
+
"electron-origin": `${scheme}:/`,
|
|
416
|
+
"x-skip-oauth-proxy": "true"
|
|
417
|
+
};
|
|
418
|
+
if (url.endsWith("/sign-out")) {
|
|
419
|
+
setEncrypted(cookieName, "{}");
|
|
420
|
+
store?.atoms.session?.set({
|
|
421
|
+
...store.atoms.session.get(),
|
|
422
|
+
data: null,
|
|
423
|
+
error: null,
|
|
424
|
+
isPending: false
|
|
425
|
+
});
|
|
426
|
+
setEncrypted(localCacheName, "{}");
|
|
427
|
+
}
|
|
428
|
+
return {
|
|
429
|
+
url,
|
|
430
|
+
options
|
|
431
|
+
};
|
|
432
|
+
},
|
|
433
|
+
hooks: {
|
|
434
|
+
onSuccess: async (context) => {
|
|
435
|
+
const setCookie = context.response.headers.get("set-cookie");
|
|
436
|
+
if (setCookie) {
|
|
437
|
+
if (hasBetterAuthCookies(setCookie, opts.cookiePrefix)) {
|
|
438
|
+
const prevCookie = getDecrypted(cookieName);
|
|
439
|
+
const toSetCookie = getSetCookie(setCookie || "{}", prevCookie ?? void 0);
|
|
440
|
+
if (hasSessionCookieChanged(prevCookie, toSetCookie)) {
|
|
441
|
+
setEncrypted(cookieName, toSetCookie);
|
|
442
|
+
store?.notify("$sessionSignal");
|
|
443
|
+
} else setEncrypted(cookieName, toSetCookie);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
if (context.request.url.toString().includes("/get-session") && !opts.disableCache) {
|
|
447
|
+
const data = context.data;
|
|
448
|
+
setEncrypted(localCacheName, JSON.stringify(data));
|
|
449
|
+
}
|
|
450
|
+
},
|
|
451
|
+
onError: async (context) => {
|
|
452
|
+
webContents.getFocusedWebContents()?.send(`${getChannelPrefixWithDelimiter(opts.channelPrefix)}error`, {
|
|
453
|
+
...context.error,
|
|
454
|
+
path: context.request.url
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}],
|
|
459
|
+
getActions: ($fetch, $store, clientOptions) => {
|
|
460
|
+
store = $store;
|
|
461
|
+
const getCookieFn = () => {
|
|
462
|
+
return getCookie(getDecrypted(cookieName) || "{}");
|
|
463
|
+
};
|
|
464
|
+
return {
|
|
465
|
+
getCookie: getCookieFn,
|
|
466
|
+
requestAuth: (options) => requestAuth(clientOptions, opts, options),
|
|
467
|
+
setupRenderer: () => setupRenderer(opts),
|
|
468
|
+
setupMain: (cfg) => setupMain($fetch, store, getCookieFn, opts, clientOptions, cfg),
|
|
469
|
+
$Infer: {}
|
|
470
|
+
};
|
|
471
|
+
}
|
|
472
|
+
};
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
//#endregion
|
|
476
|
+
export { electronClient, handleDeepLink };
|