@hammadj/better-auth-expo 1.5.0-beta.9

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 ADDED
@@ -0,0 +1,20 @@
1
+ The MIT License (MIT)
2
+ Copyright (c) 2024 - present, Bereket Engida
3
+
4
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
5
+ this software and associated documentation files (the “Software”), to deal in
6
+ the Software without restriction, including without limitation the rights to
7
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
8
+ the Software, and to permit persons to whom the Software is furnished to do so,
9
+ subject to the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be included in all
12
+ copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
16
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
17
+ IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
18
+ DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
19
+ ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
20
+ DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,106 @@
1
+ # Better Auth Expo Plugin
2
+
3
+ This plugin integrates Better Auth with Expo, allowing you to easily add
4
+ authentication to your Expo (React Native) applications.
5
+ It supports both Expo native and web apps.
6
+
7
+ ## Installation
8
+
9
+ To get started, install the necessary packages:
10
+
11
+ ```bash
12
+ # Using npm
13
+ npm install better-auth @better-auth/expo
14
+
15
+ # Using yarn
16
+ yarn add better-auth @better-auth/expo
17
+
18
+ # Using pnpm
19
+ pnpm add better-auth @better-auth/expo
20
+
21
+ # Using bun
22
+ bun add better-auth @better-auth/expo
23
+ ```
24
+
25
+ You will also need to install `expo-secure-store` for secure session and cookie
26
+ storage in your Expo app:
27
+
28
+ ```bash
29
+ npm install expo-secure-store
30
+ # or
31
+ yarn add expo-secure-store
32
+ # or
33
+ pnpm add expo-secure-store
34
+ # or
35
+ bun add expo-secure-store
36
+ ```
37
+
38
+ ## Basic Usage
39
+
40
+ ### Configure the Better Auth Backend
41
+
42
+ Ensure you have a Better Auth backend set up.
43
+ You can follow the main [Installation Guide][].
44
+
45
+ Then, add the Expo plugin to your Better Auth server configuration (e.g., in
46
+ your `auth.ts` or `lib/auth.ts` file):
47
+
48
+ ```typescript
49
+ // lib/auth.ts
50
+ import { betterAuth } from 'better-auth';
51
+ import { expo } from '@better-auth/expo'; // Import the server plugin
52
+
53
+ export const auth = betterAuth({
54
+ // ...your other Better Auth options
55
+ baseURL: 'http://localhost:8081', // The base URL of your application server where the routes are mounted.
56
+ plugins: [expo()], // Add the Expo server plugin
57
+ emailAndPassword: {
58
+ enabled: true,
59
+ },
60
+ // Add other configurations like trustedOrigins
61
+ trustedOrigins: ['myapp://'], // Replace "myapp" with your app's scheme
62
+ });
63
+ ```
64
+
65
+ ### Initialize the Better Auth Client in Expo
66
+
67
+ In your Expo app, initialize the client (e.g., in `lib/auth-client.ts`):
68
+
69
+ ```typescript
70
+ // lib/auth-client.ts
71
+ import { createAuthClient } from 'better-auth/react';
72
+ import { expoClient } from '@better-auth/expo/client'; // Import the client plugin
73
+ import * as SecureStore from 'expo-secure-store';
74
+
75
+ export const authClient = createAuthClient({
76
+ baseURL: 'http://localhost:8081', // Your Better Auth backend URL
77
+ plugins: [
78
+ expoClient({
79
+ scheme: 'myapp', // Your app's scheme (defined in app.json)
80
+ storagePrefix: 'myapp', // A prefix for storage keys
81
+ storage: SecureStore, // Pass SecureStore for token storage
82
+ }),
83
+ ],
84
+ });
85
+
86
+ // You can also export specific methods if you prefer:
87
+ // export const { signIn, signUp, useSession } = authClient;
88
+ ```
89
+
90
+ Make sure your app’s scheme (e.g., “myapp”) is defined in your `app.json`.
91
+
92
+ ## Documentation
93
+
94
+ For more detailed information and advanced configurations, please refer to the
95
+ documentation:
96
+
97
+ * **Main Better Auth Installation:** [Installation Guide][]
98
+ * **Expo Integration Guide:** [Expo Integration Guide][]
99
+
100
+ ## License
101
+
102
+ MIT
103
+
104
+ [expo integration guide]: https://www.better-auth.com/docs/integrations/expo
105
+
106
+ [installation guide]: https://www.better-auth.com/docs/installation
@@ -0,0 +1,93 @@
1
+ import { parseSetCookieHeader } from "better-auth/cookies";
2
+ import { FocusManager, OnlineManager } from "better-auth/client";
3
+ import * as expo_web_browser0 from "expo-web-browser";
4
+
5
+ //#region src/focus-manager.d.ts
6
+ declare function setupExpoFocusManager(): FocusManager;
7
+ //#endregion
8
+ //#region src/online-manager.d.ts
9
+ declare function setupExpoOnlineManager(): OnlineManager;
10
+ //#endregion
11
+ //#region src/client.d.ts
12
+ interface ExpoClientOptions {
13
+ scheme?: string | undefined;
14
+ storage: {
15
+ setItem: (key: string, value: string) => any;
16
+ getItem: (key: string) => string | null;
17
+ };
18
+ /**
19
+ * Prefix for local storage keys (e.g., "my-app_cookie", "my-app_session_data")
20
+ * @default "better-auth"
21
+ */
22
+ storagePrefix?: string | undefined;
23
+ /**
24
+ * Prefix(es) for server cookie names to filter (e.g., "better-auth.session_token")
25
+ * This is used to identify which cookies belong to better-auth to prevent
26
+ * infinite refetching when third-party cookies are set.
27
+ * Can be a single string or an array of strings to match multiple prefixes.
28
+ * @default "better-auth"
29
+ * @example "better-auth"
30
+ * @example ["better-auth", "my-app"]
31
+ */
32
+ cookiePrefix?: string | string[] | undefined;
33
+ disableCache?: boolean | undefined;
34
+ /**
35
+ * Options to customize the Expo web browser behavior when opening authentication
36
+ * sessions. These are passed directly to `expo-web-browser`'s
37
+ * `Browser.openBrowserAsync`.
38
+ *
39
+ * For example, on iOS you can use `{ preferEphemeralSession: true }` to prevent
40
+ * the authentication session from sharing cookies with the user's default
41
+ * browser session:
42
+ *
43
+ * ```ts
44
+ * const client = createClient({
45
+ * expo: {
46
+ * webBrowserOptions: {
47
+ * preferEphemeralSession: true,
48
+ * },
49
+ * },
50
+ * });
51
+ * ```
52
+ */
53
+ webBrowserOptions?: expo_web_browser0.AuthSessionOpenOptions;
54
+ }
55
+ declare function getSetCookie(header: string, prevCookie?: string | undefined): string;
56
+ declare function getCookie(cookie: string): string;
57
+ /**
58
+ * Check if the Set-Cookie header contains better-auth cookies.
59
+ * This prevents infinite refetching when non-better-auth cookies (like third-party cookies) change.
60
+ *
61
+ * Supports multiple cookie naming patterns:
62
+ * - Default: "better-auth.session_token", "better-auth-passkey", "__Secure-better-auth.session_token"
63
+ * - Custom prefix: "myapp.session_token", "myapp-passkey", "__Secure-myapp.session_token"
64
+ * - Custom full names: "my_custom_session_token", "custom_session_data"
65
+ * - No prefix (cookiePrefix=""): matches any cookie with known suffixes
66
+ * - Multiple prefixes: ["better-auth", "my-app"] matches cookies starting with any of the prefixes
67
+ *
68
+ * @param setCookieHeader - The Set-Cookie header value
69
+ * @param cookiePrefix - The cookie prefix(es) to check for. Can be a string, array of strings, or empty string.
70
+ * @returns true if the header contains better-auth cookies, false otherwise
71
+ */
72
+ declare function hasBetterAuthCookies(setCookieHeader: string, cookiePrefix: string | string[]): boolean;
73
+ /**
74
+ * Expo secure store does not support colons in the keys.
75
+ * This function replaces colons with underscores.
76
+ *
77
+ * @see https://github.com/better-auth/better-auth/issues/5426
78
+ *
79
+ * @param name cookie name to be saved in the storage
80
+ * @returns normalized cookie name
81
+ */
82
+ declare function normalizeCookieName(name: string): string;
83
+ declare function storageAdapter(storage: {
84
+ getItem: (name: string) => string | null;
85
+ setItem: (name: string, value: string) => void;
86
+ }): {
87
+ getItem: (name: string) => string | null;
88
+ setItem: (name: string, value: string) => void;
89
+ };
90
+ declare const expoClient: (opts: ExpoClientOptions) => BetterAuthClientPlugin;
91
+ //#endregion
92
+ export { expoClient, getCookie, getSetCookie, hasBetterAuthCookies, normalizeCookieName, parseSetCookieHeader, setupExpoFocusManager, setupExpoOnlineManager, storageAdapter };
93
+ //# sourceMappingURL=client.d.mts.map
@@ -0,0 +1,319 @@
1
+ import { safeJSONParse } from "@better-auth/core/utils/json";
2
+ import { SECURE_COOKIE_PREFIX, parseSetCookieHeader, parseSetCookieHeader as parseSetCookieHeader$1, stripSecureCookiePrefix } from "better-auth/cookies";
3
+ import Constants from "expo-constants";
4
+ import * as Linking from "expo-linking";
5
+ import { AppState, Platform } from "react-native";
6
+ import { kFocusManager, kOnlineManager } from "better-auth/client";
7
+
8
+ //#region src/focus-manager.ts
9
+ var ExpoFocusManager = class {
10
+ listeners = /* @__PURE__ */ new Set();
11
+ subscription;
12
+ isFocused;
13
+ subscribe(listener) {
14
+ this.listeners.add(listener);
15
+ return () => {
16
+ this.listeners.delete(listener);
17
+ };
18
+ }
19
+ setFocused(focused) {
20
+ if (this.isFocused === focused) return;
21
+ this.isFocused = focused;
22
+ this.listeners.forEach((listener) => listener(focused));
23
+ }
24
+ setup() {
25
+ this.subscription = AppState.addEventListener("change", (state) => {
26
+ this.setFocused(state === "active");
27
+ });
28
+ return () => {
29
+ this.subscription?.remove();
30
+ };
31
+ }
32
+ };
33
+ function setupExpoFocusManager() {
34
+ if (!globalThis[kFocusManager]) globalThis[kFocusManager] = new ExpoFocusManager();
35
+ return globalThis[kFocusManager];
36
+ }
37
+
38
+ //#endregion
39
+ //#region src/online-manager.ts
40
+ var ExpoOnlineManager = class {
41
+ listeners = /* @__PURE__ */ new Set();
42
+ isOnline = true;
43
+ unsubscribe;
44
+ subscribe(listener) {
45
+ this.listeners.add(listener);
46
+ return () => {
47
+ this.listeners.delete(listener);
48
+ };
49
+ }
50
+ setOnline(online) {
51
+ if (this.isOnline === online) return;
52
+ this.isOnline = online;
53
+ this.listeners.forEach((listener) => listener(online));
54
+ }
55
+ setup() {
56
+ import("expo-network").then(({ addNetworkStateListener }) => {
57
+ const subscription = addNetworkStateListener((state) => {
58
+ this.setOnline(!!state.isInternetReachable);
59
+ });
60
+ this.unsubscribe = () => subscription.remove();
61
+ }).catch(() => {
62
+ this.setOnline(true);
63
+ });
64
+ return () => {
65
+ this.unsubscribe?.();
66
+ };
67
+ }
68
+ };
69
+ function setupExpoOnlineManager() {
70
+ if (!globalThis[kOnlineManager]) globalThis[kOnlineManager] = new ExpoOnlineManager();
71
+ return globalThis[kOnlineManager];
72
+ }
73
+
74
+ //#endregion
75
+ //#region src/client.ts
76
+ if (Platform.OS !== "web") {
77
+ setupExpoFocusManager();
78
+ setupExpoOnlineManager();
79
+ }
80
+ function getSetCookie(header, prevCookie) {
81
+ const parsed = parseSetCookieHeader$1(header);
82
+ let toSetCookie = {};
83
+ parsed.forEach((cookie, key) => {
84
+ const expiresAt = cookie["expires"];
85
+ const maxAge = cookie["max-age"];
86
+ const expires = maxAge ? new Date(Date.now() + Number(maxAge) * 1e3) : expiresAt ? new Date(String(expiresAt)) : null;
87
+ toSetCookie[key] = {
88
+ value: cookie["value"],
89
+ expires: expires ? expires.toISOString() : null
90
+ };
91
+ });
92
+ if (prevCookie) try {
93
+ toSetCookie = {
94
+ ...JSON.parse(prevCookie),
95
+ ...toSetCookie
96
+ };
97
+ } catch {}
98
+ return JSON.stringify(toSetCookie);
99
+ }
100
+ function getCookie(cookie) {
101
+ let parsed = {};
102
+ try {
103
+ parsed = JSON.parse(cookie);
104
+ } catch {}
105
+ return Object.entries(parsed).reduce((acc, [key, value]) => {
106
+ if (value.expires && new Date(value.expires) < /* @__PURE__ */ new Date()) return acc;
107
+ return `${acc}; ${key}=${value.value}`;
108
+ }, "");
109
+ }
110
+ function getOAuthStateValue(cookieJson, cookiePrefix) {
111
+ if (!cookieJson) return null;
112
+ const parsed = safeJSONParse(cookieJson);
113
+ if (!parsed) return null;
114
+ const prefixes = Array.isArray(cookiePrefix) ? cookiePrefix : [cookiePrefix];
115
+ for (const prefix of prefixes) {
116
+ const candidates = [`${SECURE_COOKIE_PREFIX}${prefix}.oauth_state`, `${prefix}.oauth_state`];
117
+ for (const name of candidates) {
118
+ const value = parsed?.[name]?.value;
119
+ if (value) return value;
120
+ }
121
+ }
122
+ return null;
123
+ }
124
+ function getOrigin(scheme) {
125
+ return Linking.createURL("", { scheme });
126
+ }
127
+ /**
128
+ * Compare if session cookies have actually changed by comparing their values.
129
+ * Ignores expiry timestamps that naturally change on each request.
130
+ *
131
+ * @param prevCookie - Previous cookie JSON string
132
+ * @param newCookie - New cookie JSON string
133
+ * @returns true if session cookies have changed, false otherwise
134
+ */
135
+ function hasSessionCookieChanged(prevCookie, newCookie) {
136
+ if (!prevCookie) return true;
137
+ try {
138
+ const prev = JSON.parse(prevCookie);
139
+ const next = JSON.parse(newCookie);
140
+ const sessionKeys = /* @__PURE__ */ new Set();
141
+ Object.keys(prev).forEach((key) => {
142
+ if (key.includes("session_token") || key.includes("session_data")) sessionKeys.add(key);
143
+ });
144
+ Object.keys(next).forEach((key) => {
145
+ if (key.includes("session_token") || key.includes("session_data")) sessionKeys.add(key);
146
+ });
147
+ for (const key of sessionKeys) if (prev[key]?.value !== next[key]?.value) return true;
148
+ return false;
149
+ } catch {
150
+ return true;
151
+ }
152
+ }
153
+ /**
154
+ * Check if the Set-Cookie header contains better-auth cookies.
155
+ * This prevents infinite refetching when non-better-auth cookies (like third-party cookies) change.
156
+ *
157
+ * Supports multiple cookie naming patterns:
158
+ * - Default: "better-auth.session_token", "better-auth-passkey", "__Secure-better-auth.session_token"
159
+ * - Custom prefix: "myapp.session_token", "myapp-passkey", "__Secure-myapp.session_token"
160
+ * - Custom full names: "my_custom_session_token", "custom_session_data"
161
+ * - No prefix (cookiePrefix=""): matches any cookie with known suffixes
162
+ * - Multiple prefixes: ["better-auth", "my-app"] matches cookies starting with any of the prefixes
163
+ *
164
+ * @param setCookieHeader - The Set-Cookie header value
165
+ * @param cookiePrefix - The cookie prefix(es) to check for. Can be a string, array of strings, or empty string.
166
+ * @returns true if the header contains better-auth cookies, false otherwise
167
+ */
168
+ function hasBetterAuthCookies(setCookieHeader, cookiePrefix) {
169
+ const cookies = parseSetCookieHeader$1(setCookieHeader);
170
+ const cookieSuffixes = ["session_token", "session_data"];
171
+ const prefixes = Array.isArray(cookiePrefix) ? cookiePrefix : [cookiePrefix];
172
+ for (const name of cookies.keys()) {
173
+ const nameWithoutSecure = stripSecureCookiePrefix(name);
174
+ for (const prefix of prefixes) if (prefix) {
175
+ if (nameWithoutSecure.startsWith(prefix)) return true;
176
+ } else for (const suffix of cookieSuffixes) if (nameWithoutSecure.endsWith(suffix)) return true;
177
+ }
178
+ return false;
179
+ }
180
+ /**
181
+ * Expo secure store does not support colons in the keys.
182
+ * This function replaces colons with underscores.
183
+ *
184
+ * @see https://github.com/better-auth/better-auth/issues/5426
185
+ *
186
+ * @param name cookie name to be saved in the storage
187
+ * @returns normalized cookie name
188
+ */
189
+ function normalizeCookieName(name) {
190
+ return name.replace(/:/g, "_");
191
+ }
192
+ function storageAdapter(storage) {
193
+ return {
194
+ getItem: (name) => {
195
+ return storage.getItem(normalizeCookieName(name));
196
+ },
197
+ setItem: (name, value) => {
198
+ return storage.setItem(normalizeCookieName(name), value);
199
+ }
200
+ };
201
+ }
202
+ const expoClient = (opts) => {
203
+ let store = null;
204
+ const storagePrefix = opts?.storagePrefix || "better-auth";
205
+ const cookieName = `${storagePrefix}_cookie`;
206
+ const localCacheName = `${storagePrefix}_session_data`;
207
+ const storage = storageAdapter(opts?.storage);
208
+ const isWeb = Platform.OS === "web";
209
+ const cookiePrefix = opts?.cookiePrefix || "better-auth";
210
+ const rawScheme = opts?.scheme || Constants.expoConfig?.scheme || Constants.platform?.scheme;
211
+ const scheme = Array.isArray(rawScheme) ? rawScheme[0] : rawScheme;
212
+ if (!scheme && !isWeb) throw new Error("Scheme not found in app.json. Please provide a scheme in the options.");
213
+ return {
214
+ id: "expo",
215
+ getActions(_, $store) {
216
+ store = $store;
217
+ return { getCookie: () => {
218
+ return getCookie(storage.getItem(cookieName) || "{}");
219
+ } };
220
+ },
221
+ fetchPlugins: [{
222
+ id: "expo",
223
+ name: "Expo",
224
+ hooks: { async onSuccess(context) {
225
+ if (isWeb) return;
226
+ const setCookie = context.response.headers.get("set-cookie");
227
+ if (setCookie) {
228
+ if (hasBetterAuthCookies(setCookie, cookiePrefix)) {
229
+ const prevCookie = storage.getItem(cookieName);
230
+ const toSetCookie = getSetCookie(setCookie || "", prevCookie ?? void 0);
231
+ if (hasSessionCookieChanged(prevCookie, toSetCookie)) {
232
+ storage.setItem(cookieName, toSetCookie);
233
+ store?.notify("$sessionSignal");
234
+ } else storage.setItem(cookieName, toSetCookie);
235
+ }
236
+ }
237
+ if (context.request.url.toString().includes("/get-session") && !opts?.disableCache) {
238
+ const data = context.data;
239
+ storage.setItem(localCacheName, JSON.stringify(data));
240
+ }
241
+ if (context.data?.redirect && (context.request.url.toString().includes("/sign-in") || context.request.url.toString().includes("/link-social")) && !context.request?.body.includes("idToken")) {
242
+ const to = JSON.parse(context.request.body)?.callbackURL;
243
+ const signInURL = context.data?.url;
244
+ let Browser = void 0;
245
+ try {
246
+ Browser = await import("expo-web-browser");
247
+ } catch (error) {
248
+ throw new Error("\"expo-web-browser\" is not installed as a dependency!", { cause: error });
249
+ }
250
+ if (Platform.OS === "android") try {
251
+ Browser.dismissAuthSession();
252
+ } catch {}
253
+ const oauthStateValue = getOAuthStateValue(storage.getItem(cookieName), cookiePrefix);
254
+ const params = new URLSearchParams({ authorizationURL: signInURL });
255
+ if (oauthStateValue) params.append("oauthState", oauthStateValue);
256
+ const proxyURL = `${context.request.baseURL}/expo-authorization-proxy?${params.toString()}`;
257
+ const result = await Browser.openAuthSessionAsync(proxyURL, to, opts?.webBrowserOptions);
258
+ if (result.type !== "success") return;
259
+ const cookie = new URL(result.url).searchParams.get("cookie");
260
+ if (!cookie) return;
261
+ const toSetCookie = getSetCookie(cookie, storage.getItem(cookieName) ?? void 0);
262
+ storage.setItem(cookieName, toSetCookie);
263
+ store?.notify("$sessionSignal");
264
+ }
265
+ } },
266
+ async init(url, options) {
267
+ if (isWeb) return {
268
+ url,
269
+ options
270
+ };
271
+ options = options || {};
272
+ const cookie = getCookie(storage.getItem(cookieName) || "{}");
273
+ options.credentials = "omit";
274
+ options.headers = {
275
+ ...options.headers,
276
+ cookie,
277
+ "expo-origin": getOrigin(scheme),
278
+ "x-skip-oauth-proxy": "true"
279
+ };
280
+ if (options.body?.callbackURL) {
281
+ if (options.body.callbackURL.startsWith("/")) {
282
+ const url = Linking.createURL(options.body.callbackURL, { scheme });
283
+ options.body.callbackURL = url;
284
+ }
285
+ }
286
+ if (options.body?.newUserCallbackURL) {
287
+ if (options.body.newUserCallbackURL.startsWith("/")) {
288
+ const url = Linking.createURL(options.body.newUserCallbackURL, { scheme });
289
+ options.body.newUserCallbackURL = url;
290
+ }
291
+ }
292
+ if (options.body?.errorCallbackURL) {
293
+ if (options.body.errorCallbackURL.startsWith("/")) {
294
+ const url = Linking.createURL(options.body.errorCallbackURL, { scheme });
295
+ options.body.errorCallbackURL = url;
296
+ }
297
+ }
298
+ if (url.includes("/sign-out")) {
299
+ storage.setItem(cookieName, "{}");
300
+ store?.atoms.session?.set({
301
+ ...store.atoms.session.get(),
302
+ data: null,
303
+ error: null,
304
+ isPending: false
305
+ });
306
+ storage.setItem(localCacheName, "{}");
307
+ }
308
+ return {
309
+ url,
310
+ options
311
+ };
312
+ }
313
+ }]
314
+ };
315
+ };
316
+
317
+ //#endregion
318
+ export { expoClient, getCookie, getSetCookie, hasBetterAuthCookies, normalizeCookieName, parseSetCookieHeader, setupExpoFocusManager, setupExpoOnlineManager, storageAdapter };
319
+ //# sourceMappingURL=client.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"client.mjs","names":["parseSetCookieHeader"],"sources":["../src/focus-manager.ts","../src/online-manager.ts","../src/client.ts"],"sourcesContent":["import type { FocusListener, FocusManager } from \"better-auth/client\";\nimport { kFocusManager } from \"better-auth/client\";\nimport type { AppStateStatus } from \"react-native\";\nimport { AppState } from \"react-native\";\n\nclass ExpoFocusManager implements FocusManager {\n\tlisteners = new Set<FocusListener>();\n\tsubscription?: ReturnType<typeof AppState.addEventListener>;\n\tisFocused: boolean | undefined;\n\n\tsubscribe(listener: FocusListener) {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tsetFocused(focused: boolean) {\n\t\tif (this.isFocused === focused) return;\n\t\tthis.isFocused = focused;\n\t\tthis.listeners.forEach((listener) => listener(focused));\n\t}\n\n\tsetup() {\n\t\tthis.subscription = AppState.addEventListener(\n\t\t\t\"change\",\n\t\t\t(state: AppStateStatus) => {\n\t\t\t\tthis.setFocused(state === \"active\");\n\t\t\t},\n\t\t);\n\n\t\treturn () => {\n\t\t\tthis.subscription?.remove();\n\t\t};\n\t}\n}\n\nexport function setupExpoFocusManager() {\n\tif (!(globalThis as any)[kFocusManager]) {\n\t\t(globalThis as any)[kFocusManager] = new ExpoFocusManager();\n\t}\n\treturn (globalThis as any)[kFocusManager] as FocusManager;\n}\n","import type { OnlineListener, OnlineManager } from \"better-auth/client\";\nimport { kOnlineManager } from \"better-auth/client\";\n\nclass ExpoOnlineManager implements OnlineManager {\n\tlisteners = new Set<OnlineListener>();\n\tisOnline = true;\n\tunsubscribe?: () => void;\n\n\tsubscribe(listener: OnlineListener) {\n\t\tthis.listeners.add(listener);\n\t\treturn () => {\n\t\t\tthis.listeners.delete(listener);\n\t\t};\n\t}\n\n\tsetOnline(online: boolean) {\n\t\tif (this.isOnline === online) return;\n\t\tthis.isOnline = online;\n\t\tthis.listeners.forEach((listener) => listener(online));\n\t}\n\n\tsetup() {\n\t\timport(\"expo-network\")\n\t\t\t.then(({ addNetworkStateListener }) => {\n\t\t\t\tconst subscription = addNetworkStateListener((state) => {\n\t\t\t\t\tthis.setOnline(!!state.isInternetReachable);\n\t\t\t\t});\n\t\t\t\tthis.unsubscribe = () => subscription.remove();\n\t\t\t})\n\t\t\t.catch(() => {\n\t\t\t\t// fallback to always online\n\t\t\t\tthis.setOnline(true);\n\t\t\t});\n\n\t\treturn () => {\n\t\t\tthis.unsubscribe?.();\n\t\t};\n\t}\n}\n\nexport function setupExpoOnlineManager() {\n\tif (!(globalThis as any)[kOnlineManager]) {\n\t\t(globalThis as any)[kOnlineManager] = new ExpoOnlineManager();\n\t}\n\treturn (globalThis as any)[kOnlineManager] as OnlineManager;\n}\n","import type {\n\tBetterAuthClientPlugin,\n\tClientFetchOption,\n\tClientStore,\n} from \"@better-auth/core\";\nimport { safeJSONParse } from \"@better-auth/core/utils/json\";\nimport {\n\tparseSetCookieHeader,\n\tSECURE_COOKIE_PREFIX,\n\tstripSecureCookiePrefix,\n} from \"better-auth/cookies\";\nimport Constants from \"expo-constants\";\nimport * as Linking from \"expo-linking\";\nimport { Platform } from \"react-native\";\nimport { setupExpoFocusManager } from \"./focus-manager\";\nimport { setupExpoOnlineManager } from \"./online-manager\";\n\nif (Platform.OS !== \"web\") {\n\tsetupExpoFocusManager();\n\tsetupExpoOnlineManager();\n}\n\ninterface ExpoClientOptions {\n\tscheme?: string | undefined;\n\tstorage: {\n\t\tsetItem: (key: string, value: string) => any;\n\t\tgetItem: (key: string) => string | null;\n\t};\n\t/**\n\t * Prefix for local storage keys (e.g., \"my-app_cookie\", \"my-app_session_data\")\n\t * @default \"better-auth\"\n\t */\n\tstoragePrefix?: string | undefined;\n\t/**\n\t * Prefix(es) for server cookie names to filter (e.g., \"better-auth.session_token\")\n\t * This is used to identify which cookies belong to better-auth to prevent\n\t * infinite refetching when third-party cookies are set.\n\t * Can be a single string or an array of strings to match multiple prefixes.\n\t * @default \"better-auth\"\n\t * @example \"better-auth\"\n\t * @example [\"better-auth\", \"my-app\"]\n\t */\n\tcookiePrefix?: string | string[] | undefined;\n\tdisableCache?: boolean | undefined;\n\t/**\n\t * Options to customize the Expo web browser behavior when opening authentication\n\t * sessions. These are passed directly to `expo-web-browser`'s\n\t * `Browser.openBrowserAsync`.\n\t *\n\t * For example, on iOS you can use `{ preferEphemeralSession: true }` to prevent\n\t * the authentication session from sharing cookies with the user's default\n\t * browser session:\n\t *\n\t * ```ts\n\t * const client = createClient({\n\t * expo: {\n\t * webBrowserOptions: {\n\t * preferEphemeralSession: true,\n\t * },\n\t * },\n\t * });\n\t * ```\n\t */\n\twebBrowserOptions?: import(\"expo-web-browser\").AuthSessionOpenOptions;\n}\n\ninterface StoredCookie {\n\tvalue: string;\n\texpires: string | null;\n}\n\nexport function getSetCookie(header: string, prevCookie?: string | undefined) {\n\tconst parsed = parseSetCookieHeader(header);\n\tlet toSetCookie: Record<string, StoredCookie> = {};\n\tparsed.forEach((cookie, key) => {\n\t\tconst expiresAt = cookie[\"expires\"];\n\t\tconst maxAge = cookie[\"max-age\"];\n\t\tconst expires = maxAge\n\t\t\t? new Date(Date.now() + Number(maxAge) * 1000)\n\t\t\t: expiresAt\n\t\t\t\t? new Date(String(expiresAt))\n\t\t\t\t: null;\n\t\ttoSetCookie[key] = {\n\t\t\tvalue: cookie[\"value\"],\n\t\t\texpires: expires ? expires.toISOString() : null,\n\t\t};\n\t});\n\tif (prevCookie) {\n\t\ttry {\n\t\t\tconst prevCookieParsed = JSON.parse(prevCookie);\n\t\t\ttoSetCookie = {\n\t\t\t\t...prevCookieParsed,\n\t\t\t\t...toSetCookie,\n\t\t\t};\n\t\t} catch {\n\t\t\t//\n\t\t}\n\t}\n\treturn JSON.stringify(toSetCookie);\n}\n\nexport function getCookie(cookie: string) {\n\tlet parsed = {} as Record<string, StoredCookie>;\n\ttry {\n\t\tparsed = JSON.parse(cookie) as Record<string, StoredCookie>;\n\t} catch {}\n\tconst toSend = Object.entries(parsed).reduce((acc, [key, value]) => {\n\t\tif (value.expires && new Date(value.expires) < new Date()) {\n\t\t\treturn acc;\n\t\t}\n\t\treturn `${acc}; ${key}=${value.value}`;\n\t}, \"\");\n\treturn toSend;\n}\n\nfunction getOAuthStateValue(\n\tcookieJson: string | null,\n\tcookiePrefix: string | string[],\n): string | null {\n\tif (!cookieJson) return null;\n\n\tconst parsed = safeJSONParse<Record<string, StoredCookie>>(cookieJson);\n\tif (!parsed) return null;\n\n\tconst prefixes = Array.isArray(cookiePrefix) ? cookiePrefix : [cookiePrefix];\n\n\tfor (const prefix of prefixes) {\n\t\t// cookie strategy uses: <prefix>.oauth_state\n\t\tconst candidates = [\n\t\t\t`${SECURE_COOKIE_PREFIX}${prefix}.oauth_state`,\n\t\t\t`${prefix}.oauth_state`,\n\t\t];\n\n\t\tfor (const name of candidates) {\n\t\t\tconst value = parsed?.[name]?.value;\n\t\t\tif (value) return value;\n\t\t}\n\t}\n\n\treturn null;\n}\n\nfunction getOrigin(scheme: string) {\n\tconst schemeURI = Linking.createURL(\"\", { scheme });\n\treturn schemeURI;\n}\n\n/**\n * Compare if session cookies have actually changed by comparing their values.\n * Ignores expiry timestamps that naturally change on each request.\n *\n * @param prevCookie - Previous cookie JSON string\n * @param newCookie - New cookie JSON string\n * @returns true if session cookies have changed, false otherwise\n */\nfunction hasSessionCookieChanged(\n\tprevCookie: string | null,\n\tnewCookie: string,\n): boolean {\n\tif (!prevCookie) return true;\n\n\ttry {\n\t\tconst prev = JSON.parse(prevCookie) as Record<string, StoredCookie>;\n\t\tconst next = JSON.parse(newCookie) as Record<string, StoredCookie>;\n\n\t\t// Get all session-related cookie keys (session_token, session_data)\n\t\tconst sessionKeys = new Set<string>();\n\t\tObject.keys(prev).forEach((key) => {\n\t\t\tif (key.includes(\"session_token\") || key.includes(\"session_data\")) {\n\t\t\t\tsessionKeys.add(key);\n\t\t\t}\n\t\t});\n\t\tObject.keys(next).forEach((key) => {\n\t\t\tif (key.includes(\"session_token\") || key.includes(\"session_data\")) {\n\t\t\t\tsessionKeys.add(key);\n\t\t\t}\n\t\t});\n\n\t\t// Compare the values of session cookies (ignore expires timestamps)\n\t\tfor (const key of sessionKeys) {\n\t\t\tconst prevValue = prev[key]?.value;\n\t\t\tconst nextValue = next[key]?.value;\n\t\t\tif (prevValue !== nextValue) {\n\t\t\t\treturn true;\n\t\t\t}\n\t\t}\n\n\t\treturn false;\n\t} catch {\n\t\t// If parsing fails, assume cookie changed\n\t\treturn true;\n\t}\n}\n\n/**\n * Check if the Set-Cookie header contains better-auth cookies.\n * This prevents infinite refetching when non-better-auth cookies (like third-party cookies) change.\n *\n * Supports multiple cookie naming patterns:\n * - Default: \"better-auth.session_token\", \"better-auth-passkey\", \"__Secure-better-auth.session_token\"\n * - Custom prefix: \"myapp.session_token\", \"myapp-passkey\", \"__Secure-myapp.session_token\"\n * - Custom full names: \"my_custom_session_token\", \"custom_session_data\"\n * - No prefix (cookiePrefix=\"\"): matches any cookie with known suffixes\n * - Multiple prefixes: [\"better-auth\", \"my-app\"] matches cookies starting with any of the prefixes\n *\n * @param setCookieHeader - The Set-Cookie header value\n * @param cookiePrefix - The cookie prefix(es) to check for. Can be a string, array of strings, or empty string.\n * @returns true if the header contains better-auth cookies, false otherwise\n */\nexport function hasBetterAuthCookies(\n\tsetCookieHeader: string,\n\tcookiePrefix: string | string[],\n): boolean {\n\tconst cookies = parseSetCookieHeader(setCookieHeader);\n\tconst cookieSuffixes = [\"session_token\", \"session_data\"];\n\tconst prefixes = Array.isArray(cookiePrefix) ? cookiePrefix : [cookiePrefix];\n\n\t// Check if any cookie is a better-auth cookie\n\tfor (const name of cookies.keys()) {\n\t\t// Remove __Secure- prefix if present for comparison\n\t\tconst nameWithoutSecure = stripSecureCookiePrefix(name);\n\n\t\t// Check against all provided prefixes\n\t\tfor (const prefix of prefixes) {\n\t\t\tif (prefix) {\n\t\t\t\t// When prefix is provided, check if cookie starts with the prefix\n\t\t\t\t// This matches all better-auth cookies including session cookies, passkey cookies, etc.\n\t\t\t\tif (nameWithoutSecure.startsWith(prefix)) {\n\t\t\t\t\treturn true;\n\t\t\t\t}\n\t\t\t} else {\n\t\t\t\t// When prefix is empty, check for common better-auth cookie patterns\n\t\t\t\tfor (const suffix of cookieSuffixes) {\n\t\t\t\t\tif (nameWithoutSecure.endsWith(suffix)) {\n\t\t\t\t\t\treturn true;\n\t\t\t\t\t}\n\t\t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn false;\n}\n\n/**\n * Expo secure store does not support colons in the keys.\n * This function replaces colons with underscores.\n *\n * @see https://github.com/better-auth/better-auth/issues/5426\n *\n * @param name cookie name to be saved in the storage\n * @returns normalized cookie name\n */\nexport function normalizeCookieName(name: string) {\n\treturn name.replace(/:/g, \"_\");\n}\n\nexport function storageAdapter(storage: {\n\tgetItem: (name: string) => string | null;\n\tsetItem: (name: string, value: string) => void;\n}) {\n\treturn {\n\t\tgetItem: (name: string) => {\n\t\t\treturn storage.getItem(normalizeCookieName(name));\n\t\t},\n\t\tsetItem: (name: string, value: string) => {\n\t\t\treturn storage.setItem(normalizeCookieName(name), value);\n\t\t},\n\t};\n}\n\nexport const expoClient = (opts: ExpoClientOptions) => {\n\tlet store: ClientStore | null = null;\n\tconst storagePrefix = opts?.storagePrefix || \"better-auth\";\n\tconst cookieName = `${storagePrefix}_cookie`;\n\tconst localCacheName = `${storagePrefix}_session_data`;\n\tconst storage = storageAdapter(opts?.storage);\n\tconst isWeb = Platform.OS === \"web\";\n\tconst cookiePrefix = opts?.cookiePrefix || \"better-auth\";\n\n\tconst rawScheme =\n\t\topts?.scheme || Constants.expoConfig?.scheme || Constants.platform?.scheme;\n\tconst scheme = Array.isArray(rawScheme) ? rawScheme[0] : rawScheme;\n\n\tif (!scheme && !isWeb) {\n\t\tthrow new Error(\n\t\t\t\"Scheme not found in app.json. Please provide a scheme in the options.\",\n\t\t);\n\t}\n\treturn {\n\t\tid: \"expo\",\n\t\tgetActions(_, $store) {\n\t\t\tstore = $store;\n\t\t\treturn {\n\t\t\t\t/**\n\t\t\t\t * Get the stored cookie.\n\t\t\t\t *\n\t\t\t\t * You can use this to get the cookie stored in the device and use it in your fetch\n\t\t\t\t * requests.\n\t\t\t\t *\n\t\t\t\t * @example\n\t\t\t\t * ```ts\n\t\t\t\t * const cookie = client.getCookie();\n\t\t\t\t * fetch(\"https://api.example.com\", {\n\t\t\t\t * \theaders: {\n\t\t\t\t * \t\tcookie,\n\t\t\t\t * \t},\n\t\t\t\t * });\n\t\t\t\t */\n\t\t\t\tgetCookie: () => {\n\t\t\t\t\tconst cookie = storage.getItem(cookieName);\n\t\t\t\t\treturn getCookie(cookie || \"{}\");\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t\tfetchPlugins: [\n\t\t\t{\n\t\t\t\tid: \"expo\",\n\t\t\t\tname: \"Expo\",\n\t\t\t\thooks: {\n\t\t\t\t\tasync onSuccess(context) {\n\t\t\t\t\t\tif (isWeb) return;\n\t\t\t\t\t\tconst setCookie = context.response.headers.get(\"set-cookie\");\n\t\t\t\t\t\tif (setCookie) {\n\t\t\t\t\t\t\t// Only process and notify if the Set-Cookie header contains better-auth cookies\n\t\t\t\t\t\t\t// This prevents infinite refetching when other cookies (like Cloudflare's __cf_bm) are present\n\t\t\t\t\t\t\tif (hasBetterAuthCookies(setCookie, cookiePrefix)) {\n\t\t\t\t\t\t\t\tconst prevCookie = storage.getItem(cookieName);\n\t\t\t\t\t\t\t\tconst toSetCookie = getSetCookie(\n\t\t\t\t\t\t\t\t\tsetCookie || \"\",\n\t\t\t\t\t\t\t\t\tprevCookie ?? undefined,\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t\t// Only notify $sessionSignal if the session cookie values actually changed\n\t\t\t\t\t\t\t\t// This prevents infinite refetching when the server sends the same cookie with updated expiry\n\t\t\t\t\t\t\t\tif (hasSessionCookieChanged(prevCookie, toSetCookie)) {\n\t\t\t\t\t\t\t\t\tstorage.setItem(cookieName, toSetCookie);\n\t\t\t\t\t\t\t\t\tstore?.notify(\"$sessionSignal\");\n\t\t\t\t\t\t\t\t} else {\n\t\t\t\t\t\t\t\t\t// Still update the storage to refresh expiry times, but don't trigger refetch\n\t\t\t\t\t\t\t\t\tstorage.setItem(cookieName, toSetCookie);\n\t\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tcontext.request.url.toString().includes(\"/get-session\") &&\n\t\t\t\t\t\t\t!opts?.disableCache\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tconst data = context.data;\n\t\t\t\t\t\t\tstorage.setItem(localCacheName, JSON.stringify(data));\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tif (\n\t\t\t\t\t\t\tcontext.data?.redirect &&\n\t\t\t\t\t\t\t(context.request.url.toString().includes(\"/sign-in\") ||\n\t\t\t\t\t\t\t\tcontext.request.url.toString().includes(\"/link-social\")) &&\n\t\t\t\t\t\t\t!context.request?.body.includes(\"idToken\") // id token is used for silent sign-in\n\t\t\t\t\t\t) {\n\t\t\t\t\t\t\tconst callbackURL = JSON.parse(context.request.body)?.callbackURL;\n\t\t\t\t\t\t\tconst to = callbackURL;\n\t\t\t\t\t\t\tconst signInURL = context.data?.url;\n\t\t\t\t\t\t\tlet Browser: typeof import(\"expo-web-browser\") | undefined =\n\t\t\t\t\t\t\t\tundefined;\n\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\tBrowser = await import(\"expo-web-browser\");\n\t\t\t\t\t\t\t} catch (error) {\n\t\t\t\t\t\t\t\tthrow new Error(\n\t\t\t\t\t\t\t\t\t'\"expo-web-browser\" is not installed as a dependency!',\n\t\t\t\t\t\t\t\t\t{\n\t\t\t\t\t\t\t\t\t\tcause: error,\n\t\t\t\t\t\t\t\t\t},\n\t\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tif (Platform.OS === \"android\") {\n\t\t\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\t\t\tBrowser.dismissAuthSession();\n\t\t\t\t\t\t\t\t} catch {}\n\t\t\t\t\t\t\t}\n\n\t\t\t\t\t\t\tconst storedCookieJson = storage.getItem(cookieName);\n\t\t\t\t\t\t\tconst oauthStateValue = getOAuthStateValue(\n\t\t\t\t\t\t\t\tstoredCookieJson,\n\t\t\t\t\t\t\t\tcookiePrefix,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tconst params = new URLSearchParams({\n\t\t\t\t\t\t\t\tauthorizationURL: signInURL,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\tif (oauthStateValue) {\n\t\t\t\t\t\t\t\tparams.append(\"oauthState\", oauthStateValue);\n\t\t\t\t\t\t\t}\n\t\t\t\t\t\t\tconst proxyURL = `${context.request.baseURL}/expo-authorization-proxy?${params.toString()}`;\n\t\t\t\t\t\t\tconst result = await Browser.openAuthSessionAsync(\n\t\t\t\t\t\t\t\tproxyURL,\n\t\t\t\t\t\t\t\tto,\n\t\t\t\t\t\t\t\topts?.webBrowserOptions,\n\t\t\t\t\t\t\t);\n\t\t\t\t\t\t\tif (result.type !== \"success\") return;\n\t\t\t\t\t\t\tconst url = new URL(result.url);\n\t\t\t\t\t\t\tconst cookie = url.searchParams.get(\"cookie\");\n\t\t\t\t\t\t\tif (!cookie) return;\n\t\t\t\t\t\t\tconst prevCookie = storage.getItem(cookieName);\n\t\t\t\t\t\t\tconst toSetCookie = getSetCookie(cookie, prevCookie ?? undefined);\n\t\t\t\t\t\t\tstorage.setItem(cookieName, toSetCookie);\n\t\t\t\t\t\t\tstore?.notify(\"$sessionSignal\");\n\t\t\t\t\t\t}\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t\tasync init(url, options) {\n\t\t\t\t\tif (isWeb) {\n\t\t\t\t\t\treturn {\n\t\t\t\t\t\t\turl,\n\t\t\t\t\t\t\toptions: options as ClientFetchOption,\n\t\t\t\t\t\t};\n\t\t\t\t\t}\n\t\t\t\t\toptions = options || {};\n\t\t\t\t\tconst storedCookie = storage.getItem(cookieName);\n\t\t\t\t\tconst cookie = getCookie(storedCookie || \"{}\");\n\t\t\t\t\toptions.credentials = \"omit\";\n\t\t\t\t\toptions.headers = {\n\t\t\t\t\t\t...options.headers,\n\t\t\t\t\t\tcookie,\n\t\t\t\t\t\t\"expo-origin\": getOrigin(scheme!),\n\t\t\t\t\t\t\"x-skip-oauth-proxy\": \"true\", // skip oauth proxy for expo\n\t\t\t\t\t};\n\t\t\t\t\tif (options.body?.callbackURL) {\n\t\t\t\t\t\tif (options.body.callbackURL.startsWith(\"/\")) {\n\t\t\t\t\t\t\tconst url = Linking.createURL(options.body.callbackURL, {\n\t\t\t\t\t\t\t\tscheme,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\toptions.body.callbackURL = url;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (options.body?.newUserCallbackURL) {\n\t\t\t\t\t\tif (options.body.newUserCallbackURL.startsWith(\"/\")) {\n\t\t\t\t\t\t\tconst url = Linking.createURL(options.body.newUserCallbackURL, {\n\t\t\t\t\t\t\t\tscheme,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\toptions.body.newUserCallbackURL = url;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (options.body?.errorCallbackURL) {\n\t\t\t\t\t\tif (options.body.errorCallbackURL.startsWith(\"/\")) {\n\t\t\t\t\t\t\tconst url = Linking.createURL(options.body.errorCallbackURL, {\n\t\t\t\t\t\t\t\tscheme,\n\t\t\t\t\t\t\t});\n\t\t\t\t\t\t\toptions.body.errorCallbackURL = url;\n\t\t\t\t\t\t}\n\t\t\t\t\t}\n\t\t\t\t\tif (url.includes(\"/sign-out\")) {\n\t\t\t\t\t\tstorage.setItem(cookieName, \"{}\");\n\t\t\t\t\t\tstore?.atoms.session?.set({\n\t\t\t\t\t\t\t...store.atoms.session.get(),\n\t\t\t\t\t\t\tdata: null,\n\t\t\t\t\t\t\terror: null,\n\t\t\t\t\t\t\tisPending: false,\n\t\t\t\t\t\t});\n\t\t\t\t\t\tstorage.setItem(localCacheName, \"{}\");\n\t\t\t\t\t}\n\t\t\t\t\treturn {\n\t\t\t\t\t\turl,\n\t\t\t\t\t\toptions: options as ClientFetchOption,\n\t\t\t\t\t};\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t} satisfies BetterAuthClientPlugin;\n};\n\nexport { parseSetCookieHeader } from \"better-auth/cookies\";\nexport * from \"./focus-manager\";\nexport * from \"./online-manager\";\n"],"mappings":"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"}
@@ -0,0 +1,19 @@
1
+ //#region src/index.d.ts
2
+ interface ExpoOptions {
3
+ /**
4
+ * Disable origin override for expo API routes
5
+ * When set to true, the origin header will not be overridden for expo API routes
6
+ */
7
+ disableOriginOverride?: boolean | undefined;
8
+ }
9
+ declare module "@better-auth/core" {
10
+ interface BetterAuthPluginRegistry<AuthOptions, Options> {
11
+ expo: {
12
+ creator: typeof expo;
13
+ };
14
+ }
15
+ }
16
+ declare const expo: (options?: ExpoOptions | undefined) => BetterAuthPlugin;
17
+ //#endregion
18
+ export { ExpoOptions, expo };
19
+ //# sourceMappingURL=index.d.mts.map
package/dist/index.mjs ADDED
@@ -0,0 +1,73 @@
1
+ import { createAuthMiddleware } from "@better-auth/core/api";
2
+ import { HIDE_METADATA } from "better-auth";
3
+ import { APIError, createAuthEndpoint } from "better-auth/api";
4
+ import * as z from "zod";
5
+
6
+ //#region src/routes.ts
7
+ const expoAuthorizationProxy = createAuthEndpoint("/expo-authorization-proxy", {
8
+ method: "GET",
9
+ query: z.object({
10
+ authorizationURL: z.string(),
11
+ oauthState: z.string().optional()
12
+ }),
13
+ metadata: HIDE_METADATA
14
+ }, async (ctx) => {
15
+ const { oauthState } = ctx.query;
16
+ if (oauthState) {
17
+ const oauthStateCookie = ctx.context.createAuthCookie("oauth_state", { maxAge: 600 * 1e3 });
18
+ ctx.setCookie(oauthStateCookie.name, oauthState, oauthStateCookie.attributes);
19
+ return ctx.redirect(ctx.query.authorizationURL);
20
+ }
21
+ const { authorizationURL } = ctx.query;
22
+ const state = new URL(authorizationURL).searchParams.get("state");
23
+ if (!state) throw new APIError("BAD_REQUEST", { message: "Unexpected error" });
24
+ const stateCookie = ctx.context.createAuthCookie("state", { maxAge: 300 * 1e3 });
25
+ await ctx.setSignedCookie(stateCookie.name, state, ctx.context.secret, stateCookie.attributes);
26
+ return ctx.redirect(ctx.query.authorizationURL);
27
+ });
28
+
29
+ //#endregion
30
+ //#region src/index.ts
31
+ const expo = (options) => {
32
+ return {
33
+ id: "expo",
34
+ init: (ctx) => {
35
+ return { options: { trustedOrigins: process.env.NODE_ENV === "development" ? ["exp://"] : [] } };
36
+ },
37
+ async onRequest(request, ctx) {
38
+ if (options?.disableOriginOverride || request.headers.get("origin")) return;
39
+ /**
40
+ * To bypass origin check from expo, we need to set the origin
41
+ * header to the expo-origin header
42
+ */
43
+ const expoOrigin = request.headers.get("expo-origin");
44
+ if (!expoOrigin) return;
45
+ const req = request.clone();
46
+ req.headers.set("origin", expoOrigin);
47
+ return { request: req };
48
+ },
49
+ hooks: { after: [{
50
+ matcher(context) {
51
+ return !!(context.path?.startsWith("/callback") || context.path?.startsWith("/oauth2/callback") || context.path?.startsWith("/magic-link/verify") || context.path?.startsWith("/verify-email"));
52
+ },
53
+ handler: createAuthMiddleware(async (ctx) => {
54
+ const headers = ctx.context.responseHeaders;
55
+ const location = headers?.get("location");
56
+ if (!location) return;
57
+ if (location.includes("/oauth-proxy-callback")) return;
58
+ if (!ctx.context.trustedOrigins.filter((origin) => !origin.startsWith("http")).some((origin) => location?.startsWith(origin))) return;
59
+ const cookie = headers?.get("set-cookie");
60
+ if (!cookie) return;
61
+ const url = new URL(location);
62
+ url.searchParams.set("cookie", cookie);
63
+ ctx.setHeader("location", url.toString());
64
+ })
65
+ }] },
66
+ endpoints: { expoAuthorizationProxy },
67
+ options
68
+ };
69
+ };
70
+
71
+ //#endregion
72
+ export { expo };
73
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../src/routes.ts","../src/index.ts"],"sourcesContent":["import { HIDE_METADATA } from \"better-auth\";\nimport { APIError, createAuthEndpoint } from \"better-auth/api\";\nimport * as z from \"zod\";\n\nexport const expoAuthorizationProxy = createAuthEndpoint(\n\t\"/expo-authorization-proxy\",\n\t{\n\t\tmethod: \"GET\",\n\t\tquery: z.object({\n\t\t\tauthorizationURL: z.string(),\n\t\t\toauthState: z.string().optional(),\n\t\t}),\n\t\tmetadata: HIDE_METADATA,\n\t},\n\tasync (ctx) => {\n\t\tconst { oauthState } = ctx.query;\n\t\tif (oauthState) {\n\t\t\tconst oauthStateCookie = ctx.context.createAuthCookie(\"oauth_state\", {\n\t\t\t\tmaxAge: 10 * 60 * 1000, // 10 minutes\n\t\t\t});\n\t\t\tctx.setCookie(\n\t\t\t\toauthStateCookie.name,\n\t\t\t\toauthState,\n\t\t\t\toauthStateCookie.attributes,\n\t\t\t);\n\t\t\treturn ctx.redirect(ctx.query.authorizationURL);\n\t\t}\n\n\t\tconst { authorizationURL } = ctx.query;\n\t\tconst url = new URL(authorizationURL);\n\t\tconst state = url.searchParams.get(\"state\");\n\t\tif (!state) {\n\t\t\tthrow new APIError(\"BAD_REQUEST\", {\n\t\t\t\tmessage: \"Unexpected error\",\n\t\t\t});\n\t\t}\n\t\tconst stateCookie = ctx.context.createAuthCookie(\"state\", {\n\t\t\tmaxAge: 5 * 60 * 1000, // 5 minutes\n\t\t});\n\t\tawait ctx.setSignedCookie(\n\t\t\tstateCookie.name,\n\t\t\tstate,\n\t\t\tctx.context.secret,\n\t\t\tstateCookie.attributes,\n\t\t);\n\t\treturn ctx.redirect(ctx.query.authorizationURL);\n\t},\n);\n","import type { BetterAuthPlugin } from \"@better-auth/core\";\nimport { createAuthMiddleware } from \"@better-auth/core/api\";\nimport { expoAuthorizationProxy } from \"./routes\";\n\nexport interface ExpoOptions {\n\t/**\n\t * Disable origin override for expo API routes\n\t * When set to true, the origin header will not be overridden for expo API routes\n\t */\n\tdisableOriginOverride?: boolean | undefined;\n}\n\ndeclare module \"@better-auth/core\" {\n\tinterface BetterAuthPluginRegistry<AuthOptions, Options> {\n\t\texpo: {\n\t\t\tcreator: typeof expo;\n\t\t};\n\t}\n}\n\nexport const expo = (options?: ExpoOptions | undefined) => {\n\treturn {\n\t\tid: \"expo\",\n\t\tinit: (ctx) => {\n\t\t\tconst trustedOrigins =\n\t\t\t\tprocess.env.NODE_ENV === \"development\" ? [\"exp://\"] : [];\n\n\t\t\treturn {\n\t\t\t\toptions: {\n\t\t\t\t\ttrustedOrigins,\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t\tasync onRequest(request, ctx) {\n\t\t\tif (options?.disableOriginOverride || request.headers.get(\"origin\")) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\t/**\n\t\t\t * To bypass origin check from expo, we need to set the origin\n\t\t\t * header to the expo-origin header\n\t\t\t */\n\t\t\tconst expoOrigin = request.headers.get(\"expo-origin\");\n\t\t\tif (!expoOrigin) {\n\t\t\t\treturn;\n\t\t\t}\n\t\t\tconst req = request.clone();\n\t\t\treq.headers.set(\"origin\", expoOrigin);\n\t\t\treturn {\n\t\t\t\trequest: req,\n\t\t\t};\n\t\t},\n\t\thooks: {\n\t\t\tafter: [\n\t\t\t\t{\n\t\t\t\t\tmatcher(context) {\n\t\t\t\t\t\treturn !!(\n\t\t\t\t\t\t\tcontext.path?.startsWith(\"/callback\") ||\n\t\t\t\t\t\t\tcontext.path?.startsWith(\"/oauth2/callback\") ||\n\t\t\t\t\t\t\tcontext.path?.startsWith(\"/magic-link/verify\") ||\n\t\t\t\t\t\t\tcontext.path?.startsWith(\"/verify-email\")\n\t\t\t\t\t\t);\n\t\t\t\t\t},\n\t\t\t\t\thandler: createAuthMiddleware(async (ctx) => {\n\t\t\t\t\t\tconst headers = ctx.context.responseHeaders;\n\t\t\t\t\t\tconst location = headers?.get(\"location\");\n\t\t\t\t\t\tif (!location) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst isProxyURL = location.includes(\"/oauth-proxy-callback\");\n\t\t\t\t\t\tif (isProxyURL) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst trustedOrigins = ctx.context.trustedOrigins.filter(\n\t\t\t\t\t\t\t(origin: string) => !origin.startsWith(\"http\"),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tconst isTrustedOrigin = trustedOrigins.some((origin: string) =>\n\t\t\t\t\t\t\tlocation?.startsWith(origin),\n\t\t\t\t\t\t);\n\t\t\t\t\t\tif (!isTrustedOrigin) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst cookie = headers?.get(\"set-cookie\");\n\t\t\t\t\t\tif (!cookie) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\t\t\t\t\t\tconst url = new URL(location);\n\t\t\t\t\t\turl.searchParams.set(\"cookie\", cookie);\n\t\t\t\t\t\tctx.setHeader(\"location\", url.toString());\n\t\t\t\t\t}),\n\t\t\t\t},\n\t\t\t],\n\t\t},\n\t\tendpoints: {\n\t\t\texpoAuthorizationProxy,\n\t\t},\n\t\toptions,\n\t} satisfies BetterAuthPlugin;\n};\n"],"mappings":";;;;;;AAIA,MAAa,yBAAyB,mBACrC,6BACA;CACC,QAAQ;CACR,OAAO,EAAE,OAAO;EACf,kBAAkB,EAAE,QAAQ;EAC5B,YAAY,EAAE,QAAQ,CAAC,UAAU;EACjC,CAAC;CACF,UAAU;CACV,EACD,OAAO,QAAQ;CACd,MAAM,EAAE,eAAe,IAAI;AAC3B,KAAI,YAAY;EACf,MAAM,mBAAmB,IAAI,QAAQ,iBAAiB,eAAe,EACpE,QAAQ,MAAU,KAClB,CAAC;AACF,MAAI,UACH,iBAAiB,MACjB,YACA,iBAAiB,WACjB;AACD,SAAO,IAAI,SAAS,IAAI,MAAM,iBAAiB;;CAGhD,MAAM,EAAE,qBAAqB,IAAI;CAEjC,MAAM,QADM,IAAI,IAAI,iBAAiB,CACnB,aAAa,IAAI,QAAQ;AAC3C,KAAI,CAAC,MACJ,OAAM,IAAI,SAAS,eAAe,EACjC,SAAS,oBACT,CAAC;CAEH,MAAM,cAAc,IAAI,QAAQ,iBAAiB,SAAS,EACzD,QAAQ,MAAS,KACjB,CAAC;AACF,OAAM,IAAI,gBACT,YAAY,MACZ,OACA,IAAI,QAAQ,QACZ,YAAY,WACZ;AACD,QAAO,IAAI,SAAS,IAAI,MAAM,iBAAiB;EAEhD;;;;AC3BD,MAAa,QAAQ,YAAsC;AAC1D,QAAO;EACN,IAAI;EACJ,OAAO,QAAQ;AAId,UAAO,EACN,SAAS,EACR,gBAJD,QAAQ,IAAI,aAAa,gBAAgB,CAAC,SAAS,GAAG,EAAE,EAKvD,EACD;;EAEF,MAAM,UAAU,SAAS,KAAK;AAC7B,OAAI,SAAS,yBAAyB,QAAQ,QAAQ,IAAI,SAAS,CAClE;;;;;GAMD,MAAM,aAAa,QAAQ,QAAQ,IAAI,cAAc;AACrD,OAAI,CAAC,WACJ;GAED,MAAM,MAAM,QAAQ,OAAO;AAC3B,OAAI,QAAQ,IAAI,UAAU,WAAW;AACrC,UAAO,EACN,SAAS,KACT;;EAEF,OAAO,EACN,OAAO,CACN;GACC,QAAQ,SAAS;AAChB,WAAO,CAAC,EACP,QAAQ,MAAM,WAAW,YAAY,IACrC,QAAQ,MAAM,WAAW,mBAAmB,IAC5C,QAAQ,MAAM,WAAW,qBAAqB,IAC9C,QAAQ,MAAM,WAAW,gBAAgB;;GAG3C,SAAS,qBAAqB,OAAO,QAAQ;IAC5C,MAAM,UAAU,IAAI,QAAQ;IAC5B,MAAM,WAAW,SAAS,IAAI,WAAW;AACzC,QAAI,CAAC,SACJ;AAGD,QADmB,SAAS,SAAS,wBAAwB,CAE5D;AAQD,QAAI,CANmB,IAAI,QAAQ,eAAe,QAChD,WAAmB,CAAC,OAAO,WAAW,OAAO,CAC9C,CACsC,MAAM,WAC5C,UAAU,WAAW,OAAO,CAC5B,CAEA;IAED,MAAM,SAAS,SAAS,IAAI,aAAa;AACzC,QAAI,CAAC,OACJ;IAED,MAAM,MAAM,IAAI,IAAI,SAAS;AAC7B,QAAI,aAAa,IAAI,UAAU,OAAO;AACtC,QAAI,UAAU,YAAY,IAAI,UAAU,CAAC;KACxC;GACF,CACD,EACD;EACD,WAAW,EACV,wBACA;EACD;EACA"}
@@ -0,0 +1,23 @@
1
+ import { Awaitable } from "@better-auth/core";
2
+
3
+ //#region src/plugins/last-login-method.d.ts
4
+ interface LastLoginMethodClientConfig {
5
+ storage: {
6
+ setItem: (key: string, value: string) => any;
7
+ getItem: (key: string) => string | null;
8
+ deleteItemAsync: (key: string) => Awaitable<void>;
9
+ };
10
+ /**
11
+ * Prefix for local storage keys (e.g., "my-app_last_login_method")
12
+ * @default "better-auth"
13
+ */
14
+ storagePrefix?: string | undefined;
15
+ /**
16
+ * Custom resolve method for retrieving the last login method
17
+ */
18
+ customResolveMethod?: ((url: string | URL) => Awaitable<string | undefined | null>) | undefined;
19
+ }
20
+ declare const lastLoginMethodClient: (config: LastLoginMethodClientConfig) => BetterAuthClientPlugin;
21
+ //#endregion
22
+ export { LastLoginMethodClientConfig, lastLoginMethodClient };
23
+ //# sourceMappingURL=index.d.mts.map
@@ -0,0 +1,47 @@
1
+ //#region src/plugins/last-login-method.ts
2
+ const paths = [
3
+ "/callback/",
4
+ "/oauth2/callback/",
5
+ "/sign-in/email",
6
+ "/sign-up/email"
7
+ ];
8
+ const defaultResolveMethod = (url) => {
9
+ const { pathname } = new URL(url.toString(), "http://localhost");
10
+ if (paths.some((p) => pathname.includes(p))) return pathname.split("/").pop();
11
+ if (pathname.includes("siwe")) return "siwe";
12
+ if (pathname.includes("/passkey/verify-authentication")) return "passkey";
13
+ };
14
+ const lastLoginMethodClient = (config) => {
15
+ const resolveMethod = config.customResolveMethod || defaultResolveMethod;
16
+ const lastLoginMethodName = `${config.storagePrefix || "better-auth"}_last_login_method`;
17
+ const storage = config.storage;
18
+ return {
19
+ id: "last-login-method-expo",
20
+ fetchPlugins: [{
21
+ id: "last-login-method-expo",
22
+ name: "Last Login Method",
23
+ hooks: { onResponse: async (ctx) => {
24
+ const lastMethod = await resolveMethod(ctx.request.url);
25
+ if (!lastMethod) return;
26
+ await storage.setItem(lastLoginMethodName, lastMethod);
27
+ } }
28
+ }],
29
+ getActions() {
30
+ return {
31
+ getLastUsedLoginMethod: () => {
32
+ return storage.getItem(lastLoginMethodName);
33
+ },
34
+ clearLastUsedLoginMethod: async () => {
35
+ await storage.deleteItemAsync(lastLoginMethodName);
36
+ },
37
+ isLastUsedLoginMethod: (method) => {
38
+ return storage.getItem(lastLoginMethodName) === method;
39
+ }
40
+ };
41
+ }
42
+ };
43
+ };
44
+
45
+ //#endregion
46
+ export { lastLoginMethodClient };
47
+ //# sourceMappingURL=index.mjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.mjs","names":[],"sources":["../../src/plugins/last-login-method.ts"],"sourcesContent":["import type { Awaitable, BetterAuthClientPlugin } from \"@better-auth/core\";\n\nexport interface LastLoginMethodClientConfig {\n\tstorage: {\n\t\tsetItem: (key: string, value: string) => any;\n\t\tgetItem: (key: string) => string | null;\n\t\tdeleteItemAsync: (key: string) => Awaitable<void>;\n\t};\n\t/**\n\t * Prefix for local storage keys (e.g., \"my-app_last_login_method\")\n\t * @default \"better-auth\"\n\t */\n\tstoragePrefix?: string | undefined;\n\t/**\n\t * Custom resolve method for retrieving the last login method\n\t */\n\tcustomResolveMethod?:\n\t\t| ((url: string | URL) => Awaitable<string | undefined | null>)\n\t\t| undefined;\n}\n\nconst paths = [\n\t\"/callback/\",\n\t\"/oauth2/callback/\",\n\t\"/sign-in/email\",\n\t\"/sign-up/email\",\n];\nconst defaultResolveMethod = (url: string | URL) => {\n\tconst { pathname } = new URL(url.toString(), \"http://localhost\");\n\n\tif (paths.some((p) => pathname.includes(p))) {\n\t\treturn pathname.split(\"/\").pop();\n\t}\n\tif (pathname.includes(\"siwe\")) return \"siwe\";\n\tif (pathname.includes(\"/passkey/verify-authentication\")) {\n\t\treturn \"passkey\";\n\t}\n\n\treturn;\n};\n\nexport const lastLoginMethodClient = (config: LastLoginMethodClientConfig) => {\n\tconst resolveMethod = config.customResolveMethod || defaultResolveMethod;\n\tconst storagePrefix = config.storagePrefix || \"better-auth\";\n\tconst lastLoginMethodName = `${storagePrefix}_last_login_method`;\n\tconst storage = config.storage;\n\n\treturn {\n\t\tid: \"last-login-method-expo\",\n\t\tfetchPlugins: [\n\t\t\t{\n\t\t\t\tid: \"last-login-method-expo\",\n\t\t\t\tname: \"Last Login Method\",\n\t\t\t\thooks: {\n\t\t\t\t\tonResponse: async (ctx) => {\n\t\t\t\t\t\tconst lastMethod = await resolveMethod(ctx.request.url);\n\t\t\t\t\t\tif (!lastMethod) {\n\t\t\t\t\t\t\treturn;\n\t\t\t\t\t\t}\n\n\t\t\t\t\t\tawait storage.setItem(lastLoginMethodName, lastMethod);\n\t\t\t\t\t},\n\t\t\t\t},\n\t\t\t},\n\t\t],\n\t\tgetActions() {\n\t\t\treturn {\n\t\t\t\t/**\n\t\t\t\t * Get the last used login method from storage\n\t\t\t\t *\n\t\t\t\t * @returns The last used login method or null if not found\n\t\t\t\t */\n\t\t\t\tgetLastUsedLoginMethod: (): string | null => {\n\t\t\t\t\treturn storage.getItem(lastLoginMethodName);\n\t\t\t\t},\n\t\t\t\t/**\n\t\t\t\t * Clear the last used login method from storage\n\t\t\t\t */\n\t\t\t\tclearLastUsedLoginMethod: async () => {\n\t\t\t\t\tawait storage.deleteItemAsync(lastLoginMethodName);\n\t\t\t\t},\n\t\t\t\t/**\n\t\t\t\t * Check if a specific login method was the last used\n\t\t\t\t * @param method The method to check\n\t\t\t\t * @returns True if the method was the last used, false otherwise\n\t\t\t\t */\n\t\t\t\tisLastUsedLoginMethod: (method: string): boolean => {\n\t\t\t\t\tconst lastMethod = storage.getItem(lastLoginMethodName);\n\t\t\t\t\treturn lastMethod === method;\n\t\t\t\t},\n\t\t\t};\n\t\t},\n\t} satisfies BetterAuthClientPlugin;\n};\n"],"mappings":";AAqBA,MAAM,QAAQ;CACb;CACA;CACA;CACA;CACA;AACD,MAAM,wBAAwB,QAAsB;CACnD,MAAM,EAAE,aAAa,IAAI,IAAI,IAAI,UAAU,EAAE,mBAAmB;AAEhE,KAAI,MAAM,MAAM,MAAM,SAAS,SAAS,EAAE,CAAC,CAC1C,QAAO,SAAS,MAAM,IAAI,CAAC,KAAK;AAEjC,KAAI,SAAS,SAAS,OAAO,CAAE,QAAO;AACtC,KAAI,SAAS,SAAS,iCAAiC,CACtD,QAAO;;AAMT,MAAa,yBAAyB,WAAwC;CAC7E,MAAM,gBAAgB,OAAO,uBAAuB;CAEpD,MAAM,sBAAsB,GADN,OAAO,iBAAiB,cACD;CAC7C,MAAM,UAAU,OAAO;AAEvB,QAAO;EACN,IAAI;EACJ,cAAc,CACb;GACC,IAAI;GACJ,MAAM;GACN,OAAO,EACN,YAAY,OAAO,QAAQ;IAC1B,MAAM,aAAa,MAAM,cAAc,IAAI,QAAQ,IAAI;AACvD,QAAI,CAAC,WACJ;AAGD,UAAM,QAAQ,QAAQ,qBAAqB,WAAW;MAEvD;GACD,CACD;EACD,aAAa;AACZ,UAAO;IAMN,8BAA6C;AAC5C,YAAO,QAAQ,QAAQ,oBAAoB;;IAK5C,0BAA0B,YAAY;AACrC,WAAM,QAAQ,gBAAgB,oBAAoB;;IAOnD,wBAAwB,WAA4B;AAEnD,YADmB,QAAQ,QAAQ,oBAAoB,KACjC;;IAEvB;;EAEF"}
package/package.json ADDED
@@ -0,0 +1,103 @@
1
+ {
2
+ "name": "@hammadj/better-auth-expo",
3
+ "version": "1.5.0-beta.9",
4
+ "type": "module",
5
+ "description": "Better Auth integration for Expo and React Native applications.",
6
+ "main": "dist/index.mjs",
7
+ "module": "dist/index.mjs",
8
+ "types": "dist/index.d.mts",
9
+ "repository": {
10
+ "type": "git",
11
+ "url": "git+https://github.com/META-DREAMER/better-auth.git",
12
+ "directory": "packages/expo"
13
+ },
14
+ "homepage": "https://www.better-auth.com/docs/integrations/expo",
15
+ "exports": {
16
+ ".": {
17
+ "dev-source": "./src/index.ts",
18
+ "types": "./dist/index.d.mts",
19
+ "default": "./dist/index.mjs"
20
+ },
21
+ "./client": {
22
+ "dev-source": "./src/client.ts",
23
+ "types": "./dist/client.d.mts",
24
+ "default": "./dist/client.mjs"
25
+ },
26
+ "./plugins": {
27
+ "dev-source": "./src/plugins/index.ts",
28
+ "types": "./dist/plugins/index.d.mts",
29
+ "default": "./dist/plugins/index.mjs"
30
+ }
31
+ },
32
+ "typesVersions": {
33
+ "*": {
34
+ "*": [
35
+ "./dist/index.d.mts"
36
+ ],
37
+ "client": [
38
+ "./dist/client.d.mts"
39
+ ],
40
+ "plugins": [
41
+ "./dist/plugins/index.d.mts"
42
+ ]
43
+ }
44
+ },
45
+ "keywords": [
46
+ "auth",
47
+ "expo",
48
+ "react-native",
49
+ "typescript",
50
+ "better-auth"
51
+ ],
52
+ "publishConfig": {
53
+ "access": "public"
54
+ },
55
+ "license": "MIT",
56
+ "devDependencies": {
57
+ "@better-fetch/fetch": "1.1.21",
58
+ "expo-constants": "~18.0.13",
59
+ "expo-linking": "~8.0.11",
60
+ "expo-network": "^8.0.8",
61
+ "expo-web-browser": "~15.0.10",
62
+ "react-native": "~0.83.1",
63
+ "tsdown": "^0.20.1",
64
+ "@hammadj/better-auth-core": "1.5.0-beta.9",
65
+ "@hammadj/better-auth": "1.5.0-beta.9"
66
+ },
67
+ "peerDependencies": {
68
+ "expo-constants": ">=17.0.0",
69
+ "expo-linking": ">=7.0.0",
70
+ "expo-network": "^8.0.7",
71
+ "expo-web-browser": ">=14.0.0",
72
+ "@hammadj/better-auth-core": "1.5.0-beta.9",
73
+ "@hammadj/better-auth": "1.5.0-beta.9"
74
+ },
75
+ "peerDependenciesMeta": {
76
+ "expo-constants": {
77
+ "optional": true
78
+ },
79
+ "expo-linking": {
80
+ "optional": true
81
+ },
82
+ "expo-web-browser": {
83
+ "optional": true
84
+ }
85
+ },
86
+ "dependencies": {
87
+ "@better-fetch/fetch": "1.1.21",
88
+ "better-call": "1.2.0",
89
+ "zod": "^4.3.6"
90
+ },
91
+ "files": [
92
+ "dist"
93
+ ],
94
+ "scripts": {
95
+ "test": "vitest",
96
+ "coverage": "vitest run --coverage --coverage.provider=istanbul",
97
+ "lint:types": "attw --profile esm-only --pack .",
98
+ "lint:package": "publint run --strict",
99
+ "build": "tsdown",
100
+ "dev": "tsdown --watch",
101
+ "typecheck": "tsc --project tsconfig.json"
102
+ }
103
+ }