react-native-nitro-storage 0.3.0 → 0.3.2

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.
Files changed (53) hide show
  1. package/README.md +594 -247
  2. package/android/CMakeLists.txt +2 -0
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +102 -11
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +16 -0
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +154 -34
  6. package/android/src/main/java/com/nitrostorage/NitroStoragePackage.kt +2 -2
  7. package/cpp/bindings/HybridStorage.cpp +176 -21
  8. package/cpp/bindings/HybridStorage.hpp +29 -2
  9. package/cpp/core/NativeStorageAdapter.hpp +16 -0
  10. package/ios/IOSStorageAdapterCpp.hpp +20 -0
  11. package/ios/IOSStorageAdapterCpp.mm +239 -32
  12. package/lib/commonjs/Storage.types.js +23 -1
  13. package/lib/commonjs/Storage.types.js.map +1 -1
  14. package/lib/commonjs/index.js +292 -75
  15. package/lib/commonjs/index.js.map +1 -1
  16. package/lib/commonjs/index.web.js +473 -86
  17. package/lib/commonjs/index.web.js.map +1 -1
  18. package/lib/commonjs/internal.js +10 -0
  19. package/lib/commonjs/internal.js.map +1 -1
  20. package/lib/commonjs/storage-hooks.js +36 -0
  21. package/lib/commonjs/storage-hooks.js.map +1 -0
  22. package/lib/module/Storage.types.js +22 -0
  23. package/lib/module/Storage.types.js.map +1 -1
  24. package/lib/module/index.js +264 -75
  25. package/lib/module/index.js.map +1 -1
  26. package/lib/module/index.web.js +445 -86
  27. package/lib/module/index.web.js.map +1 -1
  28. package/lib/module/internal.js +8 -0
  29. package/lib/module/internal.js.map +1 -1
  30. package/lib/module/storage-hooks.js +30 -0
  31. package/lib/module/storage-hooks.js.map +1 -0
  32. package/lib/typescript/Storage.nitro.d.ts +12 -0
  33. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  34. package/lib/typescript/Storage.types.d.ts +20 -0
  35. package/lib/typescript/Storage.types.d.ts.map +1 -1
  36. package/lib/typescript/index.d.ts +33 -10
  37. package/lib/typescript/index.d.ts.map +1 -1
  38. package/lib/typescript/index.web.d.ts +45 -10
  39. package/lib/typescript/index.web.d.ts.map +1 -1
  40. package/lib/typescript/internal.d.ts +2 -0
  41. package/lib/typescript/internal.d.ts.map +1 -1
  42. package/lib/typescript/storage-hooks.d.ts +10 -0
  43. package/lib/typescript/storage-hooks.d.ts.map +1 -0
  44. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +12 -0
  45. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +12 -0
  46. package/package.json +8 -3
  47. package/src/Storage.nitro.ts +13 -2
  48. package/src/Storage.types.ts +22 -0
  49. package/src/index.ts +382 -123
  50. package/src/index.web.ts +618 -134
  51. package/src/internal.ts +14 -4
  52. package/src/migration.ts +1 -1
  53. package/src/storage-hooks.ts +48 -0
@@ -1,10 +1,18 @@
1
1
  "use strict";
2
2
 
3
- import { useRef, useSyncExternalStore } from "react";
4
- import { StorageScope } from "./Storage.types";
5
- import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath } from "./internal";
6
- export { StorageScope } from "./Storage.types";
3
+ import { StorageScope, AccessControl } from "./Storage.types";
4
+ import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, prefixKey, isNamespaced } from "./internal";
5
+ export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
7
6
  export { migrateFromMMKV } from "./migration";
7
+ function asInternal(item) {
8
+ return item;
9
+ }
10
+ function isUpdater(valueOrFn) {
11
+ return typeof valueOrFn === "function";
12
+ }
13
+ function typedKeys(record) {
14
+ return Object.keys(record);
15
+ }
8
16
  const registeredMigrations = new Map();
9
17
  const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
10
18
  Promise.resolve().then(task);
@@ -13,17 +21,71 @@ const memoryStore = new Map();
13
21
  const memoryListeners = new Map();
14
22
  const webScopeListeners = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
15
23
  const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
24
+ const webScopeKeyIndex = new Map([[StorageScope.Disk, new Set()], [StorageScope.Secure, new Set()]]);
25
+ const hydratedWebScopeKeyIndex = new Set();
16
26
  const pendingSecureWrites = new Map();
17
27
  let secureFlushScheduled = false;
