@better-auth/expo 1.7.2 → 1.7.3

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
@@ -14,6 +14,8 @@ declare function setupExpoOnlineManager(): OnlineManager;
14
14
  //#region src/client.d.ts
15
15
  /**
16
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.
17
19
  */
18
20
  type ExpoClientStorage = Pick<typeof SecureStore, "setItem" | "setItemAsync" | "getItem" | "getItemAsync">;
19
21
  interface ExpoClientOptions {
@@ -84,25 +86,17 @@ declare function hasBetterAuthCookies(setCookieHeader: string, cookiePrefix: str
84
86
  * @returns normalized cookie name
85
87
  */
86
88
  declare function normalizeCookieName(name: string): string;
87
- declare function storageAdapter(storage: ExpoClientStorage): {
88
- /**
89
- * Reads a value, reassembling it if it was split across chunk keys. A value
90
- * that fit is returned as-is (values written before chunking still read
91
- * back); a missing chunk returns `null` so a torn write fails closed.
92
- */
93
- getItem: (name: string) => string | null;
94
- getItemAsync: (name: string) => Promise<string | null>;
95
- /**
96
- * Stores `value`, splitting it across chunk keys when it exceeds the
97
- * per-write limit. The base key is cleared before the chunks are rewritten
98
- * and set to the marker last, as the commit point, so a write interrupted
99
- * partway through reads as absent rather than a mix of old and new chunks.
100
- * Failures are logged, not thrown: persistence is best-effort and must not
101
- * break the request.
102
- */
103
- setItem: (name: string, value: string) => void;
104
- setItemAsync: (name: string, value: string) => Promise<void>;
105
- };
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;
106
100
  declare const expoClient: (opts: ExpoClientOptions) => {
107
101
  id: "expo";
108
102
  version: string;
package/dist/client.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-D-tGaVc4.js";
1
+ import { t as PACKAGE_VERSION } from "./version-DddGjO8z.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";
@@ -204,82 +204,219 @@ function normalizeCookieName(name) {
204
204
  * @see https://github.com/better-auth/better-auth/issues/9151
205
205
  */
206
206
  const STORAGE_VALUE_LIMIT = 1800;
207
+ const MAX_STORAGE_CHUNKS = 100;
207
208
  /**
208
- * Marks a base key whose value is split across `<key>.0..N` chunks. The leading
209
- * control char can't start a JSON value (so it never collides) and, unlike NUL,
210
- * survives the native storage bridge without C-string truncation.
209
+ * Marks a base key whose value is split across multiple storage keys. Legacy
210
+ * markers contain only the chunk count. Current markers also identify the
211
+ * active slot and retain the previous slot's chunk count for recovery.
212
+ *
213
+ * @see https://github.com/better-auth/better-auth/issues/11082
211
214
  */
212
215
  const CHUNK_MARKER = "ba-chunks:";
213
- function getStorageWrites(key, value) {
216
+ function parseChunkCount(value) {
217
+ if (value === void 0) return null;
218
+ const count = Number(value);
219
+ if (!Number.isInteger(count) || count < 1 || count > MAX_STORAGE_CHUNKS) return null;
220
+ return count;
221
+ }
222
+ function parseChunkMarker(baseValue) {
223
+ const parts = baseValue.slice(11).split(":");
224
+ if (parts.length > 3) return null;
225
+ const [countValue, slotValue, fallbackCountValue] = parts;
226
+ const count = parseChunkCount(countValue);
227
+ if (count === null) return null;
228
+ if (slotValue === void 0) return fallbackCountValue === void 0 ? {
229
+ count,
230
+ slot: null,
231
+ fallbackCount: null
232
+ } : null;
233
+ if (slotValue !== "0" && slotValue !== "1") return null;
234
+ const fallbackCount = parseChunkCount(fallbackCountValue);
235
+ if (fallbackCountValue !== void 0 && fallbackCount === null) return null;
236
+ return {
237
+ count,
238
+ slot: slotValue === "0" ? 0 : 1,
239
+ fallbackCount
240
+ };
241
+ }
242
+ function getChunkKey(key, marker, index) {
243
+ return marker.slot === null ? `${key}.${index}` : `${key}.${marker.slot}.${index}`;
244
+ }
245
+ function getOtherSlot(slot) {
246
+ return slot === 0 ? 1 : 0;
247
+ }
248
+ function serializeChunkMarker(marker) {
249
+ if (marker.slot === null) return `${CHUNK_MARKER}${marker.count}`;
250
+ const fallback = marker.fallbackCount === null ? "" : `:${marker.fallbackCount}`;
251
+ return `${CHUNK_MARKER}${marker.count}:${marker.slot}${fallback}`;
252
+ }
253
+ function readChunks(storage, key, marker) {
254
+ let value = "";
255
+ for (let i = 0; i < marker.count; i++) {
256
+ const chunk = storage.getItem(getChunkKey(key, marker, i));
257
+ if (chunk == null) return null;
258
+ value += chunk;
259
+ }
260
+ return value;
261
+ }
262
+ async function readChunksAsync(storage, key, marker) {
263
+ let value = "";
264
+ for (let i = 0; i < marker.count; i++) {
265
+ const chunk = await storage.getItemAsync(getChunkKey(key, marker, i));
266
+ if (chunk == null) return null;
267
+ value += chunk;
268
+ }
269
+ return value;
270
+ }
271
+ function readStoredValue(storage, key, baseValue) {
272
+ if (baseValue == null || !baseValue.startsWith(CHUNK_MARKER)) return baseValue;
273
+ const marker = parseChunkMarker(baseValue);
274
+ if (!marker) return null;
275
+ const value = readChunks(storage, key, marker);
276
+ if (value !== null || marker.slot === null || marker.fallbackCount === null) return value;
277
+ return readChunks(storage, key, {
278
+ count: marker.fallbackCount,
279
+ slot: getOtherSlot(marker.slot),
280
+ fallbackCount: null
281
+ });
282
+ }
283
+ async function readStoredValueAsync(storage, key, baseValue) {
284
+ if (baseValue == null || !baseValue.startsWith(CHUNK_MARKER)) return baseValue;
285
+ const marker = parseChunkMarker(baseValue);
286
+ if (!marker) return null;
287
+ const value = await readChunksAsync(storage, key, marker);
288
+ if (value !== null || marker.slot === null || marker.fallbackCount === null) return value;
289
+ return readChunksAsync(storage, key, {
290
+ count: marker.fallbackCount,
291
+ slot: getOtherSlot(marker.slot),
292
+ fallbackCount: null
293
+ });
294
+ }
295
+ function getStorageWrites(key, value, currentBaseValue) {
214
296
  if (value.length <= STORAGE_VALUE_LIMIT) return [[key, value]];
215
297
  const count = Math.ceil(value.length / STORAGE_VALUE_LIMIT);
216
- const writes = [[key, ""]];
298
+ if (count > MAX_STORAGE_CHUNKS) throw new Error(`Storage value requires ${count} chunks, exceeding the limit of ${MAX_STORAGE_CHUNKS}`);
299
+ const currentMarker = currentBaseValue?.startsWith(CHUNK_MARKER) ? parseChunkMarker(currentBaseValue) : null;
300
+ const marker = {
301
+ count,
302
+ slot: currentMarker?.slot === 0 ? 1 : 0,
303
+ fallbackCount: currentMarker?.slot == null ? null : currentMarker.count
304
+ };
305
+ const writes = [];
306
+ if (currentMarker?.slot != null && currentMarker.fallbackCount !== null) writes.push([key, serializeChunkMarker({
307
+ ...currentMarker,
308
+ fallbackCount: null
309
+ })]);
217
310
  for (let i = 0; i < count; i++) {
218
311
  const start = i * STORAGE_VALUE_LIMIT;
219
- writes.push([`${key}.${i}`, value.slice(start, start + STORAGE_VALUE_LIMIT)]);
312
+ writes.push([getChunkKey(key, marker, i), value.slice(start, start + STORAGE_VALUE_LIMIT)]);
220
313
  }
221
- writes.push([key, `${CHUNK_MARKER}${count}`]);
314
+ writes.push([key, serializeChunkMarker(marker)]);
222
315
  return writes;
223
316
  }
224
- function storageAdapter(storage) {
317
+ function createKeyedWriteQueue() {
318
+ const tails = /* @__PURE__ */ new Map();
225
319
  return {
226
- /**
227
- * Reads a value, reassembling it if it was split across chunk keys. A value
228
- * that fit is returned as-is (values written before chunking still read
229
- * back); a missing chunk returns `null` so a torn write fails closed.
230
- */
231
- getItem: (name) => {
232
- const key = normalizeCookieName(name);
233
- const stored = storage.getItem(key);
234
- if (stored == null || !stored.startsWith(CHUNK_MARKER)) return stored;
235
- const count = Number(stored.slice(11));
236
- if (!Number.isInteger(count) || count < 1) return null;
237
- let value = "";
238
- for (let i = 0; i < count; i++) {
239
- const chunk = storage.getItem(`${key}.${i}`);
240
- if (chunk == null) return null;
241
- value += chunk;
242
- }
243
- return value;
320
+ pending(key) {
321
+ return tails.has(key);
244
322
  },
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
- },
259
- /**
260
- * Stores `value`, splitting it across chunk keys when it exceeds the
261
- * per-write limit. The base key is cleared before the chunks are rewritten
262
- * and set to the marker last, as the commit point, so a write interrupted
263
- * partway through reads as absent rather than a mix of old and new chunks.
264
- * Failures are logged, not thrown: persistence is best-effort and must not
265
- * break the request.
266
- */
267
- setItem: (name, value) => {
268
- const key = normalizeCookieName(name);
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
+ }
332
+ };
333
+ }
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;
341
+ }
342
+ function createManagedStorage(storage) {
343
+ const writeQueue = getStorageWriteQueue(storage);
344
+ const logWriteError = (key, error) => {
345
+ console.error(`[better-auth/expo] failed to persist "${key}" to storage`, error);
346
+ };
347
+ const getItem = (name) => {
348
+ const key = normalizeCookieName(name);
349
+ return readStoredValue(storage, key, storage.getItem(key));
350
+ };
351
+ const getItemAsync = async (name) => {
352
+ const key = normalizeCookieName(name);
353
+ return readStoredValueAsync(storage, key, await storage.getItemAsync(key));
354
+ };
355
+ const writeItem = (key, value, currentBaseValue) => {
356
+ for (const [writeKey, writeValue] of getStorageWrites(key, value, currentBaseValue)) storage.setItem(writeKey, writeValue);
357
+ };
358
+ const writeItemAsync = async (key, value, currentBaseValue) => {
359
+ for (const [writeKey, writeValue] of getStorageWrites(key, value, currentBaseValue)) await storage.setItemAsync(writeKey, writeValue);
360
+ };
361
+ const setItem = (name, value) => {
362
+ const key = normalizeCookieName(name);
363
+ if (writeQueue.pending(key)) {
364
+ logWriteError(key, /* @__PURE__ */ new Error("Cannot write synchronously while an async write is pending"));
365
+ return;
366
+ }
367
+ try {
368
+ writeItem(key, value, value.length > STORAGE_VALUE_LIMIT ? storage.getItem(key) : null);
369
+ } catch (error) {
370
+ logWriteError(key, error);
371
+ }
372
+ };
373
+ const setItemAsync = (name, value) => {
374
+ const key = normalizeCookieName(name);
375
+ return writeQueue.enqueue(key, async () => {
269
376
  try {
270
- for (const [writeKey, writeValue] of getStorageWrites(key, value)) storage.setItem(writeKey, writeValue);
377
+ await writeItemAsync(key, value, value.length > STORAGE_VALUE_LIMIT ? await storage.getItemAsync(key) : null);
271
378
  } catch (error) {
272
- console.error(`[better-auth/expo] failed to persist "${key}" to storage`, error);
379
+ logWriteError(key, error);
273
380
  }
274
- },
275
- setItemAsync: async (name, value) => {
276
- const key = normalizeCookieName(name);
381
+ });
382
+ };
383
+ const updateItemAsync = (name, update) => {
384
+ const key = normalizeCookieName(name);
385
+ return writeQueue.enqueue(key, async () => {
277
386
  try {
278
- for (const [writeKey, writeValue] of getStorageWrites(key, value)) await storage.setItemAsync(writeKey, writeValue);
387
+ const currentBaseValue = await storage.getItemAsync(key);
388
+ const previousValue = await readStoredValueAsync(storage, key, currentBaseValue);
389
+ const value = update(previousValue);
390
+ await writeItemAsync(key, value, currentBaseValue);
391
+ return {
392
+ previousValue,
393
+ value
394
+ };
279
395
  } catch (error) {
280
- console.error(`[better-auth/expo] failed to persist "${key}" to storage`, error);
396
+ logWriteError(key, error);
397
+ return null;
281
398
  }
282
- }
399
+ });
400
+ };
401
+ return {
402
+ getItem,
403
+ getItemAsync,
404
+ setItem,
405
+ setItemAsync,
406
+ updateItemAsync
407
+ };
408
+ }
409
+ /**
410
+ * Wraps Expo storage with chunking, recoverable writes, and serialized async
411
+ * updates.
412
+ */
413
+ function storageAdapter(storage) {
414
+ const managedStorage = createManagedStorage(storage);
415
+ return {
416
+ getItem: managedStorage.getItem,
417
+ getItemAsync: managedStorage.getItemAsync,
418
+ setItem: managedStorage.setItem,
419
+ setItemAsync: managedStorage.setItemAsync
283
420
  };
284
421
  }
285
422
  const expoClient = (opts) => {
@@ -287,7 +424,7 @@ const expoClient = (opts) => {
287
424
  const storagePrefix = opts?.storagePrefix || "better-auth";
288
425
  const cookieName = `${storagePrefix}_cookie`;
289
426
  const localCacheName = `${storagePrefix}_session_data`;
290
- const storage = storageAdapter(opts.storage);
427
+ const storage = createManagedStorage(opts.storage);
291
428
  const isWeb = Platform.OS === "web";
292
429
  const cookiePrefix = opts?.cookiePrefix || "better-auth";
293
430
  let sessionCacheHydration;
@@ -359,12 +496,8 @@ getCookie: async () => {
359
496
  const setCookie = context.response.headers.get("set-cookie");
360
497
  if (setCookie) {
361
498
  if (hasBetterAuthCookies(setCookie, cookiePrefix)) {
362
- const prevCookie = await storage.getItemAsync(cookieName);
363
- const toSetCookie = getSetCookie(setCookie || "", prevCookie ?? void 0);
364
- if (hasSessionCookieChanged(prevCookie, toSetCookie)) {
365
- await storage.setItemAsync(cookieName, toSetCookie);
366
- store?.notify("$sessionSignal");
367
- } else await storage.setItemAsync(cookieName, toSetCookie);
499
+ const update = await storage.updateItemAsync(cookieName, (currentValue) => getSetCookie(setCookie, currentValue ?? void 0));
500
+ if (update && hasSessionCookieChanged(update.previousValue, update.value)) store?.notify("$sessionSignal");
368
501
  }
369
502
  }
370
503
  if (pathname.endsWith("/get-session") && !opts?.disableCache) {
@@ -398,9 +531,7 @@ getCookie: async () => {
398
531
  if (result.type !== "success") return;
399
532
  const cookie = new URL(result.url).searchParams.get("cookie");
400
533
  if (!cookie) return;
401
- const toSetCookie = getSetCookie(cookie, await storage.getItemAsync(cookieName) ?? void 0);
402
- await storage.setItemAsync(cookieName, toSetCookie);
403
- store?.notify("$sessionSignal");
534
+ if (await storage.updateItemAsync(cookieName, (currentValue) => getSetCookie(cookie, currentValue ?? void 0))) store?.notify("$sessionSignal");
404
535
  }
405
536
  } },
406
537
  async init(url, options) {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { t as PACKAGE_VERSION } from "./version-D-tGaVc4.js";
1
+ import { t as PACKAGE_VERSION } from "./version-DddGjO8z.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-D-tGaVc4.js";
1
+ import { t as PACKAGE_VERSION } from "../version-DddGjO8z.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.2";
3
+ const PACKAGE_VERSION = "1.7.3";
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.2",
3
+ "version": "1.7.3",
4
4
  "description": "Better Auth integration for Expo and React Native applications.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -60,7 +60,7 @@
60
60
  "dependencies": {
61
61
  "@better-fetch/fetch": "1.3.1",
62
62
  "better-call": "1.4.0",
63
- "zod": "^4.3.6"
63
+ "zod": "^4.5.4"
64
64
  },
65
65
  "devDependencies": {
66
66
  "@better-fetch/fetch": "1.3.1",
@@ -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.2",
75
- "better-auth": "1.7.2"
74
+ "@better-auth/core": "1.7.3",
75
+ "better-auth": "1.7.3"
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.2",
84
- "better-auth": "^1.7.2"
83
+ "@better-auth/core": "^1.7.3",
84
+ "better-auth": "^1.7.3"
85
85
  },
86
86
  "peerDependenciesMeta": {
87
87
  "expo-constants": {