@better-auth/expo 1.7.3 → 1.7.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
@@ -5,6 +5,35 @@ import * as _$_better_fetch_fetch0 from "@better-fetch/fetch";
5
5
  import { ClientFetchOption, ClientStore } from "@better-auth/core";
6
6
  import * as SecureStore from "expo-secure-store";
7
7
 
8
+ //#region src/client-storage.d.ts
9
+ /**
10
+ * Storage used by the Expo client for cookies and cached session data.
11
+ * Async access is coordinated through the provided object, so reuse it across
12
+ * clients that access the same stored data.
13
+ */
14
+ type ExpoClientStorage = Pick<typeof SecureStore, "setItem" | "setItemAsync" | "getItem" | "getItemAsync">;
15
+ /**
16
+ * Expo secure store does not support colons in the keys.
17
+ * This function replaces colons with underscores.
18
+ *
19
+ * @see https://github.com/better-auth/better-auth/issues/5426
20
+ *
21
+ * @param name cookie name to be saved in the storage
22
+ * @returns normalized cookie name
23
+ */
24
+ declare function normalizeCookieName(name: string): string;
25
+ interface ExpoStorageAdapter {
26
+ getItem(name: string): string | null;
27
+ getItemAsync(name: string): Promise<string | null>;
28
+ setItem(name: string, value: string): void;
29
+ setItemAsync(name: string, value: string): Promise<void>;
30
+ }
31
+ /**
32
+ * Wraps Expo storage with chunking, recoverable writes, and serialized async
33
+ * access.
34
+ */
35
+ declare function storageAdapter(storage: ExpoClientStorage): ExpoStorageAdapter;
36
+ //#endregion
8
37
  //#region src/focus-manager.d.ts
9
38
  declare function setupExpoFocusManager(): FocusManager;
10
39
  //#endregion
@@ -12,12 +41,6 @@ declare function setupExpoFocusManager(): FocusManager;
12
41
  declare function setupExpoOnlineManager(): OnlineManager;
13
42
  //#endregion
14
43
  //#region src/client.d.ts