28
+ const SECURE_WEB_PREFIX = "__secure_";
29
+ const BIOMETRIC_WEB_PREFIX = "__bio_";
30
+ let hasWarnedAboutWebBiometricFallback = false;
18
31
  function getBrowserStorage(scope) {
19
32
  if (scope === StorageScope.Disk) {
20
33
  return globalThis.localStorage;
21
34
  }
22
35
  if (scope === StorageScope.Secure) {
23
- return globalThis.sessionStorage;
36
+ return globalThis.localStorage;
24
37
  }
25
38
  return undefined;
26
39
  }
40
+ function toSecureStorageKey(key) {
41
+ return `${SECURE_WEB_PREFIX}${key}`;
42
+ }
43
+ function fromSecureStorageKey(key) {
44
+ return key.slice(SECURE_WEB_PREFIX.length);
45
+ }
46
+ function toBiometricStorageKey(key) {
47
+ return `${BIOMETRIC_WEB_PREFIX}${key}`;
48
+ }
49
+ function fromBiometricStorageKey(key) {
50
+ return key.slice(BIOMETRIC_WEB_PREFIX.length);
51
+ }
52
+ function getWebScopeKeyIndex(scope) {
53
+ return webScopeKeyIndex.get(scope);
54
+ }
55
+ function hydrateWebScopeKeyIndex(scope) {
56
+ if (hydratedWebScopeKeyIndex.has(scope)) {
57
+ return;
58
+ }
59
+ const storage = getBrowserStorage(scope);
60
+ const keyIndex = getWebScopeKeyIndex(scope);
61
+ keyIndex.clear();
62
+ if (storage) {
63
+ for (let index = 0; index < storage.length; index += 1) {
64
+ const key = storage.key(index);
65
+ if (!key) {
66
+ continue;
67
+ }
68
+ if (scope === StorageScope.Disk) {
69
+ if (!key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)) {
70
+ keyIndex.add(key);
71
+ }
72
+ continue;
73
+ }
74
+ if (key.startsWith(SECURE_WEB_PREFIX)) {
75
+ keyIndex.add(fromSecureStorageKey(key));
76
+ continue;
77
+ }
78
+ if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
79
+ keyIndex.add(fromBiometricStorageKey(key));
80
+ }
81
+ }
82
+ }
83
+ hydratedWebScopeKeyIndex.add(scope);
84
+ }
85
+ function ensureWebScopeKeyIndex(scope) {
86
+ hydrateWebScopeKeyIndex(scope);
87
+ return getWebScopeKeyIndex(scope);
88
+ }
27
89
  function getScopedListeners(scope) {
28
90
  return webScopeListeners.get(scope);
29
91
  }
@@ -122,26 +184,65 @@ const WebStorage = {
122
184
  dispose: () => {},
123
185
  set: (key, value, scope) => {
124
186
  const storage = getBrowserStorage(scope);
125
- storage?.setItem(key, value);
187
+ if (!storage) {
188
+ return;
189
+ }
190
+ const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
191
+ storage.setItem(storageKey, value);
126
192
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
193
+ ensureWebScopeKeyIndex(scope).add(key);
127
194
  notifyKeyListeners(getScopedListeners(scope), key);
128
195
  }
129
196
  },
130
197
  get: (key, scope) => {
131
198
  const storage = getBrowserStorage(scope);
132
- return storage?.getItem(key) ?? undefined;
199
+ const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
200
+ return storage?.getItem(storageKey) ?? undefined;
133
201
  },
134
202
  remove: (key, scope) => {
135
203
  const storage = getBrowserStorage(scope);
136
- storage?.removeItem(key);
204
+ if (!storage) {
205
+ return;
206
+ }
207
+ if (scope === StorageScope.Secure) {
208
+ storage.removeItem(toSecureStorageKey(key));
209
+ storage.removeItem(toBiometricStorageKey(key));
210
+ } else {
211
+ storage.removeItem(key);
212
+ }
137
213
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
214
+ ensureWebScopeKeyIndex(scope).delete(key);
138
215
  notifyKeyListeners(getScopedListeners(scope), key);
139
216
  }
140
217
  },
