@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.
@@ -0,0 +1,205 @@
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 { Buffer } from "node:buffer";
5
+ import { createServer } from "node:http";
6
+ //#region src/adapters/electron.ts
7
+ const ELECTRON_AUTH_CHANNELS = {
8
+ requestAuth: "desktop:requestAuth",
9
+ getUser: "desktop:getUser",
10
+ signOut: "desktop:signOut",
11
+ getUserImage: "desktop:getUserImage",
12
+ onAuthenticated: "desktop:onAuthenticated",
13
+ onUserUpdated: "desktop:onUserUpdated",
14
+ onAuthError: "desktop:onAuthError"
15
+ };
16
+ async function loadElectron() {
17
+ const electron = await import("electron");
18
+ return {
19
+ shell: electron.shell,
20
+ ipcMain: electron.ipcMain
21
+ };
22
+ }
23
+ const ENC_PREFIX = "enc:";
24
+ async function electronStorage(opts = {}) {
25
+ const { name = "better-auth-desktop", encrypt = true } = opts;
26
+ const { default: Store } = await import("electron-store");
27
+ const store = new Store({ name });
28
+ let safeStorage = null;
29
+ if (encrypt) try {
30
+ const electron = await import("electron");
31
+ if (electron.safeStorage?.isEncryptionAvailable()) safeStorage = electron.safeStorage;
32
+ } catch {
33
+ safeStorage = null;
34
+ }
35
+ const seal = (value) => safeStorage ? ENC_PREFIX + safeStorage.encryptString(value).toString("base64") : value;
36
+ const open = (value) => {
37
+ if (!safeStorage || !value.startsWith(ENC_PREFIX)) return value;
38
+ return safeStorage.decryptString(Buffer.from(value.slice(4), "base64"));
39
+ };
40
+ return {
41
+ getItem: (key) => {
42
+ const raw = store.get(key);
43
+ if (typeof raw !== "string") return null;
44
+ try {
45
+ return open(raw);
46
+ } catch {
47
+ return null;
48
+ }
49
+ },
50
+ setItem: (key, value) => {
51
+ store.set(key, seal(String(value)));
52
+ }
53
+ };
54
+ }
55
+ function createLoopbackAdapter(shell, getWindow, storage) {
56
+ return {
57
+ openExternal: (url) => shell.openExternal(url),
58
+ serveLoopback(onRequest, opts) {
59
+ return new Promise((resolve) => {
60
+ const server = createServer(async (req, res) => {
61
+ const url = new URL(req.url ?? "/", "http://127.0.0.1");
62
+ const query = {};
63
+ url.searchParams.forEach((value, key) => {
64
+ query[key] = value;
65
+ });
66
+ const out = await onRequest({
67
+ path: url.pathname,
68
+ query
69
+ });
70
+ res.writeHead(out.status, out.headers);
71
+ res.end(out.body);
72
+ });
73
+ server.listen(opts?.port ?? 0, "127.0.0.1", () => {
74
+ const address = server.address();
75
+ resolve({
76
+ port: typeof address === "object" && address ? address.port : 0,
77
+ close: () => server.close()
78
+ });
79
+ });
80
+ });
81
+ },
82
+ notifyRenderer(event) {
83
+ const wc = getWindow()?.webContents;
84
+ if (!wc) return;
85
+ switch (event.type) {
86
+ case "authenticated":
87
+ wc.send(ELECTRON_AUTH_CHANNELS.onAuthenticated, event.user);
88
+ break;
89
+ case "user-updated":
90
+ wc.send(ELECTRON_AUTH_CHANNELS.onUserUpdated, event.user);
91
+ break;
92
+ case "error":
93
+ wc.send(ELECTRON_AUTH_CHANNELS.onAuthError, {
94
+ error: { message: event.error.message },
95
+ path: event.path
96
+ });
97
+ break;
98
+ }
99
+ },
100
+ storage
101
+ };
102
+ }
103
+ /**
104
+ * Better Auth client plugin for Electron (main process). Shares the same core
105
+ * as the Electrobun plugin; pulls in **no** Electrobun code. Use it with
106
+ * `createAuthClient`, then call `setupMain()` (which registers the IPC handlers
107
+ * and the session subscription, and returns a cleanup function):
108
+ *
109
+ * ```ts
110
+ * const authClient = createAuthClient({
111
+ * baseURL,
112
+ * plugins: [electronDesktop({ clientID, storage: await electronStorage(), getWindow })],
113
+ * });
114
+ * await authClient.setupMain();
115
+ * ```
116
+ */
117
+ const electronDesktop = (options) => {
118
+ const layer = createDesktopCookieLayer(options);
119
+ const sanitizeUser = async (user) => {
120
+ if (user !== null && typeof options.sanitizeUser === "function") try {
121
+ return await options.sanitizeUser(user);
122
+ } catch (error) {
123
+ console.error("Error while sanitizing user", error);
124
+ return null;
125
+ }
126
+ return user;
127
+ };
128
+ return {
129
+ id: "desktop",
130
+ version: PACKAGE_VERSION,
131
+ fetchPlugins: [layer.fetchPlugin],
132
+ getActions: ($fetch, $store, clientOptions) => {
133
+ layer.bindStore($store);
134
+ layer.bindServerOrigin(clientOptions?.baseURL);
135
+ const getCookie = layer.getCookie;
136
+ return {
137
+ getCookie,
138
+ setupMain: async () => {
139
+ const { shell, ipcMain } = await loadElectron();
140
+ const adapter = createLoopbackAdapter(shell, options.getWindow, options.storage);
141
+ ipcMain.handle(ELECTRON_AUTH_CHANNELS.requestAuth, async (_event, cfg) => {
142
+ await startAuthFlow({
143
+ adapter,
144
+ $fetch,
145
+ clientOptions,
146
+ options,
147
+ cfg,
148
+ onAuthenticated: (user) => adapter.notifyRenderer({
149
+ type: "authenticated",
150
+ user
151
+ }),
152
+ onError: (error) => adapter.notifyRenderer({
153
+ type: "error",
154
+ error: error instanceof Error ? error : new Error(String(error)),
155
+ path: "/desktop/init-oauth-proxy"
156
+ })
157
+ });
158
+ });
159
+ ipcMain.handle(ELECTRON_AUTH_CHANNELS.getUser, async () => {
160
+ return await sanitizeUser((await $fetch("/get-session", {
161
+ method: "GET",
162
+ headers: {
163
+ cookie: getCookie(),
164
+ "content-type": "application/json"
165
+ }
166
+ })).data?.user ?? null);
167
+ });
168
+ ipcMain.handle(ELECTRON_AUTH_CHANNELS.signOut, async () => {
169
+ await $fetch("/sign-out", {
170
+ method: "POST",
171
+ body: "{}",
172
+ headers: {
173
+ cookie: getCookie(),
174
+ "content-type": "application/json"
175
+ }
176
+ });
177
+ });
178
+ ipcMain.handle(ELECTRON_AUTH_CHANNELS.getUserImage, async (_event, url) => {
179
+ const result = await fetchUserImage(clientOptions?.baseURL, url);
180
+ if (!result) return { dataUrl: null };
181
+ const { base64 } = await import("@better-auth/utils/base64");
182
+ return { dataUrl: `data:${result.mimeType};base64,${base64.encode(result.bytes)}` };
183
+ });
184
+ const unsub = $store.atoms.session?.subscribe(async (state) => {
185
+ if (state.isPending === true) return;
186
+ const user = await sanitizeUser(state.data?.user ?? null);
187
+ adapter.notifyRenderer({
188
+ type: "user-updated",
189
+ user
190
+ });
191
+ });
192
+ return () => {
193
+ unsub?.();
194
+ ipcMain.removeHandler(ELECTRON_AUTH_CHANNELS.requestAuth);
195
+ ipcMain.removeHandler(ELECTRON_AUTH_CHANNELS.getUser);
196
+ ipcMain.removeHandler(ELECTRON_AUTH_CHANNELS.signOut);
197
+ ipcMain.removeHandler(ELECTRON_AUTH_CHANNELS.getUserImage);
198
+ };
199
+ }
200
+ };
201
+ }
202
+ };
203
+ };
204
+ //#endregion
205
+ export { ELECTRON_AUTH_CHANNELS, electronDesktop, electronStorage };
@@ -0,0 +1,34 @@
1
+ import { i as DesktopClientOptions, l as Storage } from "./types-h9AEfSnq.mjs";
2
+ import { BetterFetch, BetterFetchPlugin } from "@better-fetch/fetch";
3
+ import { BetterAuthClientOptions, ClientStore } from "@better-auth/core";
4
+
5
+ //#region src/core/client.d.ts
6
+ type DesktopClientPluginOptions = DesktopClientOptions & {
7
+ storage: Storage;
8
+ };
9
+ type DesktopClientInternals = {
10
+ $fetch: BetterFetch;
11
+ $store: ClientStore;
12
+ clientOptions: BetterAuthClientOptions | undefined;
13
+ getCookie: () => string;
14
+ clearSession: () => void;
15
+ };
16
+ type DesktopCookieLayer = {
17
+ fetchPlugin: BetterFetchPlugin;
18
+ getCookie: () => string;
19
+ clearSession: () => void;
20
+ bindStore: (store: ClientStore) => void;
21
+ bindServerOrigin: (baseURL: string | undefined) => void;
22
+ };
23
+ declare function createDesktopCookieLayer(options: DesktopClientPluginOptions): DesktopCookieLayer;
24
+ declare const desktopClient: (options: DesktopClientPluginOptions) => {
25
+ id: "desktop";
26
+ version: string;
27
+ fetchPlugins: BetterFetchPlugin[];
28
+ getActions: ($fetch: BetterFetch, $store: ClientStore, clientOptions: BetterAuthClientOptions | undefined) => {
29
+ getCookie: () => string;
30
+ getDesktopInternals: () => DesktopClientInternals;
31
+ };
32
+ };
33
+ //#endregion
34
+ export { desktopClient as a, createDesktopCookieLayer as i, DesktopClientPluginOptions as n, DesktopCookieLayer as r, DesktopClientInternals as t };
@@ -0,0 +1,157 @@
1
+ import { t as PACKAGE_VERSION } from "./version-1hEssvcU.mjs";
2
+ import { cookieNameRegex, parseSetCookieHeader } from "better-auth/cookies";
3
+ //#region src/core/cookies.ts
4
+ function getSetCookie(header, prevCookie) {
5
+ const parsed = parseSetCookieHeader(header);
6
+ let toSetCookie = {};
7
+ parsed.forEach((cookie, key) => {
8
+ const expiresAt = cookie.expires;
9
+ const maxAge = cookie["max-age"];
10
+ const expires = maxAge ? new Date(Date.now() + Number(maxAge) * 1e3) : expiresAt ? new Date(String(expiresAt)) : null;
11
+ toSetCookie[key] = {
12
+ value: cookie.value,
13
+ expires: expires ? expires.toISOString() : null
14
+ };
15
+ });
16
+ if (prevCookie) try {
17
+ toSetCookie = {
18
+ ...JSON.parse(prevCookie),
19
+ ...toSetCookie
20
+ };
21
+ } catch {}
22
+ return JSON.stringify(toSetCookie);
23
+ }
24
+ function getCookie(cookie) {
25
+ let parsed = {};
26
+ try {
27
+ parsed = JSON.parse(cookie);
28
+ } catch (_e) {}
29
+ const pairs = [];
30
+ for (const [key, value] of Object.entries(parsed)) {
31
+ if (value.expires && new Date(value.expires) < /* @__PURE__ */ new Date()) continue;
32
+ if (!cookieNameRegex.test(key)) continue;
33
+ pairs.push(`${key}=${encodeURIComponent(value.value)}`);
34
+ }
35
+ return pairs.join("; ");
36
+ }
37
+ function hasBetterAuthCookies(setCookieHeader, cookiePrefix) {
38
+ const cookies = parseSetCookieHeader(setCookieHeader);
39
+ const cookieSuffixes = ["session_token", "session_data"];
40
+ const prefixes = Array.isArray(cookiePrefix) ? cookiePrefix : [cookiePrefix];
41
+ for (const name of cookies.keys()) {
42
+ const nameWithoutSecure = name.startsWith("__Secure-") ? name.slice(9) : name;
43
+ for (const prefix of prefixes) if (prefix) {
44
+ if (nameWithoutSecure.startsWith(prefix)) return true;
45
+ } else for (const suffix of cookieSuffixes) if (nameWithoutSecure.endsWith(suffix)) return true;
46
+ }
47
+ return false;
48
+ }
49
+ //#endregion
50
+ //#region src/core/client.ts
51
+ const USER_AGENT = `better-auth-desktop/${PACKAGE_VERSION}`;
52
+ function createDesktopCookieLayer(options) {
53
+ const opts = {
54
+ storagePrefix: "better-auth",
55
+ cookiePrefix: "better-auth",
56
+ ...options
57
+ };
58
+ const cookieName = `${opts.storagePrefix}.cookie`;
59
+ const localCacheName = `${opts.storagePrefix}.local_cache`;
60
+ const getStored = (name) => {
61
+ const item = opts.storage.getItem(name);
62
+ return typeof item === "string" ? item : null;
63
+ };
64
+ const setStored = (name, value) => {
65
+ try {
66
+ opts.storage.setItem(name, value);
67
+ } catch {}
68
+ };
69
+ let store = null;
70
+ let serverOrigin;
71
+ const originOf = (url) => {
72
+ if (serverOrigin) return serverOrigin;
73
+ try {
74
+ return new URL(url).origin;
75
+ } catch {
76
+ return "";
77
+ }
78
+ };
79
+ const getCookieFn = () => getCookie(getStored(cookieName) || "");
80
+ const clearSession = () => {
81
+ setStored(cookieName, "");
82
+ setStored(localCacheName, "");
83
+ store?.atoms.session?.set({
84
+ ...store.atoms.session.get(),
85
+ data: null,
86
+ error: null,
87
+ isPending: false
88
+ });
89
+ };
90
+ return {
91
+ fetchPlugin: {
92
+ id: "desktop",
93
+ name: "Desktop",
94
+ async init(url, fetchOptions) {
95
+ const resolvedOptions = fetchOptions ?? {};
96
+ resolvedOptions.credentials = "omit";
97
+ resolvedOptions.headers = {
98
+ ...resolvedOptions.headers,
99
+ cookie: getCookieFn(),
100
+ "user-agent": USER_AGENT,
101
+ "desktop-origin": originOf(url),
102
+ "x-skip-oauth-proxy": "true"
103
+ };
104
+ if (url.endsWith("/sign-out")) clearSession();
105
+ return {
106
+ url,
107
+ options: resolvedOptions
108
+ };
109
+ },
110
+ hooks: { onSuccess: async (context) => {
111
+ const setCookie = context.response.headers.get("set-cookie");
112
+ if (setCookie && hasBetterAuthCookies(setCookie, opts.cookiePrefix)) {
113
+ setStored(cookieName, getSetCookie(setCookie, getStored(cookieName) ?? void 0));
114
+ store?.notify("$sessionSignal");
115
+ }
116
+ const requestUrl = context.request.url.toString();
117
+ if (requestUrl.includes("/get-session") && !opts.disableCache) setStored(localCacheName, JSON.stringify(context.data));
118
+ if (requestUrl.includes("/sign-out")) clearSession();
119
+ } }
120
+ },
121
+ getCookie: getCookieFn,
122
+ clearSession,
123
+ bindStore: (s) => {
124
+ store = s;
125
+ },
126
+ bindServerOrigin: (baseURL) => {
127
+ if (!baseURL) return;
128
+ try {
129
+ serverOrigin = new URL(baseURL).origin;
130
+ } catch {}
131
+ }
132
+ };
133
+ }
134
+ const desktopClient = (options) => {
135
+ const layer = createDesktopCookieLayer(options);
136
+ return {
137
+ id: "desktop",
138
+ version: PACKAGE_VERSION,
139
+ fetchPlugins: [layer.fetchPlugin],
140
+ getActions: ($fetch, $store, clientOptions) => {
141
+ layer.bindStore($store);
142
+ layer.bindServerOrigin(clientOptions?.baseURL);
143
+ return {
144
+ getCookie: layer.getCookie,
145
+ getDesktopInternals: () => ({
146
+ $fetch,
147
+ $store,
148
+ clientOptions,
149
+ getCookie: layer.getCookie,
150
+ clearSession: layer.clearSession
151
+ })
152
+ };
153
+ }
154
+ };
155
+ };
156
+ //#endregion
157
+ export { desktopClient as n, createDesktopCookieLayer as t };
@@ -0,0 +1,3 @@
1
+ import { i as DesktopClientOptions, l as Storage } from "./types-h9AEfSnq.mjs";
2
+ import { a as desktopClient, n as DesktopClientPluginOptions, t as DesktopClientInternals } from "./client-De_zQzE1.mjs";
3
+ export { type DesktopClientInternals, type DesktopClientOptions, type DesktopClientPluginOptions, type Storage, desktopClient };
@@ -0,0 +1,2 @@
1
+ import { n as desktopClient } from "./client-DhRcoap2.mjs";
2
+ export { desktopClient };
@@ -0,0 +1,6 @@
1
+ import { a as LoopbackRequest, c as RequestAuthOptions, i as DesktopClientOptions, l as Storage, n as AuthUser, o as LoopbackResponse, r as DesktopAdapter, s as LoopbackServer, t as AuthEvent } from "../types-h9AEfSnq.mjs";
2
+ import { a as desktopClient, i as createDesktopCookieLayer, n as DesktopClientPluginOptions, r as DesktopCookieLayer, t as DesktopClientInternals } from "../client-De_zQzE1.mjs";
3
+ import { n as keychainStorage, t as KeychainStorageOptions } from "../storage-DtgX_vmE.mjs";
4
+ import { a as StartAuthFlowArgs, i as ExchangeTokenArgs, n as fetchUserImage, o as exchangeToken, r as normalizeUserOutput, s as startAuthFlow, t as FetchUserImageResult } from "../index-CP_2Wlut.mjs";
5
+ import { a as isAllowedLoopbackPort, i as generateNonce, n as LOOPBACK_HOST, o as parseLoopbackUrl, r as buildLoopbackUrl, s as successPage, t as AllowedLoopbackPorts } from "../loopback-ClkAoS2u.mjs";
6
+ export { AllowedLoopbackPorts, AuthEvent, AuthUser, DesktopAdapter, DesktopClientInternals, DesktopClientOptions, DesktopClientPluginOptions, DesktopCookieLayer, ExchangeTokenArgs, FetchUserImageResult, KeychainStorageOptions, LOOPBACK_HOST, LoopbackRequest, LoopbackResponse, LoopbackServer, RequestAuthOptions, StartAuthFlowArgs, Storage, buildLoopbackUrl, createDesktopCookieLayer, desktopClient, exchangeToken, fetchUserImage, generateNonce, isAllowedLoopbackPort, keychainStorage, normalizeUserOutput, parseLoopbackUrl, startAuthFlow, successPage };
@@ -0,0 +1,5 @@
1
+ import { n as desktopClient, t as createDesktopCookieLayer } from "../client-DhRcoap2.mjs";
2
+ import { a as parseLoopbackUrl, i as isAllowedLoopbackPort, n as buildLoopbackUrl, o as successPage, r as generateNonce, t as LOOPBACK_HOST } from "../loopback-DTQgg1Za.mjs";
3
+ import { i as normalizeUserOutput, n as startAuthFlow, r as fetchUserImage, t as exchangeToken } from "../exchange-DCZ0j_T5.mjs";
4
+ import { t as keychainStorage } from "../storage-BcwDmS-W.mjs";
5
+ export { LOOPBACK_HOST, buildLoopbackUrl, createDesktopCookieLayer, desktopClient, exchangeToken, fetchUserImage, generateNonce, isAllowedLoopbackPort, keychainStorage, normalizeUserOutput, parseLoopbackUrl, startAuthFlow, successPage };
@@ -0,0 +1,217 @@
1
+ import { n as buildLoopbackUrl, o as successPage, r as generateNonce } from "./loopback-DTQgg1Za.mjs";
2
+ import { BetterAuthError } from "@better-auth/core/error";
3
+ import { base64, base64Url } from "@better-auth/utils/base64";
4
+ import { createHash } from "@better-auth/utils/hash";
5
+ import { getBaseURL, safeJSONParse } from "better-auth";
6
+ import { generateRandomString } from "better-auth/crypto";
7
+ //#region src/core/user.ts
8
+ const DEFAULT_MAX_BYTES = 1024 * 1024 * 5;
9
+ async function fetchUserImage(baseURL, url) {
10
+ const decoded = await decodeDataImageUrl(url);
11
+ if (decoded) return {
12
+ bytes: decoded.bytes,
13
+ mimeType: decoded.mimeType
14
+ };
15
+ let resolvedUrl;
16
+ try {
17
+ let parsed;
18
+ try {
19
+ parsed = new URL(url);
20
+ } catch {
21
+ if (!baseURL) return null;
22
+ const base = baseURL.endsWith("/") ? baseURL : `${baseURL}/`;
23
+ const relative = url.startsWith("/") ? url.slice(1) : url;
24
+ parsed = new URL(relative, base);
25
+ }
26
+ if (parsed.protocol !== "http:" && parsed.protocol !== "https:") return null;
27
+ resolvedUrl = parsed.href;
28
+ } catch {
29
+ return null;
30
+ }
31
+ const response = await fetch(resolvedUrl, {
32
+ method: "GET",
33
+ headers: { accept: "image/*" }
34
+ });
35
+ if (!response.ok) return null;
36
+ const contentType = response.headers.get("content-type");
37
+ if (!contentType?.startsWith("image/") || contentType.startsWith("image/svg")) return null;
38
+ const contentLength = response.headers.get("content-length");
39
+ if (contentLength && Number(contentLength) > DEFAULT_MAX_BYTES) return null;
40
+ const buf = await response.arrayBuffer();
41
+ const bytes = new Uint8Array(buf);
42
+ if (bytes.byteLength > DEFAULT_MAX_BYTES) return null;
43
+ return {
44
+ bytes,
45
+ mimeType: contentType.split(";")[0]?.trim() || "image/png"
46
+ };
47
+ }
48
+ function normalizeUserOutput(user, _options) {
49
+ return { ...user };
50
+ }
51
+ async function decodeDataImageUrl(url) {
52
+ const maxBase64Size = Math.ceil(DEFAULT_MAX_BYTES * 4 / 3);
53
+ const lower = url.toLowerCase();
54
+ if (!lower.startsWith("data:image/") || lower.startsWith("data:image/svg")) return null;
55
+ const markerIdx = lower.indexOf(";base64,");
56
+ if (markerIdx === -1) return null;
57
+ const mimeType = url.substring(5, markerIdx);
58
+ const payload = url.substring(markerIdx + 8);
59
+ if (!payload || payload.length > maxBase64Size) return null;
60
+ try {
61
+ const bytes = base64.decode(payload);
62
+ if (!detectImageType(bytes)) return null;
63
+ return {
64
+ bytes,
65
+ mimeType
66
+ };
67
+ } catch {
68
+ return null;
69
+ }
70
+ }
71
+ function detectImageType(bytes) {
72
+ if (bytes.length < 12) return null;
73
+ if (bytes[0] === 137 && bytes[1] === 80 && bytes[2] === 78 && bytes[3] === 71 && bytes[4] === 13 && bytes[5] === 10 && bytes[6] === 26 && bytes[7] === 10) return "image/png";
74
+ if (bytes[0] === 255 && bytes[1] === 216 && bytes[2] === 255) return "image/jpg";
75
+ if (bytes[0] === 71 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 56 && (bytes[4] === 55 || bytes[4] === 57) && bytes[5] === 97) return "image/gif";
76
+ if (bytes.length >= 12 && bytes[0] === 82 && bytes[1] === 73 && bytes[2] === 70 && bytes[3] === 70 && bytes[8] === 87 && bytes[9] === 69 && bytes[10] === 66 && bytes[11] === 80) return "image/webp";
77
+ if (bytes[0] === 66 && bytes[1] === 77) return "image/bmp";
78
+ if (bytes[0] === 73 && bytes[1] === 73 && bytes[2] === 42 && bytes[3] === 0 || bytes[0] === 77 && bytes[1] === 77 && bytes[2] === 0 && bytes[3] === 42) return "image/tiff";
79
+ if (bytes[0] === 0 && bytes[1] === 0 && bytes[2] === 1 && bytes[3] === 0) return "image/x-icon";
80
+ if (bytes.length < 16) return null;
81
+ if (String.fromCharCode(...bytes.slice(4, 8)) !== "ftyp") return null;
82
+ const brand = String.fromCharCode(...bytes.slice(8, 12));
83
+ if (brand === "avif" || brand === "heic" || brand === "heif") return `image/${brand}`;
84
+ if (brand === "heix" || brand === "hevc" || brand === "mif1" || brand === "msf1") return "image/heic";
85
+ return null;
86
+ }
87
+ //#endregion
88
+ //#region src/core/exchange.ts
89
+ const verifierStore = /* @__PURE__ */ new Map();
90
+ const DEFAULT_LOOPBACK_PATH = "/callback";
91
+ const DEFAULT_LOOPBACK_TIMEOUT = 3e5;
92
+ function randomVerifier() {
93
+ const bytes = new Uint8Array(32);
94
+ crypto.getRandomValues(bytes);
95
+ return base64Url.encode(bytes);
96
+ }
97
+ function loopbackSuccessResponse(success) {
98
+ if (success && typeof success === "object") return {
99
+ status: 302,
100
+ headers: { location: success.redirectTo },
101
+ body: ""
102
+ };
103
+ return {
104
+ status: 200,
105
+ headers: { "content-type": "text/html; charset=utf-8" },
106
+ body: success ?? successPage()
107
+ };
108
+ }
109
+ async function sanitize(user, options) {
110
+ let u = user;
111
+ if (u !== null && typeof options.sanitizeUser === "function") try {
112
+ u = await options.sanitizeUser(u);
113
+ } catch (error) {
114
+ console.error("Error while sanitizing user", error);
115
+ u = null;
116
+ }
117
+ if (u !== null) u = normalizeUserOutput(u, options);
118
+ return u;
119
+ }
120
+ async function exchangeToken({ $fetch, options, token, onAuthenticated, fetchOptions }) {
121
+ const decoded = safeJSONParse(new TextDecoder().decode(base64Url.decode(decodeURIComponent(token))));
122
+ const codeVerifier = decoded ? verifierStore.get(decoded.state) : void 0;
123
+ if (decoded) verifierStore.delete(decoded.state);
124
+ if (!decoded || !codeVerifier) throw new BetterAuthError("Code verifier not found.");
125
+ return await $fetch("/desktop/token", {
126
+ ...fetchOptions,
127
+ method: "POST",
128
+ body: {
129
+ ...fetchOptions?.body || {},
130
+ token: decoded.identifier,
131
+ state: decoded.state,
132
+ code_verifier: codeVerifier
133
+ },
134
+ onSuccess: async (ctx) => {
135
+ const user = await sanitize(ctx.data?.user ?? null, options);
136
+ if (user === null) return;
137
+ await fetchOptions?.onSuccess?.(ctx);
138
+ onAuthenticated(user);
139
+ }
140
+ });
141
+ }
142
+ async function startAuthFlow({ adapter, $fetch, clientOptions, options, cfg, onAuthenticated, onError }) {
143
+ const baseURL = getBaseURL(clientOptions?.baseURL, clientOptions?.basePath, void 0, true);
144
+ if (!baseURL) throw new BetterAuthError("Base URL is required to start a desktop sign-in flow.");
145
+ const state = generateRandomString(16, "A-Z", "a-z", "0-9");
146
+ const codeVerifier = randomVerifier();
147
+ const codeChallenge = base64Url.encode(await createHash("SHA-256").digest(codeVerifier));
148
+ verifierStore.set(state, codeVerifier);
149
+ const nonce = generateNonce();
150
+ const loopbackPath = options.loopbackPath ?? DEFAULT_LOOPBACK_PATH;
151
+ let server = null;
152
+ let timer = null;
153
+ const cleanup = () => {
154
+ if (timer) clearTimeout(timer);
155
+ timer = null;
156
+ server?.close();
157
+ server = null;
158
+ verifierStore.delete(state);
159
+ };
160
+ server = await adapter.serveLoopback(async (req) => {
161
+ if (req.path !== loopbackPath) return {
162
+ status: 404,
163
+ body: "Not found"
164
+ };
165
+ if (req.query.nonce !== nonce) return {
166
+ status: 403,
167
+ body: "Forbidden"
168
+ };
169
+ const token = req.query.token;
170
+ if (!token) return {
171
+ status: 400,
172
+ body: "Missing token"
173
+ };
174
+ try {
175
+ await exchangeToken({
176
+ $fetch,
177
+ options,
178
+ token,
179
+ onAuthenticated,
180
+ fetchOptions: { throw: true }
181
+ });
182
+ } catch (error) {
183
+ cleanup();
184
+ onError?.(error);
185
+ return {
186
+ status: 500,
187
+ headers: { "content-type": "text/plain; charset=utf-8" },
188
+ body: "Sign-in failed. You can close this tab."
189
+ };
190
+ }
191
+ cleanup();
192
+ return loopbackSuccessResponse(options.loopbackSuccess);
193
+ }, { port: options.loopbackPort });
194
+ const loopbackUrl = buildLoopbackUrl(server.port, loopbackPath, nonce);
195
+ const url = new URL(`${baseURL}/desktop/init-oauth-proxy`);
196
+ for (const [key, value] of Object.entries(cfg)) {
197
+ if (value === void 0) continue;
198
+ url.searchParams.set(key, typeof value === "string" ? value : JSON.stringify(value));
199
+ }
200
+ url.searchParams.set("client_id", options.clientID ?? "desktop");
201
+ url.searchParams.set("code_challenge", codeChallenge);
202
+ url.searchParams.set("code_challenge_method", "S256");
203
+ url.searchParams.set("state", state);
204
+ url.searchParams.set("callbackURL", loopbackUrl);
205
+ timer = setTimeout(() => {
206
+ cleanup();
207
+ onError?.(new BetterAuthError("Desktop sign-in timed out."));
208
+ }, options.loopbackTimeout ?? DEFAULT_LOOPBACK_TIMEOUT);
209
+ try {
210
+ await adapter.openExternal(url.toString());
211
+ } catch (error) {
212
+ cleanup();
213
+ throw error;
214
+ }
215
+ }
216
+ //#endregion
217
+ export { normalizeUserOutput as i, startAuthFlow as n, fetchUserImage as r, exchangeToken as t };