15
- /**
16
- * Storage used by the Expo client for cookies and cached session data.
17
- * Write coordination is scoped to the provided object, so reuse it across
18
- * clients that access the same stored data.
19
- */
20
- type ExpoClientStorage = Pick<typeof SecureStore, "setItem" | "setItemAsync" | "getItem" | "getItemAsync">;
21
44
  interface ExpoClientOptions {
22
45
  scheme?: string | undefined;
23
46
  storage: ExpoClientStorage;
@@ -76,27 +99,6 @@ declare function getCookie(cookie: string | null): string;
76
99
  * @returns true if the header contains better-auth cookies, false otherwise
77
100
  */
78
101
  declare function hasBetterAuthCookies(setCookieHeader: string, cookiePrefix: string | string[]): boolean;
79
- /**
80
- * Expo secure store does not support colons in the keys.
81
- * This function replaces colons with underscores.
82
- *
83
- * @see https://github.com/better-auth/better-auth/issues/5426
84
- *
85
- * @param name cookie name to be saved in the storage
86
- * @returns normalized cookie name
87
- */
88
- declare function normalizeCookieName(name: string): string;
89
- interface ExpoStorageAdapter {
90
- getItem(name: string): string | null;
91
- getItemAsync(name: string): Promise<string | null>;
92
- setItem(name: string, value: string): void;
93
- setItemAsync(name: string, value: string): Promise<void>;
94
- }
95
- /**
96
- * Wraps Expo storage with chunking, recoverable writes, and serialized async
97
- * updates.
98
- */
99
- declare function storageAdapter(storage: ExpoClientStorage): ExpoStorageAdapter;
100
102
  declare const expoClient: (opts: ExpoClientOptions) => {
101
103
  id: "expo";
102
104
  version: string;
@@ -183,4 +185,4 @@ declare const expoClient: (opts: ExpoClientOptions) => {
183
185
  }[];
184
186
  };
185
187
  //#endregion
186
- export { ExpoClientStorage, expoClient, getCookie, getSetCookie, hasBetterAuthCookies, normalizeCookieName, parseSetCookieHeader, setupExpoFocusManager, setupExpoOnlineManager, storageAdapter };
188
+ export { type ExpoClientStorage, expoClient, getCookie, getSetCookie, hasBetterAuthCookies, normalizeCookieName, parseSetCookieHeader, setupExpoFocusManager, setupExpoOnlineManager, storageAdapter };
package/dist/client.js CHANGED
@@ -1,9 +1,10 @@
1
- import { t as PACKAGE_VERSION } from "./version-DddGjO8z.js";
1
+ import { t as PACKAGE_VERSION } from "./version-CuQDLAg7.js";
2
2
  import { safeJSONParse } from "@better-auth/core/utils/json";
3
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";
7
+ import { logger } from "@better-auth/core/env";
7
8
  import { kFocusManager, kOnlineManager } from "better-auth/client";
8
9
  //#region \0rolldown/runtime.js
9
10
  var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require : typeof Proxy !== "undefined" ? new Proxy(x, { get: (a, b) => (typeof require !== "undefined" ? require : a)[b] }) : x)(function(x) {
@@ -11,178 +12,7 @@ var __require = /* @__PURE__ */ ((x) => typeof require !== "undefined" ? require
11
12
  throw Error("Calling `require` for \"" + x + "\" in an environment that doesn't expose the `require` function. See https://rolldown.rs/in-depth/bundling-cjs#require-external-modules for more details.");
12
13
  });
13
14
  //#endregion
14
- //#region src/focus-manager.ts
15
- var ExpoFocusManager = class {
16
- listeners = /* @__PURE__ */ new Set();
17
- subscription;
18
- isFocused;
19
- subscribe(listener) {
20
- this.listeners.add(listener);
21
- return () => {
22
- this.listeners.delete(listener);
23
- };
24
- }
25
- setFocused(focused) {
26
- if (this.isFocused === focused) return;
27
- this.isFocused = focused;
28
- this.listeners.forEach((listener) => listener(focused));
29
- }
30
- setup() {
31
- this.subscription = AppState.addEventListener("change", (state) => {
32
- this.setFocused(state === "active");
33
- });
34
- return () => {
35
- this.subscription?.remove();
36
- };
37
- }
38
- };
39
- function setupExpoFocusManager() {
40
- if (!globalThis[kFocusManager]) globalThis[kFocusManager] = new ExpoFocusManager();
41
- return globalThis[kFocusManager];
42
- }
43
- //#endregion
44
- //#region src/online-manager.ts
45
- var ExpoOnlineManager = class {
46
- listeners = /* @__PURE__ */ new Set();
47
- isOnline = true;
48
- unsubscribe;
49
- subscribe(listener) {
50
- this.listeners.add(listener);
51
- return () => {
52
- this.listeners.delete(listener);
53
- };
54
- }
55
- setOnline(online) {
56
- if (this.isOnline === online) return;
57
- this.isOnline = online;
58
- this.listeners.forEach((listener) => listener(online));
59
- }
60
- setup() {
61
- import("expo-network").then(({ addNetworkStateListener }) => {
62
- const subscription = addNetworkStateListener((state) => {
63
- this.setOnline(!!state.isInternetReachable);
64
- });
65
- this.unsubscribe = () => subscription.remove();
66
- }).catch(() => {
67
- this.setOnline(true);
68
- });
69
- return () => {
70
- this.unsubscribe?.();
71
- };
72
- }
73
- };
74
- function setupExpoOnlineManager() {
75
- if (!globalThis[kOnlineManager]) globalThis[kOnlineManager] = new ExpoOnlineManager();
76
- return globalThis[kOnlineManager];
77
- }
78
- //#endregion
79
- //#region src/client.ts
80
- if (Platform.OS !== "web") {
81
- setupExpoFocusManager();
82
- setupExpoOnlineManager();
83
- }
84
- function defineExpoClientPlugin(plugin) {
85
- return plugin;
86
- }
87
- function getSetCookie(header, prevCookie) {
88
- const parsed = parseSetCookieHeader$1(header);
89
- const toSetCookie = safeJSONParse(prevCookie) ?? {};
90
- parsed.forEach((cookie, key) => {
91
- const expiresAt = cookie["expires"];
92
- const maxAge = cookie["max-age"];
93
- if (maxAge !== void 0 && Number(maxAge) <= 0) {
94
- delete toSetCookie[key];
95
- return;
96
- }
97
- const expires = maxAge ? new Date(Date.now() + Number(maxAge) * 1e3) : expiresAt ? new Date(String(expiresAt)) : null;
98
- if (expires && expires.getTime() <= Date.now()) {
99
- delete toSetCookie[key];
100
- return;
101
- }
102
- toSetCookie[key] = {
103
- value: cookie["value"],
104
- expires: expires ? expires.toISOString() : null
105
- };
106
- });
107
- return JSON.stringify(toSetCookie);
108
- }
109
- function getCookie(cookie) {
110
- const parsed = safeJSONParse(cookie) ?? {};
111
- return Object.entries(parsed).reduce((acc, [key, value]) => {
112
- if (value.expires && new Date(value.expires) < /* @__PURE__ */ new Date()) return acc;
113
- return acc ? `${acc}; ${key}=${value.value}` : `${key}=${value.value}`;
114
- }, "");
115
- }
116
- function getOAuthStateValue(cookieJson, cookiePrefix) {
117
- if (!cookieJson) return null;
118
- const parsed = safeJSONParse(cookieJson);
119
- if (!parsed) return null;
120
- const prefixes = Array.isArray(cookiePrefix) ? cookiePrefix : [cookiePrefix];
121
- for (const prefix of prefixes) {
122
- const candidates = [`${SECURE_COOKIE_PREFIX}${prefix}.oauth_state`, `${prefix}.oauth_state`];
123
- for (const name of candidates) {
124
- const value = parsed?.[name]?.value;
125
- if (value) return value;
126
- }
127
- }
128
- return null;
129
- }
130
- function getOrigin(scheme) {
131
- return Linking.createURL("", { scheme });
132
- }
133
- /**
134
- * Compare if session cookies have actually changed by comparing their values.
135
- * Ignores expiry timestamps that naturally change on each request.
136
- *
137
- * @param prevCookie - Previous cookie JSON string
138
- * @param newCookie - New cookie JSON string
139
- * @returns true if session cookies have changed, false otherwise
140
- */
141
- function hasSessionCookieChanged(prevCookie, newCookie) {
142
- if (!prevCookie) return true;
143
- try {
144
- const prev = JSON.parse(prevCookie);
145
- const next = JSON.parse(newCookie);
146
- const sessionKeys = /* @__PURE__ */ new Set();
147
- Object.keys(prev).forEach((key) => {
148
- if (key.includes("session_token") || key.includes("session_data")) sessionKeys.add(key);
149
- });
150
- Object.keys(next).forEach((key) => {
151
- if (key.includes("session_token") || key.includes("session_data")) sessionKeys.add(key);
152
- });
153
- for (const key of sessionKeys) if (prev[key]?.value !== next[key]?.value) return true;
154
- return false;
155
- } catch {
156
- return true;
157
- }
158
- }
159
- /**
160
- * Check if the Set-Cookie header contains better-auth cookies.
161
- * This prevents infinite refetching when non-better-auth cookies (like third-party cookies) change.
162
- *
163
- * Supports multiple cookie naming patterns:
164
- * - Default: "better-auth.session_token", "better-auth-passkey", "__Secure-better-auth.session_token"
165
- * - Custom prefix: "myapp.session_token", "myapp-passkey", "__Secure-myapp.session_token"
166
- * - Custom full names: "my_custom_session_token", "custom_session_data"
167
- * - No prefix (cookiePrefix=""): matches any cookie with known suffixes
168
- * - Multiple prefixes: ["better-auth", "my-app"] matches cookies starting with any of the prefixes
169
- *
170
- * @param setCookieHeader - The Set-Cookie header value
171
- * @param cookiePrefix - The cookie prefix(es) to check for. Can be a string, array of strings, or empty string.
172
- * @returns true if the header contains better-auth cookies, false otherwise
173
- */
174
- function hasBetterAuthCookies(setCookieHeader, cookiePrefix) {
175
- const cookies = parseSetCookieHeader$1(setCookieHeader);
176
- const cookieSuffixes = ["session_token", "session_data"];
177
- const prefixes = Array.isArray(cookiePrefix) ? cookiePrefix : [cookiePrefix];
178
- for (const name of cookies.keys()) {
179
- const nameWithoutSecure = stripSecureCookiePrefix(name);
180
- for (const prefix of prefixes) if (prefix) {
181
- if (nameWithoutSecure.startsWith(prefix)) return true;
182
- } else for (const suffix of cookieSuffixes) if (nameWithoutSecure.endsWith(suffix)) return true;
183
- }
184
- return false;
185
- }
15
+ //#region src/client-storage.ts
186
16
  /**
187
17
  * Expo secure store does not support colons in the keys.
188
18
  * This function replaces colons with underscores.
@@ -196,15 +26,38 @@ function normalizeCookieName(name) {
196
26
  return name.replace(/:/g, "_");
197
27
  }
198
28
  /**
199
- * Max characters written per `setItem`. Native secure stores silently reject
200
- * oversized writes (iOS Keychain refuses values above ~2KB), losing the cookie,
201
- * so a larger value is split across keys here. Mirrors the server's
202
- * `chunkCookie`/`joinChunks` in `session-store.ts`; keep the two in sync.
29
+ * UTF-8 byte budget per stored chunk. Some native stores reject large values,
30
+ * so larger strings are split across keys.
203
31
  *
204
32
  * @see https://github.com/better-auth/better-auth/issues/9151
205
33
  */
206
34
  const STORAGE_VALUE_LIMIT = 1800;
207
35
  const MAX_STORAGE_CHUNKS = 100;
36
+ function getUtf8ByteLength(character) {
37
+ const codePoint = character.codePointAt(0) ?? 0;
38
+ if (codePoint <= 127) return 1;
39
+ if (codePoint <= 2047) return 2;
40
+ if (codePoint <= 65535) return 3;
41
+ return 4;
42
+ }
43
+ function splitStorageValue(value) {
44
+ const chunks = [];
45
+ let start = 0;
46
+ let end = 0;
47
+ let bytes = 0;
48
+ for (const character of value) {
49
+ const characterBytes = getUtf8ByteLength(character);
50
+ if (bytes + characterBytes > STORAGE_VALUE_LIMIT) {
51
+ chunks.push(value.slice(start, end));
52
+ start = end;
53
+ bytes = 0;
54
+ }
55
+ bytes += characterBytes;
56
+ end += character.length;
57
+ }
58
+ chunks.push(value.slice(start));
59
+ return chunks;
60
+ }
208
61
  /**
209
62
  * Marks a base key whose value is split across multiple storage keys. Legacy
210
63
  * markers contain only the chunk count. Current markers also identify the
@@ -213,6 +66,11 @@ const MAX_STORAGE_CHUNKS = 100;
213
66
  * @see https://github.com/better-auth/better-auth/issues/11082
214
67
  */
215
68
  const CHUNK_MARKER = "ba-chunks:";
69
+ const CHUNK_SLOTS = [
70
+ null,
71
+ 0,
72
+ 1
73
+ ];
216
74
  function parseChunkCount(value) {
217
75
  if (value === void 0) return null;
218
76
  const count = Number(value);
@@ -254,7 +112,7 @@ function readChunks(storage, key, marker) {
254
112
  let value = "";
255
113
  for (let i = 0; i < marker.count; i++) {
256
114
  const chunk = storage.getItem(getChunkKey(key, marker, i));
257
- if (chunk == null) return null;
115
+ if (!chunk) return null;
258
116
  value += chunk;
259
117
  }
260
118
  return value;
@@ -263,7 +121,7 @@ async function readChunksAsync(storage, key, marker) {
263
121
  let value = "";
264
122
  for (let i = 0; i < marker.count; i++) {
265
123
  const chunk = await storage.getItemAsync(getChunkKey(key, marker, i));
266
- if (chunk == null) return null;
124
+ if (!chunk) return null;
267
125
  value += chunk;
268
126
  }
269
127
  return value;
@@ -292,11 +150,15 @@ async function readStoredValueAsync(storage, key, baseValue) {
292
150
  fallbackCount: null
293
151
  });
294
152
  }
295
- function getStorageWrites(key, value, currentBaseValue) {
296
- if (value.length <= STORAGE_VALUE_LIMIT) return [[key, value]];
297
- const count = Math.ceil(value.length / STORAGE_VALUE_LIMIT);
298
- if (count > MAX_STORAGE_CHUNKS) throw new Error(`Storage value requires ${count} chunks, exceeding the limit of ${MAX_STORAGE_CHUNKS}`);
153
+ function getStorageWritePlan(key, value, currentBaseValue) {
299
154
  const currentMarker = currentBaseValue?.startsWith(CHUNK_MARKER) ? parseChunkMarker(currentBaseValue) : null;
155
+ const chunks = splitStorageValue(value);
156
+ if (chunks.length === 1) return {
157
+ writes: [[key, value]],
158
+ cleanup: getUnusedChunkRanges(key, currentMarker, null)
159
+ };
160
+ const count = chunks.length;
161
+ if (count > MAX_STORAGE_CHUNKS) throw new Error(`Storage value requires ${count} chunks, exceeding the limit of ${MAX_STORAGE_CHUNKS}`);
300
162
  const marker = {
301
163
  count,
302
164
  slot: currentMarker?.slot === 0 ? 1 : 0,
@@ -307,82 +169,190 @@ function getStorageWrites(key, value, currentBaseValue) {
307
169
  ...currentMarker,
308
170
  fallbackCount: null
309
171
  })]);
310
- for (let i = 0; i < count; i++) {
311
- const start = i * STORAGE_VALUE_LIMIT;
312
- writes.push([getChunkKey(key, marker, i), value.slice(start, start + STORAGE_VALUE_LIMIT)]);
313
- }
172
+ for (const [index, chunk] of chunks.entries()) writes.push([getChunkKey(key, marker, index), chunk]);
314
173
  writes.push([key, serializeChunkMarker(marker)]);
315
- return writes;
316
- }
317
- function createKeyedWriteQueue() {
318
- const tails = /* @__PURE__ */ new Map();
319
174
  return {
320
- pending(key) {
321
- return tails.has(key);
322
- },
323
- enqueue(key, operation) {
324
- const queued = (tails.get(key) ?? Promise.resolve()).then(operation, operation);
325
- tails.set(key, queued);
326
- const cleanup = () => {
327
- if (tails.get(key) === queued) tails.delete(key);
328
- };
329
- queued.then(cleanup, cleanup);
330
- return queued;
331
- }
175
+ writes,
176
+ cleanup: getUnusedChunkRanges(key, currentMarker, marker)
177
+ };
178
+ }
179
+ function getSlotChunkCount(marker, slot) {
180
+ if (!marker) return 0;
181
+ if (marker.slot === slot) return marker.count;
182
+ if (marker.slot === null || slot === null) return 0;
183
+ return marker.fallbackCount ?? 0;
184
+ }
185
+ function getUnusedChunkRanges(key, previousMarker, marker) {
186
+ return CHUNK_SLOTS.map((slot) => ({
187
+ prefix: slot === null ? key : `${key}.${slot}`,
188
+ start: getSlotChunkCount(marker, slot),
189
+ end: getSlotChunkCount(previousMarker, slot)
190
+ }));
191
+ }
192
+ const storageStates = /* @__PURE__ */ new WeakMap();
193
+ function getStorageState(storage, key) {
194
+ let states = storageStates.get(storage);
195
+ if (!states) {
196
+ states = /* @__PURE__ */ new Map();
197
+ storageStates.set(storage, states);
198
+ }
199
+ let state = states.get(key);
200
+ if (!state) {
201
+ state = {
202
+ pending: null,
203
+ pendingWrites: 0,
204
+ activeRead: null,
205
+ cleanupComplete: false
206
+ };
207
+ states.set(key, state);
208
+ }
209
+ return state;
210
+ }
211
+ function enqueueStorageOperation(state, operation) {
212
+ const queued = (state.pending ?? Promise.resolve()).then(operation, operation);
213
+ state.pending = queued;
214
+ const cleanup = () => {
215
+ if (state.pending === queued) state.pending = null;
332
216
  };
217
+ queued.then(cleanup, cleanup);
218
+ return queued;
333
219
  }
334
- const storageWriteQueues = /* @__PURE__ */ new WeakMap();
335
- function getStorageWriteQueue(storage) {
336
- const existing = storageWriteQueues.get(storage);
337
- if (existing) return existing;
338
- const queue = createKeyedWriteQueue();
339
- storageWriteQueues.set(storage, queue);
340
- return queue;
220
+ function enqueueStorageWrite(state, operation) {
221
+ state.pendingWrites++;
222
+ return enqueueStorageOperation(state, async () => {
223
+ try {
224
+ return await operation();
225
+ } finally {
226
+ state.pendingWrites--;
227
+ }
228
+ });
341
229
  }
230
+ /** @internal */
342
231
  function createManagedStorage(storage) {
343
- const writeQueue = getStorageWriteQueue(storage);
344
232
  const logWriteError = (key, error) => {
345
- console.error(`[better-auth/expo] failed to persist "${key}" to storage`, error);
233
+ logger.error(`[better-auth/expo] failed to persist "${key}" to storage`, error);
234
+ };
235
+ const logCleanupError = (key, error) => {
236
+ logger.error(`[better-auth/expo] failed to clear unused chunks for "${key}"`, error);
346
237
  };
347
238
  const getItem = (name) => {
348
239
  const key = normalizeCookieName(name);
349
240
  return readStoredValue(storage, key, storage.getItem(key));
350
241
  };
351
- const getItemAsync = async (name) => {
242
+ const getItemAsync = (name) => {
352
243
  const key = normalizeCookieName(name);
353
- return readStoredValueAsync(storage, key, await storage.getItemAsync(key));
244
+ const state = getStorageState(storage, key);
245
+ return enqueueStorageOperation(state, async () => {
246
+ const read = { snapshot: null };
247
+ state.activeRead = read;
248
+ try {
249
+ const value = await readStoredValueAsync(storage, key, await storage.getItemAsync(key));
250
+ return read.snapshot ? read.snapshot.value : value;
251
+ } finally {
252
+ state.activeRead = null;
253
+ }
254
+ });
354
255
  };
355
256
  const writeItem = (key, value, currentBaseValue) => {
356
- for (const [writeKey, writeValue] of getStorageWrites(key, value, currentBaseValue)) storage.setItem(writeKey, writeValue);
257
+ const state = getStorageState(storage, key);
258
+ const { writes, cleanup } = getStorageWritePlan(key, value, currentBaseValue);
259
+ for (const [writeKey, writeValue] of writes) storage.setItem(writeKey, writeValue);
260
+ try {
261
+ const scanContiguousOrphans = !state.cleanupComplete;
262
+ for (const { prefix, start, end } of cleanup) {
263
+ if (scanContiguousOrphans) {
264
+ const orphanStart = Math.max(start, end);
265
+ let orphanEnd = orphanStart;
266
+ for (; orphanEnd < MAX_STORAGE_CHUNKS; orphanEnd++) if (!storage.getItem(`${prefix}.${orphanEnd}`)) break;
267
+ for (let i = orphanEnd - 1; i >= orphanStart; i--) storage.setItem(`${prefix}.${i}`, "");
268
+ }
269
+ for (let i = end - 1; i >= start; i--) storage.setItem(`${prefix}.${i}`, "");
270
+ }
271
+ state.cleanupComplete = true;
272
+ } catch (error) {
273
+ state.cleanupComplete = false;
274
+ logCleanupError(key, error);
275
+ }
357
276
  };
358
277
  const writeItemAsync = async (key, value, currentBaseValue) => {
359
- for (const [writeKey, writeValue] of getStorageWrites(key, value, currentBaseValue)) await storage.setItemAsync(writeKey, writeValue);
278
+ const state = getStorageState(storage, key);
279
+ const { writes, cleanup } = getStorageWritePlan(key, value, currentBaseValue);
280
+ for (const [writeKey, writeValue] of writes) await storage.setItemAsync(writeKey, writeValue);
281
+ try {
282
+ const scanContiguousOrphans = !state.cleanupComplete;
283
+ for (const { prefix, start, end } of cleanup) {
284
+ if (scanContiguousOrphans) {
285
+ const orphanStart = Math.max(start, end);
286
+ let orphanEnd = orphanStart;
287
+ for (; orphanEnd < MAX_STORAGE_CHUNKS; orphanEnd++) if (!await storage.getItemAsync(`${prefix}.${orphanEnd}`)) break;
288
+ for (let i = orphanEnd - 1; i >= orphanStart; i--) await storage.setItemAsync(`${prefix}.${i}`, "");
289
+ }
290
+ for (let i = end - 1; i >= start; i--) await storage.setItemAsync(`${prefix}.${i}`, "");
291
+ }
292
+ state.cleanupComplete = true;
293
+ } catch (error) {
294
+ state.cleanupComplete = false;
295
+ logCleanupError(key, error);
296
+ }
360
297
  };
361
298
  const setItem = (name, value) => {
362
299
  const key = normalizeCookieName(name);
363
- if (writeQueue.pending(key)) {
300
+ const state = getStorageState(storage, key);
301
+ if (state.pendingWrites > 0) {
364
302
  logWriteError(key, /* @__PURE__ */ new Error("Cannot write synchronously while an async write is pending"));
365
303
  return;
366
304
  }
305
+ let currentBaseValue = null;
367
306
  try {
368
- writeItem(key, value, value.length > STORAGE_VALUE_LIMIT ? storage.getItem(key) : null);
307
+ currentBaseValue = storage.getItem(key);
369
308
  } catch (error) {
309
+ state.cleanupComplete = false;
310
+ if (splitStorageValue(value).length > 1) {
311
+ logWriteError(key, error);
312
+ return;
313
+ }
314
+ }
315
+ const read = state.activeRead;
316
+ if (read && read.snapshot === null) try {
317
+ read.snapshot = { value: readStoredValue(storage, key, currentBaseValue) };
318
+ } catch {
319
+ read.snapshot = { value: null };
320
+ }
321
+ try {
322
+ writeItem(key, value, currentBaseValue);
323
+ } catch (error) {
324
+ state.cleanupComplete = false;
370
325
  logWriteError(key, error);
326
+ return;
371
327
  }
328
+ if (read) read.snapshot = { value };
372
329
  };
373
330
  const setItemAsync = (name, value) => {
374
331
  const key = normalizeCookieName(name);
375
- return writeQueue.enqueue(key, async () => {
332
+ const state = getStorageState(storage, key);
333
+ return enqueueStorageWrite(state, async () => {
334
+ let currentBaseValue = null;
335
+ try {
336
+ currentBaseValue = await storage.getItemAsync(key);
337
+ } catch (error) {
338
+ state.cleanupComplete = false;
339
+ if (splitStorageValue(value).length > 1) {
340
+ logWriteError(key, error);
341
+ return;
342
+ }
343
+ }
376
344
  try {
377
- await writeItemAsync(key, value, value.length > STORAGE_VALUE_LIMIT ? await storage.getItemAsync(key) : null);
345
+ await writeItemAsync(key, value, currentBaseValue);
378
346
  } catch (error) {
347
+ state.cleanupComplete = false;
379
348
  logWriteError(key, error);
380
349
  }
381
350
  });
382
351
  };
383
352
  const updateItemAsync = (name, update) => {
384
353
  const key = normalizeCookieName(name);
385
- return writeQueue.enqueue(key, async () => {
354
+ const state = getStorageState(storage, key);
355
+ return enqueueStorageWrite(state, async () => {
386
356
  try {
387
357
  const currentBaseValue = await storage.getItemAsync(key);
388
358
  const previousValue = await readStoredValueAsync(storage, key, currentBaseValue);
@@ -393,6 +363,7 @@ function createManagedStorage(storage) {
393
363
  value
394
364
  };
395
365
  } catch (error) {
366
+ state.cleanupComplete = false;
396
367
  logWriteError(key, error);
397
368
  return null;
398
369
  }
@@ -408,7 +379,7 @@ function createManagedStorage(storage) {
408
379
  }
409
380
  /**
410
381
  * Wraps Expo storage with chunking, recoverable writes, and serialized async
411
- * updates.
382
+ * access.
412
383
  */
413
384
  function storageAdapter(storage) {
414
385
  const managedStorage = createManagedStorage(storage);
@@ -419,6 +390,179 @@ function storageAdapter(storage) {
419
390
  setItemAsync: managedStorage.setItemAsync
420
391
  };
421
392
  }
393
+ //#endregion
394
+ //#region src/focus-manager.ts
395
+ var ExpoFocusManager = class {
396
+ listeners = /* @__PURE__ */ new Set();
397
+ subscription;
398
+ isFocused;
399
+ subscribe(listener) {
400
+ this.listeners.add(listener);
401
+ return () => {
402
+ this.listeners.delete(listener);
403
+ };
404
+ }
405
+ setFocused(focused) {
406
+ if (this.isFocused === focused) return;
407
+ this.isFocused = focused;
408
+ this.listeners.forEach((listener) => listener(focused));
409
+ }
410
+ setup() {
411
+ this.subscription = AppState.addEventListener("change", (state) => {
412
+ this.setFocused(state === "active");
413
+ });
414
+ return () => {
415
+ this.subscription?.remove();
416
+ };
417
+ }
418
+ };
419
+ function setupExpoFocusManager() {
420
+ if (!globalThis[kFocusManager]) globalThis[kFocusManager] = new ExpoFocusManager();
421
+ return globalThis[kFocusManager];
422
+ }
423
+ //#endregion
424
+ //#region src/online-manager.ts
425
+ var ExpoOnlineManager = class {
426
+ listeners = /* @__PURE__ */ new Set();
427
+ isOnline = true;
428
+ unsubscribe;
429
+ subscribe(listener) {
430
+ this.listeners.add(listener);
431
+ return () => {
432
+ this.listeners.delete(listener);
433
+ };
434
+ }
435
+ setOnline(online) {
436
+ if (this.isOnline === online) return;
437
+ this.isOnline = online;
438
+ this.listeners.forEach((listener) => listener(online));
439
+ }
440
+ setup() {
441
+ import("expo-network").then(({ addNetworkStateListener }) => {
442
+ const subscription = addNetworkStateListener((state) => {
443
+ this.setOnline(!!state.isInternetReachable);
444
+ });
445
+ this.unsubscribe = () => subscription.remove();
446
+ }).catch(() => {
447
+ this.setOnline(true);
448
+ });
449
+ return () => {
450
+ this.unsubscribe?.();
451
+ };
452
+ }
453
+ };
454
+ function setupExpoOnlineManager() {
455
+ if (!globalThis[kOnlineManager]) globalThis[kOnlineManager] = new ExpoOnlineManager();
456
+ return globalThis[kOnlineManager];
457
+ }
458
+ //#endregion
459
+ //#region src/client.ts
460
+ if (Platform.OS !== "web") {
461
+ setupExpoFocusManager();
462
+ setupExpoOnlineManager();
463
+ }
464
+ function defineExpoClientPlugin(plugin) {
465
+ return plugin;
466
+ }
467
+ function getSetCookie(header, prevCookie) {
468
+ const parsed = parseSetCookieHeader$1(header);
469
+ const toSetCookie = safeJSONParse(prevCookie) ?? {};
470
+ parsed.forEach((cookie, key) => {
471
+ const expiresAt = cookie["expires"];
472
+ const maxAge = cookie["max-age"];
473
+ if (maxAge !== void 0 && Number(maxAge) <= 0) {
474
+ delete toSetCookie[key];
475
+ return;
476
+ }
477
+ const expires = maxAge ? new Date(Date.now() + Number(maxAge) * 1e3) : expiresAt ? new Date(String(expiresAt)) : null;
478
+ if (expires && expires.getTime() <= Date.now()) {
479
+ delete toSetCookie[key];
480
+ return;
481
+ }
482
+ toSetCookie[key] = {
483
+ value: cookie["value"],
484
+ expires: expires ? expires.toISOString() : null
485
+ };
486
+ });
487
+ return JSON.stringify(toSetCookie);
488
+ }
489
+ function getCookie(cookie) {
490
+ const parsed = safeJSONParse(cookie) ?? {};
491
+ return Object.entries(parsed).reduce((acc, [key, value]) => {
492
+ if (value.expires && new Date(value.expires) < /* @__PURE__ */ new Date()) return acc;
493
+ return acc ? `${acc}; ${key}=${value.value}` : `${key}=${value.value}`;
494
+ }, "");
495
+ }
496
+ function getOAuthStateValue(cookieJson, cookiePrefix) {
497
+ if (!cookieJson) return null;
498
+ const parsed = safeJSONParse(cookieJson);
499
+ if (!parsed) return null;
500
+ const prefixes = Array.isArray(cookiePrefix) ? cookiePrefix : [cookiePrefix];
501
+ for (const prefix of prefixes) {
502
+ const candidates = [`${SECURE_COOKIE_PREFIX}${prefix}.oauth_state`, `${prefix}.oauth_state`];
503
+ for (const name of candidates) {
504
+ const value = parsed?.[name]?.value;
505
+ if (value) return value;
506
+ }
507
+ }
508
+ return null;
509
+ }
510
+ function getOrigin(scheme) {
511
+ return Linking.createURL("", { scheme });
512
+ }
513
+ /**
514
+ * Compare if session cookies have actually changed by comparing their values.
515
+ * Ignores expiry timestamps that naturally change on each request.
516
+ *
517
+ * @param prevCookie - Previous cookie JSON string
518
+ * @param newCookie - New cookie JSON string
519
+ * @returns true if session cookies have changed, false otherwise
520
+ */
521
+ function hasSessionCookieChanged(prevCookie, newCookie) {
522
+ if (!prevCookie) return true;
523
+ try {
524
+ const prev = JSON.parse(prevCookie);
525
+ const next = JSON.parse(newCookie);
526
+ const sessionKeys = /* @__PURE__ */ new Set();
527
+ Object.keys(prev).forEach((key) => {
528
+ if (key.includes("session_token") || key.includes("session_data")) sessionKeys.add(key);
529
+ });
530
+ Object.keys(next).forEach((key) => {
531
+ if (key.includes("session_token") || key.includes("session_data")) sessionKeys.add(key);
532
+ });
533
+ for (const key of sessionKeys) if (prev[key]?.value !== next[key]?.value) return true;
534
+ return false;
535
+ } catch {
536
+ return true;
537
+ }
538
+ }
539
+ /**
540
+ * Check if the Set-Cookie header contains better-auth cookies.
541
+ * This prevents infinite refetching when non-better-auth cookies (like third-party cookies) change.
542
+ *
543
+ * Supports multiple cookie naming patterns:
544
+ * - Default: "better-auth.session_token", "better-auth-passkey", "__Secure-better-auth.session_token"
545
+ * - Custom prefix: "myapp.session_token", "myapp-passkey", "__Secure-myapp.session_token"
546
+ * - Custom full names: "my_custom_session_token", "custom_session_data"
547
+ * - No prefix (cookiePrefix=""): matches any cookie with known suffixes
548
+ * - Multiple prefixes: ["better-auth", "my-app"] matches cookies starting with any of the prefixes
549
+ *
550
+ * @param setCookieHeader - The Set-Cookie header value
551
+ * @param cookiePrefix - The cookie prefix(es) to check for. Can be a string, array of strings, or empty string.
552
+ * @returns true if the header contains better-auth cookies, false otherwise
553
+ */
554
+ function hasBetterAuthCookies(setCookieHeader, cookiePrefix) {
555
+ const cookies = parseSetCookieHeader$1(setCookieHeader);
556
+ const cookieSuffixes = ["session_token", "session_data"];
557
+ const prefixes = Array.isArray(cookiePrefix) ? cookiePrefix : [cookiePrefix];
558
+ for (const name of cookies.keys()) {
559
+ const nameWithoutSecure = stripSecureCookiePrefix(name);
560
+ for (const prefix of prefixes) if (prefix) {
561
+ if (nameWithoutSecure.startsWith(prefix)) return true;
562
+ } else for (const suffix of cookieSuffixes) if (nameWithoutSecure.endsWith(suffix)) return true;
563
+ }
564
+ return false;
565
+ }
422
566
  const expoClient = (opts) => {
423
567
  let store = null;
424
568
  const storagePrefix = opts?.storagePrefix || "better-auth";
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-DddGjO8z.js";
1
+ import { t as PACKAGE_VERSION } from "./version-CuQDLAg7.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-DddGjO8z.js";
1
+ import { t as PACKAGE_VERSION } from "../version-CuQDLAg7.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.3";
3
+ const PACKAGE_VERSION = "1.7.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.3",
3
+ "version": "1.7.4",
4
4
  "description": "Better Auth integration for Expo and React Native applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -71,8 +71,8 @@
71
71
  "expo-web-browser": "~56.0.5",
72
72
  "react-native": "~0.86.0",
73
73
  "tsdown": "0.21.10",
74
- "@better-auth/core": "1.7.3",
75
- "better-auth": "1.7.3"
74
+ "@better-auth/core": "1.7.4",
75
+ "better-auth": "1.7.4"
76
76
  },
77
77
  "peerDependencies": {
78
78
  "expo-constants": ">=17.0.0",
@@ -80,8 +80,8 @@
80
80
  "expo-network": ">=8.0.7",
81
81
  "expo-secure-store": ">=12.5.0",
82
82
  "expo-web-browser": ">=14.0.0",
83
- "@better-auth/core": "^1.7.3",
84
- "better-auth": "^1.7.3"
83
+ "@better-auth/core": "^1.7.4",
84
+ "better-auth": "^1.7.4"
85
85
  },
86
86
  "peerDependenciesMeta": {
87
87
  "expo-constants": {