141
218
  clear: scope => {
142
219
  const storage = getBrowserStorage(scope);
143
- storage?.clear();
220
+ if (!storage) {
221
+ return;
222
+ }
223
+ if (scope === StorageScope.Secure) {
224
+ const keysToRemove = [];
225
+ for (let i = 0; i < storage.length; i++) {
226
+ const key = storage.key(i);
227
+ if (key?.startsWith(SECURE_WEB_PREFIX) || key?.startsWith(BIOMETRIC_WEB_PREFIX)) {
228
+ keysToRemove.push(key);
229
+ }
230
+ }
231
+ keysToRemove.forEach(key => storage.removeItem(key));
232
+ } else if (scope === StorageScope.Disk) {
233
+ const keysToRemove = [];
234
+ for (let i = 0; i < storage.length; i++) {
235
+ const key = storage.key(i);
236
+ if (key && !key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)) {
237
+ keysToRemove.push(key);
238
+ }
239
+ }
240
+ keysToRemove.forEach(key => storage.removeItem(key));
241
+ } else {
242
+ storage.clear();
243
+ }
144
244
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
245
+ ensureWebScopeKeyIndex(scope).clear();
145
246
  notifyAllListeners(getScopedListeners(scope));
146
247
  }
147
248
  },
@@ -151,32 +252,129 @@ const WebStorage = {
151
252
  return;
152
253
  }
153
254
  keys.forEach((key, index) => {
154
- storage.setItem(key, values[index]);
255
+ const value = values[index];
256
+ if (value === undefined) {
257
+ return;
258
+ }
259
+ const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
260
+ storage.setItem(storageKey, value);
155
261
  });
156
262
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
263
+ const keyIndex = ensureWebScopeKeyIndex(scope);
264
+ keys.forEach(key => keyIndex.add(key));
157
265
  const listeners = getScopedListeners(scope);
158
266
  keys.forEach(key => notifyKeyListeners(listeners, key));
159
267
  }
160
268
  },
161
269
  getBatch: (keys, scope) => {
162
270
  const storage = getBrowserStorage(scope);
163
- return keys.map(key => storage?.getItem(key) ?? undefined);
271
+ return keys.map(key => {
272
+ const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
273
+ return storage?.getItem(storageKey) ?? undefined;
274
+ });
164
275
  },
165
276
  removeBatch: (keys, scope) => {
166
277
  const storage = getBrowserStorage(scope);
167
278
  if (!storage) {
168
279
  return;
169
280
  }
170
- keys.forEach(key => {
171
- storage.removeItem(key);
172
- });
281
+ if (scope === StorageScope.Secure) {
282
+ keys.forEach(key => {
283
+ storage.removeItem(toSecureStorageKey(key));
284
+ storage.removeItem(toBiometricStorageKey(key));
285
+ });
286
+ } else {
287
+ keys.forEach(key => {
288
+ storage.removeItem(key);
289
+ });
290
+ }
173
291
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
292
+ const keyIndex = ensureWebScopeKeyIndex(scope);
293
+ keys.forEach(key => keyIndex.delete(key));
174
294
  const listeners = getScopedListeners(scope);
175
295
  keys.forEach(key => notifyKeyListeners(listeners, key));
176
296
  }
177
297
  },
298
+ removeByPrefix: (prefix, scope) => {
299
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
300
+ return;
301
+ }
302
+ const keyIndex = ensureWebScopeKeyIndex(scope);
303
+ const keys = Array.from(keyIndex).filter(key => key.startsWith(prefix));
304
+ if (keys.length === 0) {
305
+ return;
306
+ }
307
+ WebStorage.removeBatch(keys, scope);
308
+ },
178
309
  addOnChange: (_scope, _callback) => {
179
310
  return () => {};
311
+ },
312
+ has: (key, scope) => {
313
+ const storage = getBrowserStorage(scope);
314
+ if (scope === StorageScope.Secure) {
315
+ return storage?.getItem(toSecureStorageKey(key)) !== null || storage?.getItem(toBiometricStorageKey(key)) !== null;
316
+ }
317
+ return storage?.getItem(key) !== null;
318
+ },
319
+ getAllKeys: scope => {
320
+ if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
321
+ return [];
322
+ }
323
+ return Array.from(ensureWebScopeKeyIndex(scope));
324
+ },
325
+ size: scope => {
326
+ if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
327
+ return ensureWebScopeKeyIndex(scope).size;
328
+ }
329
+ return 0;
330
+ },
331
+ setSecureAccessControl: () => {},
332
+ setSecureWritesAsync: _enabled => {},
333
+ setKeychainAccessGroup: () => {},
334
+ setSecureBiometric: (key, value) => {
335
+ if (typeof __DEV__ !== "undefined" && __DEV__ && !hasWarnedAboutWebBiometricFallback) {
336
+ hasWarnedAboutWebBiometricFallback = true;
337
+ console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
338
+ }
339
+ globalThis.localStorage?.setItem(toBiometricStorageKey(key), value);
340
+ ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
341
+ notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
342
+ },
343
+ getSecureBiometric: key => {
344
+ return globalThis.localStorage?.getItem(toBiometricStorageKey(key)) ?? undefined;
345
+ },
346
+ deleteSecureBiometric: key => {
347
+ const storage = globalThis.localStorage;
348
+ storage?.removeItem(toBiometricStorageKey(key));
349
+ if (storage?.getItem(toSecureStorageKey(key)) === null) {
350
+ ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
351
+ }
352
+ notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
353
+ },
354
+ hasSecureBiometric: key => {
355
+ return globalThis.localStorage?.getItem(toBiometricStorageKey(key)) !== null;
356
+ },
357
+ clearSecureBiometric: () => {
358
+ const storage = globalThis.localStorage;
359
+ if (!storage) return;
360
+ const keysToNotify = [];
361
+ const toRemove = [];
362
+ for (let i = 0; i < storage.length; i++) {
363
+ const k = storage.key(i);
364
+ if (k?.startsWith(BIOMETRIC_WEB_PREFIX)) {
365
+ toRemove.push(k);
366
+ keysToNotify.push(fromBiometricStorageKey(k));
367
+ }
368
+ }
369
+ toRemove.forEach(k => storage.removeItem(k));
370
+ const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
371
+ keysToNotify.forEach(key => {
372
+ if (storage.getItem(toSecureStorageKey(key)) === null) {
373
+ keyIndex.delete(key);
374
+ }
375
+ });
376
+ const listeners = getScopedListeners(StorageScope.Secure);
377
+ keysToNotify.forEach(key => notifyKeyListeners(listeners, key));
180
378
  }
