@better-auth/expo 1.7.0-rc.2 → 1.7.0-rc.4

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/dist/client.d.ts CHANGED
@@ -1,6 +1,7 @@
1
- import { parseSetCookieHeader } from "better-auth/cookies";
1
+ import { parseSetCookieHeader } from "better-auth/cookies/utils";
2
2
  import { FocusManager, OnlineManager } from "better-auth/client";
3
3
  import { ClientFetchOption, ClientStore } from "@better-auth/core";
4
+ import * as SecureStore from "expo-secure-store";
4
5
  //#region src/focus-manager.d.ts
5
6
  declare function setupExpoFocusManager(): FocusManager;
6
7
  //#endregion
@@ -8,12 +9,13 @@ declare function setupExpoFocusManager(): FocusManager;
8
9
  declare function setupExpoOnlineManager(): OnlineManager;
9
10
  //#endregion
10
11
  //#region src/client.d.ts
12
+ /**
13
+ * Storage used by the Expo client for cookies and cached session data.
14
+ */
15
+ type ExpoClientStorage = Pick<typeof SecureStore, "setItem" | "setItemAsync" | "getItem" | "getItemAsync">;
11
16
  interface ExpoClientOptions {
12
17
  scheme?: string | undefined;
13
- storage: {
14
- setItem: (key: string, value: string) => any;
15
- getItem: (key: string) => string | null;
16
- };
18
+ storage: ExpoClientStorage;
17
19
  /**
18
20
  * Prefix for local storage keys (e.g., "my-app_cookie", "my-app_session_data")
19
21
  * @default "better-auth"
@@ -52,7 +54,7 @@ interface ExpoClientOptions {
52
54
  webBrowserOptions?: import("expo-web-browser").AuthSessionOpenOptions;
53
55
  }
54
56
  declare function getSetCookie(header: string, prevCookie?: string | undefined): string;
55
- declare function getCookie(cookie: string): string;
57
+ declare function getCookie(cookie: string | null): string;
56
58
  /**
57
59
  * Check if the Set-Cookie header contains better-auth cookies.
58
60
  * This prevents infinite refetching when non-better-auth cookies (like third-party cookies) change.
@@ -79,16 +81,14 @@ declare function hasBetterAuthCookies(setCookieHeader: string, cookiePrefix: str
79
81
  * @returns normalized cookie name
80
82
  */
81
83
  declare function normalizeCookieName(name: string): string;
82
- declare function storageAdapter(storage: {
83
- getItem: (name: string) => string | null;
84
- setItem: (name: string, value: string) => unknown;
85
- }): {
84
+ declare function storageAdapter(storage: ExpoClientStorage): {
86
85
  /**
87
86
  * Reads a value, reassembling it if it was split across chunk keys. A value
88
87
  * that fit is returned as-is (values written before chunking still read
89
88
  * back); a missing chunk returns `null` so a torn write fails closed.
90
89
  */
91
90
  getItem: (name: string) => string | null;
91
+ getItemAsync: (name: string) => Promise<string | null>;
92
92
  /**
93
93
  * Stores `value`, splitting it across chunk keys when it exceeds the
94
94
  * per-write limit. The base key is cleared before the chunks are rewritten
@@ -97,28 +97,27 @@ declare function storageAdapter(storage: {
97
97
  * Failures are logged, not thrown: persistence is best-effort and must not
98
98
  * break the request.
99
99
  */
100
- setItem: (name: string, value: string) => Promise<void>;
100
+ setItem: (name: string, value: string) => void;
101
+ setItemAsync: (name: string, value: string) => Promise<void>;
101
102
  };
102
103
  declare const expoClient: (opts: ExpoClientOptions) => {
103
104
  id: "expo";
104
105
  version: string;
105
- getActions(_: import("@better-fetch/fetch").BetterFetch, $store: ClientStore): {
106
+ getActions(_fetch: unknown, $store: ClientStore): {
106
107
  /**
107
108
  * Get the stored cookie.
108
109
  *
109
- * You can use this to get the cookie stored in the device and use it in your fetch
110
- * requests.
111
- *
112
110
  * @example
113
111
  * ```ts
114
- * const cookie = client.getCookie();
112
+ * const cookie = await client.getCookie();
115
113
  * fetch("https://api.example.com", {
116
114
  * headers: {
117
115
  * cookie,
118
116
  * },
119
117
  * });
118
+ * ```
120
119
  */
121
- getCookie: () => string;
120
+ getCookie: () => Promise<string>;
122
121
  };
123
122
  fetchPlugins: {
124
123
  id: string;
@@ -187,4 +186,4 @@ declare const expoClient: (opts: ExpoClientOptions) => {
187
186
  }[];
188
187
  };
189
188
  //#endregion
190
- export { expoClient, getCookie, getSetCookie, hasBetterAuthCookies, normalizeCookieName, parseSetCookieHeader, setupExpoFocusManager, setupExpoOnlineManager, storageAdapter };
189
+ export { ExpoClientStorage, expoClient, getCookie, getSetCookie, hasBetterAuthCookies, normalizeCookieName, parseSetCookieHeader, setupExpoFocusManager, setupExpoOnlineManager, storageAdapter };
package/dist/client.js CHANGED
@@ -1,6 +1,6 @@
1
- import { t as PACKAGE_VERSION } from "./version-m-Zfjx6T.js";
1
+ import { t as PACKAGE_VERSION } from "./version-Do0kTnv7.js";
2
2
  import { safeJSONParse } from "@better-auth/core/utils/json";
3
- import { SECURE_COOKIE_PREFIX, parseSetCookieHeader, parseSetCookieHeader as parseSetCookieHeader$1, stripSecureCookiePrefix } from "better-auth/cookies";
3
+ import { SECURE_COOKIE_PREFIX, parseSetCookieHeader, parseSetCookieHeader as parseSetCookieHeader$1, stripSecureCookiePrefix } from "better-auth/cookies/utils";
4
4
  import Constants from "expo-constants";
5
5
  import * as Linking from "expo-linking";
6
6
  import { AppState, Platform } from "react-native";
@@ -81,6 +81,9 @@ if (Platform.OS !== "web") {
81
81
  setupExpoFocusManager();
82
82
  setupExpoOnlineManager();
83
83
  }
84
+ function defineExpoClientPlugin(plugin) {
85
+ return plugin;
86
+ }
84
87
  function getSetCookie(header, prevCookie) {
85
88
  const parsed = parseSetCookieHeader$1(header);
86
89
  const toSetCookie = safeJSONParse(prevCookie) ?? {};
@@ -104,10 +107,7 @@ function getSetCookie(header, prevCookie) {
104
107
  return JSON.stringify(toSetCookie);
105
108
  }
106
109
  function getCookie(cookie) {
107
- let parsed = {};
108
- try {
109
- parsed = JSON.parse(cookie);
110
- } catch {}
110
+ const parsed = safeJSONParse(cookie) ?? {};
111
111
  return Object.entries(parsed).reduce((acc, [key, value]) => {
112
112
  if (value.expires && new Date(value.expires) < /* @__PURE__ */ new Date()) return acc;
113
113
  return acc ? `${acc}; ${key}=${value.value}` : `${key}=${value.value}`;
@@ -210,6 +210,17 @@ const STORAGE_VALUE_LIMIT = 1800;
210
210
  * survives the native storage bridge without C-string truncation.
211
211
  */
212
212
  const CHUNK_MARKER = "ba-chunks:";
213
+ function getStorageWrites(key, value) {
214
+ if (value.length <= STORAGE_VALUE_LIMIT) return [[key, value]];
215
+ const count = Math.ceil(value.length / STORAGE_VALUE_LIMIT);
216
+ const writes = [[key, ""]];
217
+ for (let i = 0; i < count; i++) {
218
+ const start = i * STORAGE_VALUE_LIMIT;
219
+ writes.push([`${key}.${i}`, value.slice(start, start + STORAGE_VALUE_LIMIT)]);
220
+ }
221
+ writes.push([key, `${CHUNK_MARKER}${count}`]);
222
+ return writes;
223
+ }
213
224
  function storageAdapter(storage) {
214
225
  return {
215
226
  /**
@@ -231,6 +242,20 @@ function storageAdapter(storage) {
231
242
  }
232
243
  return value;
233
244
  },
245
+ getItemAsync: async (name) => {
246
+ const key = normalizeCookieName(name);
247
+ const stored = await storage.getItemAsync(key);
248
+ if (stored == null || !stored.startsWith(CHUNK_MARKER)) return stored;
249
+ const count = Number(stored.slice(11));
250
+ if (!Number.isInteger(count) || count < 1) return null;
251
+ let value = "";
252
+ for (let i = 0; i < count; i++) {
253
+ const chunk = await storage.getItemAsync(`${key}.${i}`);
254
+ if (chunk == null) return null;
255
+ value += chunk;
256
+ }
257
+ return value;
258
+ },
234
259
  /**
235
260
  * Stores `value`, splitting it across chunk keys when it exceeds the
236
261
  * per-write limit. The base key is cleared before the chunks are rewritten
@@ -239,20 +264,18 @@ function storageAdapter(storage) {
239
264
  * Failures are logged, not thrown: persistence is best-effort and must not
240
265
  * break the request.
241
266
  */
242
- setItem: async (name, value) => {
267
+ setItem: (name, value) => {
243
268
  const key = normalizeCookieName(name);
244
269
  try {
245
- if (value.length <= STORAGE_VALUE_LIMIT) {
246
- await storage.setItem(key, value);
247
- return;
248
- }
249
- await storage.setItem(key, "");
250
- const count = Math.ceil(value.length / STORAGE_VALUE_LIMIT);
251
- for (let i = 0; i < count; i++) {
252
- const start = i * STORAGE_VALUE_LIMIT;
253
- await storage.setItem(`${key}.${i}`, value.slice(start, start + STORAGE_VALUE_LIMIT));
254
- }
255
- await storage.setItem(key, `${CHUNK_MARKER}${count}`);
270
+ for (const [writeKey, writeValue] of getStorageWrites(key, value)) storage.setItem(writeKey, writeValue);
271
+ } catch (error) {
272
+ console.error(`[better-auth/expo] failed to persist "${key}" to storage`, error);
273
+ }
274
+ },
275
+ setItemAsync: async (name, value) => {
276
+ const key = normalizeCookieName(name);
277
+ try {
278
+ for (const [writeKey, writeValue] of getStorageWrites(key, value)) await storage.setItemAsync(writeKey, writeValue);
256
279
  } catch (error) {
257
280
  console.error(`[better-auth/expo] failed to persist "${key}" to storage`, error);
258
281
  }
@@ -264,57 +287,67 @@ const expoClient = (opts) => {
264
287
  const storagePrefix = opts?.storagePrefix || "better-auth";
265
288
  const cookieName = `${storagePrefix}_cookie`;
266
289
  const localCacheName = `${storagePrefix}_session_data`;
267
- const storage = storageAdapter(opts?.storage);
290
+ const storage = storageAdapter(opts.storage);
268
291
  const isWeb = Platform.OS === "web";
269
292
  const cookiePrefix = opts?.cookiePrefix || "better-auth";
293
+ let sessionCacheHydration;
294
+ const restoreSessionCache = async () => {
295
+ if (isWeb || opts?.disableCache) return;
296
+ const sessionAtom = store?.atoms.session;
297
+ if (!sessionAtom) return;
298
+ const initialSessionState = sessionAtom.get();
299
+ if (initialSessionState.data !== null) return;
300
+ const raw = await storage.getItemAsync(localCacheName);
301
+ const cached = raw ? safeJSONParse(raw) : null;
302
+ const expiresAt = cached?.session?.expiresAt;
303
+ const expiresAtMs = expiresAt ? new Date(expiresAt).getTime() : NaN;
304
+ if (!!cached?.user?.id && !!cached.session?.id && expiresAtMs > Date.now() && sessionAtom.get() === initialSessionState) sessionAtom.set({
305
+ ...initialSessionState,
306
+ data: cached,
307
+ error: null
308
+ });
309
+ };
310
+ const hydrateSessionCache = () => {
311
+ if (!sessionCacheHydration) sessionCacheHydration = restoreSessionCache().catch((error) => {
312
+ sessionCacheHydration = void 0;
313
+ throw error;
314
+ });
315
+ return sessionCacheHydration;
316
+ };
270
317
  const clearSessionCache = async () => {
271
- await storage.setItem(cookieName, "{}");
318
+ await storage.setItemAsync(cookieName, "{}");
272
319
  store?.atoms.session?.set({
273
320
  ...store.atoms.session.get(),
274
321
  data: null,
275
322
  error: null,
276
323
  isPending: false
277
324
  });
278
- await storage.setItem(localCacheName, "{}");
325
+ await storage.setItemAsync(localCacheName, "{}");
279
326
  };
280
327
  const rawScheme = opts?.scheme || Constants.expoConfig?.scheme || Constants.platform?.scheme;
281
328
  const scheme = Array.isArray(rawScheme) ? rawScheme[0] : rawScheme;
282
329
  if (!scheme && !isWeb) throw new Error("Scheme not found in app.json. Please provide a scheme in the options.");
283
- return {
330
+ return defineExpoClientPlugin({
284
331
  id: "expo",
285
332
  version: PACKAGE_VERSION,
286
- getActions(_, $store) {
333
+ getActions(_fetch, $store) {
287
334
  store = $store;
288
- const sessionAtom = $store.atoms.session;
289
- if (!isWeb && !opts?.disableCache && sessionAtom) {
290
- const raw = storage.getItem(localCacheName);
291
- const cached = raw ? safeJSONParse(raw) : null;
292
- const exp = cached?.session?.expiresAt;
293
- const expMs = exp ? new Date(exp).getTime() : NaN;
294
- if (!!cached?.user?.id && !!cached.session?.id && expMs > Date.now()) sessionAtom.set({
295
- ...sessionAtom.get(),
296
- data: cached,
297
- error: null
298
- });
299
- }
300
335
  return {
301
336
  /**
302
337
  * Get the stored cookie.
303
338
  *
304
- * You can use this to get the cookie stored in the device and use it in your fetch
305
- * requests.
306
- *
307
339
  * @example
308
340
  * ```ts
309
- * const cookie = client.getCookie();
341
+ * const cookie = await client.getCookie();
310
342
  * fetch("https://api.example.com", {
311
343
  * headers: {
312
344
  * cookie,
313
345
  * },
314
346
  * });
347
+ * ```
315
348
  */
316
- getCookie: () => {
317
- return getCookie(storage.getItem(cookieName) || "{}");
349
+ getCookie: async () => {
350
+ return getCookie(await storage.getItemAsync(cookieName));
318
351
  } };
319
352
  },
320
353
  fetchPlugins: [{
@@ -322,23 +355,26 @@ getCookie: () => {
322
355
  name: "Expo",
323
356
  hooks: { async onSuccess(context) {
324
357
  if (isWeb) return;
358
+ const { pathname } = new URL(context.request.url);
325
359
  const setCookie = context.response.headers.get("set-cookie");
326
360
  if (setCookie) {
327
361
  if (hasBetterAuthCookies(setCookie, cookiePrefix)) {
328
- const prevCookie = storage.getItem(cookieName);
362
+ const prevCookie = await storage.getItemAsync(cookieName);
329
363
  const toSetCookie = getSetCookie(setCookie || "", prevCookie ?? void 0);
330
364
  if (hasSessionCookieChanged(prevCookie, toSetCookie)) {
331
- await storage.setItem(cookieName, toSetCookie);
365
+ await storage.setItemAsync(cookieName, toSetCookie);
332
366
  store?.notify("$sessionSignal");
333
- } else await storage.setItem(cookieName, toSetCookie);
367
+ } else await storage.setItemAsync(cookieName, toSetCookie);
334
368
  }
335
369
  }
336
- if (context.request.url.toString().includes("/get-session") && !opts?.disableCache) {
370
+ if (pathname.endsWith("/get-session") && !opts?.disableCache) {
337
371
  const data = context.data;
338
- await storage.setItem(localCacheName, JSON.stringify(data));
372
+ await storage.setItemAsync(localCacheName, JSON.stringify(data));
339
373
  }
340
- if (context.request.url.toString().includes("/sign-out")) await clearSessionCache();
341
- if (context.data?.redirect && (context.request.url.toString().includes("/sign-in") || context.request.url.toString().includes("/link-social")) && !context.request?.body.includes("idToken")) {
374
+ if (pathname.endsWith("/sign-out")) await clearSessionCache();
375
+ const isSignInRequest = pathname.endsWith("/sign-in") || pathname.includes("/sign-in/");
376
+ const isLinkSocialRequest = pathname.endsWith("/link-social");
377
+ if (context.data?.redirect && (isSignInRequest || isLinkSocialRequest) && !context.request?.body.includes("idToken")) {
342
378
  const to = JSON.parse(context.request.body)?.callbackURL;
343
379
  const signInURL = context.data?.url;
344
380
  let Browser = void 0;
@@ -354,7 +390,7 @@ getCookie: () => {
354
390
  if (Platform.OS === "android") try {
355
391
  Browser.dismissAuthSession();
356
392
  } catch {}
357
- const oauthStateValue = getOAuthStateValue(storage.getItem(cookieName), cookiePrefix);
393
+ const oauthStateValue = getOAuthStateValue(await storage.getItemAsync(cookieName), cookiePrefix);
358
394
  const params = new URLSearchParams({ authorizationURL: signInURL });
359
395
  if (oauthStateValue) params.append("oauthState", oauthStateValue);
360
396
  const proxyURL = `${context.request.baseURL}/expo-authorization-proxy?${params.toString()}`;
@@ -362,8 +398,8 @@ getCookie: () => {
362
398
  if (result.type !== "success") return;
363
399
  const cookie = new URL(result.url).searchParams.get("cookie");
364
400
  if (!cookie) return;
365
- const toSetCookie = getSetCookie(cookie, storage.getItem(cookieName) ?? void 0);
366
- await storage.setItem(cookieName, toSetCookie);
401
+ const toSetCookie = getSetCookie(cookie, await storage.getItemAsync(cookieName) ?? void 0);
402
+ await storage.setItemAsync(cookieName, toSetCookie);
367
403
  store?.notify("$sessionSignal");
368
404
  }
369
405
  } },
@@ -372,17 +408,19 @@ getCookie: () => {
372
408
  url,
373
409
  options
374
410
  };
411
+ const { pathname } = new URL(url, options?.baseURL);
412
+ if (pathname.endsWith("/get-session")) await hydrateSessionCache();
375
413
  options = options || {};
376
414
  options.credentials = "omit";
377
415
  if (options.body?.idToken !== void 0) {
378
- const cookie = url.includes("/link-social") ? getCookie(storage.getItem(cookieName) || "{}") : "";
416
+ const cookie = getCookie(pathname.endsWith("/link-social") ? await storage.getItemAsync(cookieName) : null);
379
417
  options.headers = {
380
418
  ...options.headers,
381
419
  ...cookie ? { cookie } : {},
382
420
  "x-skip-oauth-proxy": "true"
383
421
  };
384
422
  } else {
385
- const cookie = getCookie(storage.getItem(cookieName) || "{}");
423
+ const cookie = getCookie(await storage.getItemAsync(cookieName));
386
424
  options.headers = {
387
425
  ...options.headers,
388
426
  ...cookie ? { cookie } : {},
@@ -407,7 +445,7 @@ getCookie: () => {
407
445
  options.body.errorCallbackURL = url;
408
446
  }
409
447
  }
410
- if (url.includes("/sign-out")) await clearSessionCache();
448
+ if (pathname.endsWith("/sign-out")) await clearSessionCache();
411
449
  }
412
450
  return {
413
451
  url,
@@ -415,7 +453,7 @@ getCookie: () => {
415
453
  };
416
454
  }
417
455
  }]
418
- };
456
+ });
419
457
  };
420
458
  //#endregion
421
459
  export { expoClient, getCookie, getSetCookie, hasBetterAuthCookies, normalizeCookieName, parseSetCookieHeader, setupExpoFocusManager, setupExpoOnlineManager, storageAdapter };
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-m-Zfjx6T.js";
1
+ import { t as PACKAGE_VERSION } from "./version-Do0kTnv7.js";
2
2
  import { createAuthMiddleware } from "@better-auth/core/api";
3
3
  import { HIDE_METADATA } from "better-auth";
4
4
  import { APIError, createAuthEndpoint } from "better-auth/api";
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "../version-m-Zfjx6T.js";
1
+ import { t as PACKAGE_VERSION } from "../version-Do0kTnv7.js";
2
2
  //#region src/plugins/last-login-method.ts
3
3
  const paths = [
4
4
  "/callback/",
@@ -1,5 +1,5 @@
1
1
  //#endregion
2
2
  //#region src/version.ts
3
- const PACKAGE_VERSION = "1.7.0-rc.2";
3
+ const PACKAGE_VERSION = "1.7.0-rc.4";
4
4
  //#endregion
5
5
  export { PACKAGE_VERSION as t };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@better-auth/expo",
3
- "version": "1.7.0-rc.2",
3
+ "version": "1.7.0-rc.4",
4
4
  "description": "Better Auth integration for Expo and React Native applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -67,19 +67,21 @@
67
67
  "expo-constants": "~56.0.18",
68
68
  "expo-linking": "~56.0.14",
69
69
  "expo-network": "~56.0.5",
70
+ "expo-secure-store": "~56.0.4",
70
71
  "expo-web-browser": "~56.0.5",
71
72
  "react-native": "~0.86.0",
72
73
  "tsdown": "0.22.7",
73
- "@better-auth/core": "1.7.0-rc.2",
74
- "better-auth": "1.7.0-rc.2"
74
+ "@better-auth/core": "1.7.0-rc.4",
75
+ "better-auth": "1.7.0-rc.4"
75
76
  },
76
77
  "peerDependencies": {
77
78
  "expo-constants": ">=17.0.0",
78
79
  "expo-linking": ">=7.0.0",
79
80
  "expo-network": ">=8.0.7",
81
+ "expo-secure-store": ">=12.5.0",
80
82
  "expo-web-browser": ">=14.0.0",
81
- "@better-auth/core": "^1.7.0-rc.2",
82
- "better-auth": "^1.7.0-rc.2"
83
+ "@better-auth/core": "^1.7.0-rc.4",
84
+ "better-auth": "^1.7.0-rc.4"
83
85
  },
84
86
  "peerDependenciesMeta": {
85
87
  "expo-constants": {
@@ -91,6 +93,9 @@
91
93
  "expo-network": {
92
94
  "optional": true
93
95
  },
96
+ "expo-secure-store": {
97
+ "optional": true
98
+ },
94
99
  "expo-web-browser": {
95
100
  "optional": true
96
101
  }