181
379
  };
182
380
  function getRawValue(key, scope) {
@@ -247,10 +445,71 @@ export const storage = {
247
445
  storage.clear(StorageScope.Memory);
248
446
  storage.clear(StorageScope.Disk);
249
447
  storage.clear(StorageScope.Secure);
250
- }
448
+ },
449
+ clearNamespace: (namespace, scope) => {
450
+ assertValidScope(scope);
451
+ if (scope === StorageScope.Memory) {
452
+ for (const key of memoryStore.keys()) {
453
+ if (isNamespaced(key, namespace)) {
454
+ memoryStore.delete(key);
455
+ }
456
+ }
457
+ notifyAllListeners(memoryListeners);
458
+ return;
459
+ }
460
+ const keyPrefix = prefixKey(namespace, "");
461
+ if (scope === StorageScope.Secure) {
462
+ flushSecureWrites();
463
+ }
464
+ clearScopeRawCache(scope);
465
+ WebStorage.removeByPrefix(keyPrefix, scope);
466
+ },
467
+ clearBiometric: () => {
468
+ WebStorage.clearSecureBiometric();
469
+ },
470
+ has: (key, scope) => {
471
+ assertValidScope(scope);
472
+ if (scope === StorageScope.Memory) return memoryStore.has(key);
473
+ return WebStorage.has(key, scope);
474
+ },
475
+ getAllKeys: scope => {
476
+ assertValidScope(scope);
477
+ if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
478
+ return WebStorage.getAllKeys(scope);
479
+ },
480
+ getAll: scope => {
481
+ assertValidScope(scope);
482
+ const result = {};
483
+ if (scope === StorageScope.Memory) {
484
+ memoryStore.forEach((value, key) => {
485
+ if (typeof value === "string") result[key] = value;
486
+ });
487
+ return result;
488
+ }
489
+ const keys = WebStorage.getAllKeys(scope);
490
+ keys.forEach(key => {
491
+ const val = WebStorage.get(key, scope);
492
+ if (val !== undefined) result[key] = val;
493
+ });
494
+ return result;
495
+ },
496
+ size: scope => {
497
+ assertValidScope(scope);
498
+ if (scope === StorageScope.Memory) return memoryStore.size;
499
+ return WebStorage.size(scope);
500
+ },
501
+ setAccessControl: _level => {},
502
+ setSecureWritesAsync: _enabled => {},
503
+ flushSecureWrites: () => {
504
+ flushSecureWrites();
505
+ },
506
+ setKeychainAccessGroup: _group => {}
251
507
  };
252
508
  function canUseRawBatchPath(item) {
253
- return item._hasExpiration === false && item._hasValidation === false;
509
+ return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
510
+ }
511
+ function canUseSecureRawBatchPath(item) {
512
+ return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true;
254
513
  }
255
514
  function defaultSerialize(value) {
256
515
  return serializeWithPrimitiveFastPath(value);
@@ -259,16 +518,21 @@ function defaultDeserialize(value) {
259
518
  return deserializeWithPrimitiveFastPath(value);
260
519
  }
261
520
  export function createStorageItem(config) {
521
+ const storageKey = prefixKey(config.namespace, config.key);
262
522
  const serialize = config.serialize ?? defaultSerialize;
263
523
  const deserialize = config.deserialize ?? defaultDeserialize;
264
524
  const isMemory = config.scope === StorageScope.Memory;
525
+ const isBiometric = config.biometric === true && config.scope === StorageScope.Secure;
526
+ const secureAccessControl = config.accessControl;
265
527
  const validate = config.validate;
266
528
  const onValidationError = config.onValidationError;
267
529
  const expiration = config.expiration;
530
+ const onExpired = config.onExpired;
268
531
  const expirationTtlMs = expiration?.ttlMs;
269
532
  const memoryExpiration = expiration && isMemory ? new Map() : null;
270
533
  const readCache = !isMemory && config.readCache === true;
271
- const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true;
534
+ const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric && secureAccessControl === undefined;
535
+ const defaultValue = config.defaultValue;
272
536
  const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
273
537
  if (expiration && expiration.ttlMs <= 0) {
274
538
  throw new Error("expiration.ttlMs must be greater than 0.");
@@ -278,10 +542,12 @@ export function createStorageItem(config) {
278
542
  let lastRaw = undefined;
279
543
  let lastValue;
280
544
  let hasLastValue = false;
545
+ let lastExpiresAt = undefined;
281
546
  const invalidateParsedCache = () => {
282
547
  lastRaw = undefined;
283
548
  lastValue = undefined;
284
549
  hasLastValue = false;
550
+ lastExpiresAt = undefined;
285
551
  };
286
552
  const ensureSubscription = () => {
287
553
  if (unsubscribe) {
@@ -292,65 +558,77 @@ export function createStorageItem(config) {
292
558
  listeners.forEach(callback => callback());
293
559
  };
294
560
  if (isMemory) {
295
- unsubscribe = addKeyListener(memoryListeners, config.key, listener);
561
+ unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
296
562
  return;
297
563
  }
298
- unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), config.key, listener);
564
+ unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
299
565
  };
300
566
  const readStoredRaw = () => {
301
567
  if (isMemory) {
302
568
  if (memoryExpiration) {
303
- const expiresAt = memoryExpiration.get(config.key);
569
+ const expiresAt = memoryExpiration.get(storageKey);
304
570
  if (expiresAt !== undefined && expiresAt <= Date.now()) {
305
- memoryExpiration.delete(config.key);
306
- memoryStore.delete(config.key);
307
- notifyKeyListeners(memoryListeners, config.key);
571
+ memoryExpiration.delete(storageKey);
572
+ memoryStore.delete(storageKey);
573
+ notifyKeyListeners(memoryListeners, storageKey);
574
+ onExpired?.(storageKey);
308
575
  return undefined;
309
576
  }
310
577
  }
311
- return memoryStore.get(config.key);
578
+ return memoryStore.get(storageKey);
312
579
  }
313
- if (nonMemoryScope === StorageScope.Secure && hasPendingSecureWrite(config.key)) {
314
- return readPendingSecureWrite(config.key);
580
+ if (nonMemoryScope === StorageScope.Secure && !isBiometric && hasPendingSecureWrite(storageKey)) {
581
+ return readPendingSecureWrite(storageKey);
315
582
  }
316
583
  if (readCache) {
317
- if (hasCachedRawValue(nonMemoryScope, config.key)) {
318
- return readCachedRawValue(nonMemoryScope, config.key);
584
+ if (hasCachedRawValue(nonMemoryScope, storageKey)) {
585
+ return readCachedRawValue(nonMemoryScope, storageKey);
319
586
  }
320
587
  }
321
- const raw = WebStorage.get(config.key, config.scope);
322
- cacheRawValue(nonMemoryScope, config.key, raw);
588
+ if (isBiometric) {
589
+ return WebStorage.getSecureBiometric(storageKey);
590
+ }
591
+ const raw = WebStorage.get(storageKey, config.scope);
592
+ cacheRawValue(nonMemoryScope, storageKey, raw);
323
593
  return raw;
324
594
  };
325
595
  const writeStoredRaw = rawValue => {
326
- cacheRawValue(nonMemoryScope, config.key, rawValue);
596
+ if (isBiometric) {
597
+ WebStorage.setSecureBiometric(storageKey, rawValue);
598
+ return;
599
+ }
600
+ cacheRawValue(nonMemoryScope, storageKey, rawValue);
327
601
  if (coalesceSecureWrites) {
328
- scheduleSecureWrite(config.key, rawValue);
602
+ scheduleSecureWrite(storageKey, rawValue);
329
603
  return;
330
604
  }
331
605
  if (nonMemoryScope === StorageScope.Secure) {
332
- clearPendingSecureWrite(config.key);
606
+ clearPendingSecureWrite(storageKey);
333
607
  }
334
- WebStorage.set(config.key, rawValue, config.scope);
608
+ WebStorage.set(storageKey, rawValue, config.scope);
335
609
  };
336
610
  const removeStoredRaw = () => {
337
- cacheRawValue(nonMemoryScope, config.key, undefined);
611
+ if (isBiometric) {
612
+ WebStorage.deleteSecureBiometric(storageKey);
613
+ return;
614
+ }
615
+ cacheRawValue(nonMemoryScope, storageKey, undefined);
338
616
  if (coalesceSecureWrites) {
339
- scheduleSecureWrite(config.key, undefined);
617
+ scheduleSecureWrite(storageKey, undefined);
340
618
  return;
341
619
  }
342
620
  if (nonMemoryScope === StorageScope.Secure) {
343
- clearPendingSecureWrite(config.key);
621
+ clearPendingSecureWrite(storageKey);
344
622
  }
345
- WebStorage.remove(config.key, config.scope);
623
+ WebStorage.remove(storageKey, config.scope);
346
624
  };
347
625
  const writeValueWithoutValidation = value => {
348
626
  if (isMemory) {
349
627
  if (memoryExpiration) {
350
- memoryExpiration.set(config.key, Date.now() + (expirationTtlMs ?? 0));
628
+ memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
351
629
  }
352
- memoryStore.set(config.key, value);
353
- notifyKeyListeners(memoryListeners, config.key);
630
+ memoryStore.set(storageKey, value);
631
+ notifyKeyListeners(memoryListeners, storageKey);
354
632
  return;
355
633
  }
356
634
  const serialized = serialize(value);
@@ -369,7 +647,7 @@ export function createStorageItem(config) {
369
647
  if (onValidationError) {
370
648
  return onValidationError(invalidValue);
371
649
  }
372
- return config.defaultValue;
650
+ return defaultValue;
373
651
  };
374
652
  const ensureValidatedValue = (candidate, hadStoredValue) => {
375
653
  if (!validate || validate(candidate)) {
@@ -377,7 +655,7 @@ export function createStorageItem(config) {
377
655
  }
378
656
  const resolved = resolveInvalidValue(candidate);
379
657
  if (validate && !validate(resolved)) {
380
- return config.defaultValue;
658
+ return defaultValue;
381
659
  }
382
660
  if (hadStoredValue) {
383
661
  writeValueWithoutValidation(resolved);
@@ -386,30 +664,53 @@ export function createStorageItem(config) {
386
664
  };
387
665
  const get = () => {
388
666
  const raw = readStoredRaw();
389
- const canUseCachedValue = !expiration && !memoryExpiration;
390
- if (canUseCachedValue && raw === lastRaw && hasLastValue) {
391
- return lastValue;
667
+ if (!memoryExpiration && raw === lastRaw && hasLastValue) {
668
+ if (!expiration || lastExpiresAt === null) {
669
+ return lastValue;
670
+ }
671
+ if (typeof lastExpiresAt === "number") {
672
+ if (lastExpiresAt > Date.now()) {
673
+ return lastValue;
674
+ }
675
+ removeStoredRaw();
676
+ invalidateParsedCache();
677
+ onExpired?.(storageKey);
678
+ lastValue = ensureValidatedValue(defaultValue, false);
679
+ hasLastValue = true;
680
+ return lastValue;
681
+ }
392
682
  }
393
683
  lastRaw = raw;
394
684
  if (raw === undefined) {
395
- lastValue = ensureValidatedValue(config.defaultValue, false);
685
+ lastExpiresAt = undefined;
686
+ lastValue = ensureValidatedValue(defaultValue, false);
396
687
  hasLastValue = true;
397
688
  return lastValue;
398
689
  }
399
690
  if (isMemory) {
691
+ lastExpiresAt = undefined;
400
692
  lastValue = ensureValidatedValue(raw, true);
401
693
  hasLastValue = true;
402
694
  return lastValue;
403
695
  }
696
+ if (typeof raw !== "string") {
697
+ lastExpiresAt = undefined;
698
+ lastValue = ensureValidatedValue(defaultValue, false);
699
+ hasLastValue = true;
700
+ return lastValue;
701
+ }
404
702
  let deserializableRaw = raw;
405
703
  if (expiration) {
704
+ let envelopeExpiresAt = null;
406
705
  try {
407
706
  const parsed = JSON.parse(raw);
408
707
  if (isStoredEnvelope(parsed)) {
708
+ envelopeExpiresAt = parsed.expiresAt;
409
709
  if (parsed.expiresAt <= Date.now()) {
410
710
  removeStoredRaw();
411
711
  invalidateParsedCache();
412
- lastValue = ensureValidatedValue(config.defaultValue, false);
712
+ onExpired?.(storageKey);
713
+ lastValue = ensureValidatedValue(defaultValue, false);
413
714
  hasLastValue = true;
414
715
  return lastValue;
415
716
  }
@@ -418,17 +719,19 @@ export function createStorageItem(config) {
418
719
  } catch {
419
720
  // Keep backward compatibility with legacy raw values.
420
721
  }
722
+ lastExpiresAt = envelopeExpiresAt;
723
+ } else {
724
+ lastExpiresAt = undefined;
421
725
  }
422
726
  lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
423
727
  hasLastValue = true;
424
728
  return lastValue;
425
729
  };
426
730
  const set = valueOrFn => {
427
- const currentValue = get();
428
- const newValue = typeof valueOrFn === "function" ? valueOrFn(currentValue) : valueOrFn;
731
+ const newValue = isUpdater(valueOrFn) ? valueOrFn(get()) : valueOrFn;
429
732
  invalidateParsedCache();
430
733
  if (validate && !validate(newValue)) {
431
- throw new Error(`Validation failed for key "${config.key}" in scope "${StorageScope[config.scope]}".`);
734
+ throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
432
735
  }
433
736
  writeValueWithoutValidation(newValue);
434
737
  };
@@ -436,14 +739,19 @@ export function createStorageItem(config) {
436
739
  invalidateParsedCache();
437
740
  if (isMemory) {
438
741
  if (memoryExpiration) {
439
- memoryExpiration.delete(config.key);
742
+ memoryExpiration.delete(storageKey);
440
743
  }
441
- memoryStore.delete(config.key);
442
- notifyKeyListeners(memoryListeners, config.key);
744
+ memoryStore.delete(storageKey);
745
+ notifyKeyListeners(memoryListeners, storageKey);
443
746
  return;
444
747
  }
445
748
  removeStoredRaw();
446
749
  };
750
+ const hasItem = () => {
751
+ if (isMemory) return memoryStore.has(storageKey);
752
+ if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
753
+ return WebStorage.has(storageKey, config.scope);
754
+ };
447
755
  const subscribe = callback => {
448
756
  ensureSubscription();
449
757
  listeners.add(callback);
@@ -459,6 +767,7 @@ export function createStorageItem(config) {
459
767
  get,
460
768
  set,
461
769
  delete: deleteItem,
770
+ has: hasItem,
462
771
  subscribe,
463
772
  serialize,
464
773
  deserialize,
@@ -469,43 +778,22 @@ export function createStorageItem(config) {
469
778
  _hasValidation: validate !== undefined,
470
779
  _hasExpiration: expiration !== undefined,
471
780
  _readCacheEnabled: readCache,
781
+ _isBiometric: isBiometric,
782
+ ...(secureAccessControl !== undefined ? {
783
+ _secureAccessControl: secureAccessControl
784
+ } : {}),
472
785
  scope: config.scope,
473
- key: config.key
786
+ key: storageKey
474
787
  };
475
788
  return storageItem;
476
789
  }
477
- export function useStorage(item) {
478
- const value = useSyncExternalStore(item.subscribe, item.get, item.get);
479
- return [value, item.set];
480
- }
481
- export function useStorageSelector(item, selector, isEqual = Object.is) {
482
- const selectedRef = useRef({
483
- hasValue: false
484
- });
485
- const getSelectedSnapshot = () => {
486
- const nextSelected = selector(item.get());
487
- const current = selectedRef.current;
488
- if (current.hasValue && isEqual(current.value, nextSelected)) {
489
- return current.value;
490
- }
491
- selectedRef.current = {
492
- hasValue: true,
493
- value: nextSelected
494
- };
495
- return nextSelected;
496
- };
497
- const selectedValue = useSyncExternalStore(item.subscribe, getSelectedSnapshot, getSelectedSnapshot);
498
- return [selectedValue, item.set];
499
- }
500
- export function useSetStorage(item) {
501
- return item.set;
502
- }
790
+ export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
503
791
  export function getBatch(items, scope) {
504
792
  assertBatchScope(items, scope);
505
793
  if (scope === StorageScope.Memory) {
506
794
  return items.map(item => item.get());
507
795
  }
508
- const useRawBatchPath = items.every(item => canUseRawBatchPath(item));
796
+ const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
509
797
  if (!useRawBatchPath) {
510
798
  return items.map(item => item.get());
511
799
  }
@@ -534,6 +822,9 @@ export function getBatch(items, scope) {
534
822
  fetchedValues.forEach((value, index) => {
535
823
  const key = keysToFetch[index];
536
824
  const targetIndex = keyIndexes[index];
825
+ if (key === undefined || targetIndex === undefined) {
826
+ return;
827
+ }
537
828
  rawValues[targetIndex] = value;
538
829
  cacheRawValue(scope, key, value);
539
830
  });
@@ -555,9 +846,54 @@ export function setBatch(items, scope) {
555
846
  }) => item.set(value));
556
847
  return;
557
848
  }
849
+ if (scope === StorageScope.Secure) {
850
+ const secureEntries = items.map(({
851
+ item,
852
+ value
853
+ }) => ({
854
+ item,
855
+ value,
856
+ internal: asInternal(item)
857
+ }));
858
+ const canUseSecureBatchPath = secureEntries.every(({
859
+ internal
860
+ }) => canUseSecureRawBatchPath(internal));
861
+ if (!canUseSecureBatchPath) {
862
+ items.forEach(({
863
+ item,
864
+ value
865
+ }) => item.set(value));
866
+ return;
867
+ }
868
+ flushSecureWrites();
869
+ const groupedByAccessControl = new Map();
870
+ secureEntries.forEach(({
871
+ item,
872
+ value,
873
+ internal
874
+ }) => {
875
+ const accessControl = internal._secureAccessControl ?? AccessControl.WhenUnlocked;
876
+ const existingGroup = groupedByAccessControl.get(accessControl);
877
+ const group = existingGroup ?? {
878
+ keys: [],
879
+ values: []
880
+ };
881
+ group.keys.push(item.key);
882
+ group.values.push(item.serialize(value));
883
+ if (!existingGroup) {
884
+ groupedByAccessControl.set(accessControl, group);
885
+ }
886
+ });
887
+ groupedByAccessControl.forEach((group, accessControl) => {
888
+ WebStorage.setSecureAccessControl(accessControl);
889
+ WebStorage.setBatch(group.keys, group.values, scope);
890
+ group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
891
+ });
892
+ return;
893
+ }
558
894
  const useRawBatchPath = items.every(({
559
895
  item
560
- }) => canUseRawBatchPath(item));
896
+ }) => canUseRawBatchPath(asInternal(item)));
561
897
  if (!useRawBatchPath) {
562
898
  items.forEach(({
563
899
  item,
@@ -567,9 +903,6 @@ export function setBatch(items, scope) {
567
903
  }
568
904
  const keys = items.map(entry => entry.item.key);
569
905
  const values = items.map(entry => entry.item.serialize(entry.value));
570
- if (scope === StorageScope.Secure) {
571
- flushSecureWrites();
572
- }
573
906
  WebStorage.setBatch(keys, values, scope);
574
907
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
575
908
  }
@@ -668,4 +1001,30 @@ export function runTransaction(scope, transaction) {
668
1001
  throw error;
669
1002
  }
670
1003
  }
1004
+ export function createSecureAuthStorage(config, options) {
1005
+ const ns = options?.namespace ?? "auth";
1006
+ const result = {};
1007
+ for (const key of typedKeys(config)) {
1008
+ const itemConfig = config[key];
1009
+ const expirationConfig = itemConfig.ttlMs !== undefined ? {
1010
+ ttlMs: itemConfig.ttlMs
1011
+ } : undefined;
1012
+ result[key] = createStorageItem({
1013
+ key,
1014
+ scope: StorageScope.Secure,
1015
+ defaultValue: "",
1016
+ namespace: ns,
1017
+ ...(itemConfig.biometric !== undefined ? {
1018
+ biometric: itemConfig.biometric
1019
+ } : {}),
1020
+ ...(itemConfig.accessControl !== undefined ? {
1021
+ accessControl: itemConfig.accessControl
1022
+ } : {}),
1023
+ ...(expirationConfig !== undefined ? {
1024
+ expiration: expirationConfig
1025
+ } : {})
1026
+ });
1027
+ }
1028
+ return result;
1029
+ }
671
1030
  //# sourceMappingURL=index.web.js.map