react-native-nitro-storage 0.3.0 → 0.3.1

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 (45) hide show
  1. package/README.md +414 -256
  2. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +98 -11
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +15 -0
  4. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +130 -33
  5. package/android/src/main/java/com/nitrostorage/NitroStoragePackage.kt +2 -2
  6. package/cpp/bindings/HybridStorage.cpp +121 -12
  7. package/cpp/bindings/HybridStorage.hpp +10 -0
  8. package/cpp/core/NativeStorageAdapter.hpp +15 -0
  9. package/ios/IOSStorageAdapterCpp.hpp +19 -0
  10. package/ios/IOSStorageAdapterCpp.mm +233 -32
  11. package/lib/commonjs/Storage.types.js +23 -1
  12. package/lib/commonjs/Storage.types.js.map +1 -1
  13. package/lib/commonjs/index.js +173 -32
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/index.web.js +289 -49
  16. package/lib/commonjs/index.web.js.map +1 -1
  17. package/lib/commonjs/internal.js +10 -0
  18. package/lib/commonjs/internal.js.map +1 -1
  19. package/lib/module/Storage.types.js +22 -0
  20. package/lib/module/Storage.types.js.map +1 -1
  21. package/lib/module/index.js +163 -35
  22. package/lib/module/index.js.map +1 -1
  23. package/lib/module/index.web.js +278 -51
  24. package/lib/module/index.web.js.map +1 -1
  25. package/lib/module/internal.js +8 -0
  26. package/lib/module/internal.js.map +1 -1
  27. package/lib/typescript/Storage.nitro.d.ts +10 -0
  28. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  29. package/lib/typescript/Storage.types.d.ts +20 -0
  30. package/lib/typescript/Storage.types.d.ts.map +1 -1
  31. package/lib/typescript/index.d.ts +30 -7
  32. package/lib/typescript/index.d.ts.map +1 -1
  33. package/lib/typescript/index.web.d.ts +40 -7
  34. package/lib/typescript/index.web.d.ts.map +1 -1
  35. package/lib/typescript/internal.d.ts +2 -0
  36. package/lib/typescript/internal.d.ts.map +1 -1
  37. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +10 -0
  38. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +10 -0
  39. package/package.json +4 -1
  40. package/src/Storage.nitro.ts +11 -2
  41. package/src/Storage.types.ts +22 -0
  42. package/src/index.ts +270 -71
  43. package/src/index.web.ts +431 -90
  44. package/src/internal.ts +14 -4
  45. package/src/migration.ts +1 -1
package/src/index.web.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import { useRef, useSyncExternalStore } from "react";
2
- import { StorageScope } from "./Storage.types";
2
+ import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
3
3
  import {
4
4
  MIGRATION_VERSION_KEY,
5
5
  type StoredEnvelope,
@@ -8,9 +8,11 @@ import {
8
8
  assertValidScope,
9
9
  serializeWithPrimitiveFastPath,
10
10
  deserializeWithPrimitiveFastPath,
11
+ prefixKey,
12
+ isNamespaced,
11
13
  } from "./internal";
12
14
 
13
- export { StorageScope } from "./Storage.types";
15
+ export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
14
16
  export { migrateFromMMKV } from "./migration";
15
17
 
16
18
  export type Validator<T> = (value: unknown) => value is T;
@@ -33,15 +35,26 @@ export type TransactionContext = {
33
35
  setRaw: (key: string, value: string) => void;
34
36
  removeRaw: (key: string) => void;
35
37
  getItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "get">) => T;
36
- setItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "set">, value: T) => void;
37
- removeItem: (item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">) => void;
38
+ setItem: <T>(
39
+ item: Pick<StorageItem<T>, "scope" | "key" | "set">,
40
+ value: T,
41
+ ) => void;
42
+ removeItem: (
43
+ item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">,
44
+ ) => void;
38
45
  };
39
46
 
40
47
  type KeyListenerRegistry = Map<string, Set<() => void>>;
41
48
  type RawBatchPathItem = {
42
49
  _hasValidation?: boolean;
43
50
  _hasExpiration?: boolean;
51
+ _isBiometric?: boolean;
52
+ _secureAccessControl?: AccessControl;
44
53
  };
54
+
55
+ function asInternal(item: StorageItem<any>): StorageItemInternal<any> {
56
+ return item as unknown as StorageItemInternal<any>;
57
+ }
45
58
  type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
46
59
  type PendingSecureWrite = { key: string; value: string | undefined };
47
60
  type BrowserStorageLike = {
@@ -49,6 +62,8 @@ type BrowserStorageLike = {
49
62
  getItem: (key: string) => string | null;
50
63
  removeItem: (key: string) => void;
51
64
  clear: () => void;
65
+ key: (index: number) => string | null;
66
+ readonly length: number;
52
67
  };
53
68
 
54
69
  const registeredMigrations = new Map<number, Migration>();
@@ -67,13 +82,23 @@ export interface Storage {
67
82
  get(key: string, scope: number): string | undefined;
68
83
  remove(key: string, scope: number): void;
69
84
  clear(scope: number): void;
85
+ has(key: string, scope: number): boolean;
86
+ getAllKeys(scope: number): string[];
87
+ size(scope: number): number;
70
88
  setBatch(keys: string[], values: string[], scope: number): void;
71
89
  getBatch(keys: string[], scope: number): (string | undefined)[];
72
90
  removeBatch(keys: string[], scope: number): void;
73
91
  addOnChange(
74
92
  scope: number,
75
- callback: (key: string, value: string | undefined) => void
93
+ callback: (key: string, value: string | undefined) => void,
76
94
  ): () => void;
95
+ setSecureAccessControl(level: number): void;
96
+ setKeychainAccessGroup(group: string): void;
97
+ setSecureBiometric(key: string, value: string): void;
98
+ getSecureBiometric(key: string): string | undefined;
99
+ deleteSecureBiometric(key: string): void;
100
+ hasSecureBiometric(key: string): boolean;
101
+ clearSecureBiometric(): void;
77
102
  }
78
103
 
79
104
  const memoryStore = new Map<string, unknown>();
@@ -82,38 +107,65 @@ const webScopeListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
82
107
  [StorageScope.Disk, new Map()],
83
108
  [StorageScope.Secure, new Map()],
84
109
  ]);
85
- const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>([
86
- [StorageScope.Disk, new Map()],
87
- [StorageScope.Secure, new Map()],
88
- ]);
110
+ const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
111
+ [
112
+ [StorageScope.Disk, new Map()],
113
+ [StorageScope.Secure, new Map()],
114
+ ],
115
+ );
89
116
  const pendingSecureWrites = new Map<string, PendingSecureWrite>();
90
117
  let secureFlushScheduled = false;
118
+ const SECURE_WEB_PREFIX = "__secure_";
119
+ const BIOMETRIC_WEB_PREFIX = "__bio_";
120
+ let hasWarnedAboutWebBiometricFallback = false;
91
121
 
92
122
  function getBrowserStorage(scope: number): BrowserStorageLike | undefined {
93
123
  if (scope === StorageScope.Disk) {
94
124
  return globalThis.localStorage;
95
125
  }
96
126
  if (scope === StorageScope.Secure) {
97
- return globalThis.sessionStorage;
127
+ return globalThis.localStorage;
98
128
  }
99
129
  return undefined;
100
130
  }
101
131
 
132
+ function toSecureStorageKey(key: string): string {
133
+ return `${SECURE_WEB_PREFIX}${key}`;
134
+ }
135
+
136
+ function fromSecureStorageKey(key: string): string {
137
+ return key.slice(SECURE_WEB_PREFIX.length);
138
+ }
139
+
140
+ function toBiometricStorageKey(key: string): string {
141
+ return `${BIOMETRIC_WEB_PREFIX}${key}`;
142
+ }
143
+
144
+ function fromBiometricStorageKey(key: string): string {
145
+ return key.slice(BIOMETRIC_WEB_PREFIX.length);
146
+ }
147
+
102
148
  function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
103
149
  return webScopeListeners.get(scope)!;
104
150
  }
105
151
 
106
- function getScopeRawCache(scope: NonMemoryScope): Map<string, string | undefined> {
152
+ function getScopeRawCache(
153
+ scope: NonMemoryScope,
154
+ ): Map<string, string | undefined> {
107
155
  return scopedRawCache.get(scope)!;
108
156
  }
109
157
 
110
- function cacheRawValue(scope: NonMemoryScope, key: string, value: string | undefined): void {
158
+ function cacheRawValue(
159
+ scope: NonMemoryScope,
160
+ key: string,
161
+ value: string | undefined,
162
+ ): void {
111
163
  getScopeRawCache(scope).set(key, value);
112
164
  }
113
165
 
114
166
  function readCachedRawValue(
115
167
  scope: NonMemoryScope,
116
- key: string
168
+ key: string,
117
169
  ): string | undefined {
118
170
  return getScopeRawCache(scope).get(key);
119
171
  }
@@ -139,7 +191,7 @@ function notifyAllListeners(registry: KeyListenerRegistry): void {
139
191
  function addKeyListener(
140
192
  registry: KeyListenerRegistry,
141
193
  key: string,
142
- listener: () => void
194
+ listener: () => void,
143
195
  ): () => void {
144
196
  let listeners = registry.get(key);
145
197
  if (!listeners) {
@@ -218,25 +270,70 @@ const WebStorage: Storage = {
218
270
  dispose: () => {},
219
271
  set: (key: string, value: string, scope: number) => {
220
272
  const storage = getBrowserStorage(scope);
221
- storage?.setItem(key, value);
273
+ if (!storage) {
274
+ return;
275
+ }
276
+ const storageKey =
277
+ scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
278
+ storage.setItem(storageKey, value);
222
279
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
223
280
  notifyKeyListeners(getScopedListeners(scope), key);
224
281
  }
225
282
  },
226
283
  get: (key: string, scope: number) => {
227
284
  const storage = getBrowserStorage(scope);
228
- return storage?.getItem(key) ?? undefined;
285
+ const storageKey =
286
+ scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
287
+ return storage?.getItem(storageKey) ?? undefined;
229
288
  },
230
289
  remove: (key: string, scope: number) => {
231
290
  const storage = getBrowserStorage(scope);
232
- storage?.removeItem(key);
291
+ if (!storage) {
292
+ return;
293
+ }
294
+ if (scope === StorageScope.Secure) {
295
+ storage.removeItem(toSecureStorageKey(key));
296
+ storage.removeItem(toBiometricStorageKey(key));
297
+ } else {
298
+ storage.removeItem(key);
299
+ }
233
300
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
234
301
  notifyKeyListeners(getScopedListeners(scope), key);
235
302
  }
236
303
  },
237
304
  clear: (scope: number) => {
238
305
  const storage = getBrowserStorage(scope);
239
- storage?.clear();
306
+ if (!storage) {
307
+ return;
308
+ }
309
+ if (scope === StorageScope.Secure) {
310
+ const keysToRemove: string[] = [];
311
+ for (let i = 0; i < storage.length; i++) {
312
+ const key = storage.key(i);
313
+ if (
314
+ key?.startsWith(SECURE_WEB_PREFIX) ||
315
+ key?.startsWith(BIOMETRIC_WEB_PREFIX)
316
+ ) {
317
+ keysToRemove.push(key);
318
+ }
319
+ }
320
+ keysToRemove.forEach((key) => storage.removeItem(key));
321
+ } else if (scope === StorageScope.Disk) {
322
+ const keysToRemove: string[] = [];
323
+ for (let i = 0; i < storage.length; i++) {
324
+ const key = storage.key(i);
325
+ if (
326
+ key &&
327
+ !key.startsWith(SECURE_WEB_PREFIX) &&
328
+ !key.startsWith(BIOMETRIC_WEB_PREFIX)
329
+ ) {
330
+ keysToRemove.push(key);
331
+ }
332
+ }
333
+ keysToRemove.forEach((key) => storage.removeItem(key));
334
+ } else {
335
+ storage.clear();
336
+ }
240
337
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
241
338
  notifyAllListeners(getScopedListeners(scope));
242
339
  }
@@ -248,7 +345,9 @@ const WebStorage: Storage = {
248
345
  }
249
346
 
250
347
  keys.forEach((key, index) => {
251
- storage.setItem(key, values[index]);
348
+ const storageKey =
349
+ scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
350
+ storage.setItem(storageKey, values[index]);
252
351
  });
253
352
  if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
254
353
  const listeners = getScopedListeners(scope);
@@ -257,28 +356,109 @@ const WebStorage: Storage = {
257
356
  },
258
357
  getBatch: (keys: string[], scope: number) => {
259
358
  const storage = getBrowserStorage(scope);
260
- return keys.map((key) => storage?.getItem(key) ?? undefined);
359
+ return keys.map((key) => {
360
+ const storageKey =
361
+ scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
362
+ return storage?.getItem(storageKey) ?? undefined;
363
+ });
261
364
  },
262
365
  removeBatch: (keys: string[], scope: number) => {
263
- const storage = getBrowserStorage(scope);
264
- if (!storage) {
265
- return;
266
- }
267
-
268
366
  keys.forEach((key) => {
269
- storage.removeItem(key);
367
+ WebStorage.remove(key, scope);
270
368
  });
271
- if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
272
- const listeners = getScopedListeners(scope);
273
- keys.forEach((key) => notifyKeyListeners(listeners, key));
274
- }
275
369
  },
276
370
  addOnChange: (
277
371
  _scope: number,
278
- _callback: (key: string, value: string | undefined) => void
372
+ _callback: (key: string, value: string | undefined) => void,
279
373
  ) => {
280
374
  return () => {};
281
375
  },
376
+ has: (key: string, scope: number) => {
377
+ const storage = getBrowserStorage(scope);
378
+ if (scope === StorageScope.Secure) {
379
+ return (
380
+ storage?.getItem(toSecureStorageKey(key)) !== null ||
381
+ storage?.getItem(toBiometricStorageKey(key)) !== null
382
+ );
383
+ }
384
+ return storage?.getItem(key) !== null;
385
+ },
386
+ getAllKeys: (scope: number) => {
387
+ const storage = getBrowserStorage(scope);
388
+ if (!storage) return [];
389
+ const keys = new Set<string>();
390
+ for (let i = 0; i < storage.length; i++) {
391
+ const k = storage.key(i);
392
+ if (!k) {
393
+ continue;
394
+ }
395
+ if (scope === StorageScope.Secure) {
396
+ if (k.startsWith(SECURE_WEB_PREFIX)) {
397
+ keys.add(fromSecureStorageKey(k));
398
+ } else if (k.startsWith(BIOMETRIC_WEB_PREFIX)) {
399
+ keys.add(fromBiometricStorageKey(k));
400
+ }
401
+ continue;
402
+ }
403
+ if (
404
+ k.startsWith(SECURE_WEB_PREFIX) ||
405
+ k.startsWith(BIOMETRIC_WEB_PREFIX)
406
+ ) {
407
+ continue;
408
+ }
409
+ keys.add(k);
410
+ }
411
+ return Array.from(keys);
412
+ },
413
+ size: (scope: number) => {
414
+ return WebStorage.getAllKeys(scope).length;
415
+ },
416
+ setSecureAccessControl: () => {},
417
+ setKeychainAccessGroup: () => {},
418
+ setSecureBiometric: (key: string, value: string) => {
419
+ if (
420
+ typeof __DEV__ !== "undefined" &&
421
+ __DEV__ &&
422
+ !hasWarnedAboutWebBiometricFallback
423
+ ) {
424
+ hasWarnedAboutWebBiometricFallback = true;
425
+ console.warn(
426
+ "[NitroStorage] Biometric storage is not supported on web. Using localStorage.",
427
+ );
428
+ }
429
+ globalThis.localStorage?.setItem(toBiometricStorageKey(key), value);
430
+ notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
431
+ },
432
+ getSecureBiometric: (key: string) => {
433
+ return (
434
+ globalThis.localStorage?.getItem(toBiometricStorageKey(key)) ?? undefined
435
+ );
436
+ },
437
+ deleteSecureBiometric: (key: string) => {
438
+ globalThis.localStorage?.removeItem(toBiometricStorageKey(key));
439
+ notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
440
+ },
441
+ hasSecureBiometric: (key: string) => {
442
+ return (
443
+ globalThis.localStorage?.getItem(toBiometricStorageKey(key)) !== null
444
+ );
445
+ },
446
+ clearSecureBiometric: () => {
447
+ const storage = globalThis.localStorage;
448
+ if (!storage) return;
449
+ const keysToNotify: string[] = [];
450
+ const toRemove: string[] = [];
451
+ for (let i = 0; i < storage.length; i++) {
452
+ const k = storage.key(i);
453
+ if (k?.startsWith(BIOMETRIC_WEB_PREFIX)) {
454
+ toRemove.push(k);
455
+ keysToNotify.push(fromBiometricStorageKey(k));
456
+ }
457
+ }
458
+ toRemove.forEach((k) => storage.removeItem(k));
459
+ const listeners = getScopedListeners(StorageScope.Secure);
460
+ keysToNotify.forEach((key) => notifyKeyListeners(listeners, key));
461
+ },
282
462
  };
283
463
 
284
464
  function getRawValue(key: string, scope: StorageScope): string | undefined {
@@ -358,12 +538,75 @@ export const storage = {
358
538
 
359
539
  clearScopeRawCache(scope);
360
540
  WebStorage.clear(scope);
541
+ if (scope === StorageScope.Secure) {
542
+ WebStorage.clearSecureBiometric();
543
+ }
361
544
  },
362
545
  clearAll: () => {
363
546
  storage.clear(StorageScope.Memory);
364
547
  storage.clear(StorageScope.Disk);
365
548
  storage.clear(StorageScope.Secure);
366
549
  },
550
+ clearNamespace: (namespace: string, scope: StorageScope) => {
551
+ assertValidScope(scope);
552
+ if (scope === StorageScope.Memory) {
553
+ for (const key of memoryStore.keys()) {
554
+ if (isNamespaced(key, namespace)) {
555
+ memoryStore.delete(key);
556
+ }
557
+ }
558
+ notifyAllListeners(memoryListeners);
559
+ return;
560
+ }
561
+ if (scope === StorageScope.Secure) {
562
+ flushSecureWrites();
563
+ }
564
+ const keys = WebStorage.getAllKeys(scope);
565
+ const namespacedKeys = keys.filter((k) => isNamespaced(k, namespace));
566
+ if (namespacedKeys.length > 0) {
567
+ WebStorage.removeBatch(namespacedKeys, scope);
568
+ namespacedKeys.forEach((k) => cacheRawValue(scope, k, undefined));
569
+ if (scope === StorageScope.Secure) {
570
+ namespacedKeys.forEach((k) => clearPendingSecureWrite(k));
571
+ }
572
+ }
573
+ },
574
+ clearBiometric: () => {
575
+ WebStorage.clearSecureBiometric();
576
+ },
577
+ has: (key: string, scope: StorageScope): boolean => {
578
+ assertValidScope(scope);
579
+ if (scope === StorageScope.Memory) return memoryStore.has(key);
580
+ return WebStorage.has(key, scope);
581
+ },
582
+ getAllKeys: (scope: StorageScope): string[] => {
583
+ assertValidScope(scope);
584
+ if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
585
+ return WebStorage.getAllKeys(scope);
586
+ },
587
+ getAll: (scope: StorageScope): Record<string, string> => {
588
+ assertValidScope(scope);
589
+ const result: Record<string, string> = {};
590
+ if (scope === StorageScope.Memory) {
591
+ memoryStore.forEach((value, key) => {
592
+ if (typeof value === "string") result[key] = value;
593
+ });
594
+ return result;
595
+ }
596
+ const keys = WebStorage.getAllKeys(scope);
597
+ keys.forEach((key) => {
598
+ const val = WebStorage.get(key, scope);
599
+ if (val !== undefined) result[key] = val;
600
+ });
601
+ return result;
602
+ },
603
+ size: (scope: StorageScope): number => {
604
+ assertValidScope(scope);
605
+ if (scope === StorageScope.Memory) return memoryStore.size;
606
+ return WebStorage.size(scope);
607
+ },
608
+ setAccessControl: (_level: AccessControl) => {},
609
+ setKeychainAccessGroup: (_group: string) => {},
367
610
  };
368
611
 
369
612
  export interface StorageItemConfig<T> {
@@ -375,27 +618,42 @@ export interface StorageItemConfig<T> {
375
618
  validate?: Validator<T>;
376
619
  onValidationError?: (invalidValue: unknown) => T;
377
620
  expiration?: ExpirationConfig;
621
+ onExpired?: (key: string) => void;
378
622
  readCache?: boolean;
379
623
  coalesceSecureWrites?: boolean;
624
+ namespace?: string;
625
+ biometric?: boolean;
626
+ accessControl?: AccessControl;
380
627
  }
381
628
 
382
629
  export interface StorageItem<T> {
383
630
  get: () => T;
384
631
  set: (value: T | ((prev: T) => T)) => void;
385
632
  delete: () => void;
633
+ has: () => boolean;
386
634
  subscribe: (callback: () => void) => () => void;
387
635
  serialize: (value: T) => string;
388
636
  deserialize: (value: string) => T;
389
- _triggerListeners: () => void;
390
- _hasValidation?: boolean;
391
- _hasExpiration?: boolean;
392
- _readCacheEnabled?: boolean;
393
637
  scope: StorageScope;
394
638
  key: string;
395
639
  }
396
640
 
641
+ type StorageItemInternal<T> = StorageItem<T> & {
642
+ _triggerListeners: () => void;
643
+ _hasValidation: boolean;
644
+ _hasExpiration: boolean;
645
+ _readCacheEnabled: boolean;
646
+ _isBiometric: boolean;
647
+ _secureAccessControl?: AccessControl;
648
+ };
649
+
397
650
  function canUseRawBatchPath(item: RawBatchPathItem): boolean {
398
- return item._hasExpiration === false && item._hasValidation === false;
651
+ return (
652
+ item._hasExpiration === false &&
653
+ item._hasValidation === false &&
654
+ item._isBiometric !== true &&
655
+ item._secureAccessControl === undefined
656
+ );
399
657
  }
400
658
 
401
659
  function defaultSerialize<T>(value: T): string {
@@ -407,19 +665,28 @@ function defaultDeserialize<T>(value: string): T {
407
665
  }
408
666
 
409
667
  export function createStorageItem<T = undefined>(
410
- config: StorageItemConfig<T>
668
+ config: StorageItemConfig<T>,
411
669
  ): StorageItem<T> {
670
+ const storageKey = prefixKey(config.namespace, config.key);
412
671
  const serialize = config.serialize ?? defaultSerialize;
413
672
  const deserialize = config.deserialize ?? defaultDeserialize;
414
673
  const isMemory = config.scope === StorageScope.Memory;
674
+ const isBiometric =
675
+ config.biometric === true && config.scope === StorageScope.Secure;
676
+ const secureAccessControl = config.accessControl;
415
677
  const validate = config.validate;
416
678
  const onValidationError = config.onValidationError;
417
679
  const expiration = config.expiration;
680
+ const onExpired = config.onExpired;
418
681
  const expirationTtlMs = expiration?.ttlMs;
419
- const memoryExpiration = expiration && isMemory ? new Map<string, number>() : null;
682
+ const memoryExpiration =
683
+ expiration && isMemory ? new Map<string, number>() : null;
420
684
  const readCache = !isMemory && config.readCache === true;
421
685
  const coalesceSecureWrites =
422
- config.scope === StorageScope.Secure && config.coalesceSecureWrites === true;
686
+ config.scope === StorageScope.Secure &&
687
+ config.coalesceSecureWrites === true &&
688
+ !isBiometric &&
689
+ secureAccessControl === undefined;
423
690
  const nonMemoryScope: NonMemoryScope | null =
424
691
  config.scope === StorageScope.Disk
425
692
  ? StorageScope.Disk
@@ -454,79 +721,102 @@ export function createStorageItem<T = undefined>(
454
721
  };
455
722
 
456
723
  if (isMemory) {
457
- unsubscribe = addKeyListener(memoryListeners, config.key, listener);
724
+ unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
458
725
  return;
459
726
  }
460
727
 
461
- unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope!), config.key, listener);
728
+ unsubscribe = addKeyListener(
729
+ getScopedListeners(nonMemoryScope!),
730
+ storageKey,
731
+ listener,
732
+ );
462
733
  };
463
734
 
464
735
  const readStoredRaw = (): unknown => {
465
736
  if (isMemory) {
466
737
  if (memoryExpiration) {
467
- const expiresAt = memoryExpiration.get(config.key);
738
+ const expiresAt = memoryExpiration.get(storageKey);
468
739
  if (expiresAt !== undefined && expiresAt <= Date.now()) {
469
- memoryExpiration.delete(config.key);
470
- memoryStore.delete(config.key);
471
- notifyKeyListeners(memoryListeners, config.key);
740
+ memoryExpiration.delete(storageKey);
741
+ memoryStore.delete(storageKey);
742
+ notifyKeyListeners(memoryListeners, storageKey);
743
+ onExpired?.(storageKey);
472
744
  return undefined;
473
745
  }
474
746
  }
475
- return memoryStore.get(config.key) as T | undefined;
747
+ return memoryStore.get(storageKey) as T | undefined;
476
748
  }
477
749
 
478
- if (nonMemoryScope === StorageScope.Secure && hasPendingSecureWrite(config.key)) {
479
- return readPendingSecureWrite(config.key);
750
+ if (
751
+ nonMemoryScope === StorageScope.Secure &&
752
+ !isBiometric &&
753
+ hasPendingSecureWrite(storageKey)
754
+ ) {
755
+ return readPendingSecureWrite(storageKey);
480
756
  }
481
757
 
482
758
  if (readCache) {
483
- if (hasCachedRawValue(nonMemoryScope!, config.key)) {
484
- return readCachedRawValue(nonMemoryScope!, config.key);
759
+ if (hasCachedRawValue(nonMemoryScope!, storageKey)) {
760
+ return readCachedRawValue(nonMemoryScope!, storageKey);
485
761
  }
486
762
  }
487
763
 
488
- const raw = WebStorage.get(config.key, config.scope);
489
- cacheRawValue(nonMemoryScope!, config.key, raw);
764
+ if (isBiometric) {
765
+ return WebStorage.getSecureBiometric(storageKey);
766
+ }
767
+
768
+ const raw = WebStorage.get(storageKey, config.scope);
769
+ cacheRawValue(nonMemoryScope!, storageKey, raw);
490
770
  return raw;
491
771
  };
492
772
 
493
773
  const writeStoredRaw = (rawValue: string): void => {
494
- cacheRawValue(nonMemoryScope!, config.key, rawValue);
774
+ if (isBiometric) {
775
+ WebStorage.setSecureBiometric(storageKey, rawValue);
776
+ return;
777
+ }
778
+
779
+ cacheRawValue(nonMemoryScope!, storageKey, rawValue);
495
780
 
496
781
  if (coalesceSecureWrites) {
497
- scheduleSecureWrite(config.key, rawValue);
782
+ scheduleSecureWrite(storageKey, rawValue);
498
783
  return;
499
784
  }
500
785
 
501
786
  if (nonMemoryScope === StorageScope.Secure) {
502
- clearPendingSecureWrite(config.key);
787
+ clearPendingSecureWrite(storageKey);
503
788
  }
504
789
 
505
- WebStorage.set(config.key, rawValue, config.scope);
790
+ WebStorage.set(storageKey, rawValue, config.scope);
506
791
  };
507
792
 
508
793
  const removeStoredRaw = (): void => {
509
- cacheRawValue(nonMemoryScope!, config.key, undefined);
794
+ if (isBiometric) {
795
+ WebStorage.deleteSecureBiometric(storageKey);
796
+ return;
797
+ }
798
+
799
+ cacheRawValue(nonMemoryScope!, storageKey, undefined);
510
800
 
511
801
  if (coalesceSecureWrites) {
512
- scheduleSecureWrite(config.key, undefined);
802
+ scheduleSecureWrite(storageKey, undefined);
513
803
  return;
514
804
  }
515
805
 
516
806
  if (nonMemoryScope === StorageScope.Secure) {
517
- clearPendingSecureWrite(config.key);
807
+ clearPendingSecureWrite(storageKey);
518
808
  }
519
809
 
520
- WebStorage.remove(config.key, config.scope);
810
+ WebStorage.remove(storageKey, config.scope);
521
811
  };
522
812
 
523
813
  const writeValueWithoutValidation = (value: T): void => {
524
814
  if (isMemory) {
525
815
  if (memoryExpiration) {
526
- memoryExpiration.set(config.key, Date.now() + (expirationTtlMs ?? 0));
816
+ memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
527
817
  }
528
- memoryStore.set(config.key, value);
529
- notifyKeyListeners(memoryListeners, config.key);
818
+ memoryStore.set(storageKey, value);
819
+ notifyKeyListeners(memoryListeners, storageKey);
530
820
  return;
531
821
  }
532
822
 
@@ -552,7 +842,10 @@ export function createStorageItem<T = undefined>(
552
842
  return config.defaultValue as T;
553
843
  };
554
844
 
555
- const ensureValidatedValue = (candidate: unknown, hadStoredValue: boolean): T => {
845
+ const ensureValidatedValue = (
846
+ candidate: unknown,
847
+ hadStoredValue: boolean,
848
+ ): T => {
556
849
  if (!validate || validate(candidate)) {
557
850
  return candidate as T;
558
851
  }
@@ -598,6 +891,7 @@ export function createStorageItem<T = undefined>(
598
891
  if (parsed.expiresAt <= Date.now()) {
599
892
  removeStoredRaw();
600
893
  invalidateParsedCache();
894
+ onExpired?.(storageKey);
601
895
  lastValue = ensureValidatedValue(config.defaultValue, false);
602
896
  hasLastValue = true;
603
897
  return lastValue;
@@ -626,7 +920,7 @@ export function createStorageItem<T = undefined>(
626
920
 
627
921
  if (validate && !validate(newValue)) {
628
922
  throw new Error(
629
- `Validation failed for key "${config.key}" in scope "${StorageScope[config.scope]}".`
923
+ `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
630
924
  );
631
925
  }
632
926
 
@@ -638,16 +932,22 @@ export function createStorageItem<T = undefined>(
638
932
 
639
933
  if (isMemory) {
640
934
  if (memoryExpiration) {
641
- memoryExpiration.delete(config.key);
935
+ memoryExpiration.delete(storageKey);
642
936
  }
643
- memoryStore.delete(config.key);
644
- notifyKeyListeners(memoryListeners, config.key);
937
+ memoryStore.delete(storageKey);
938
+ notifyKeyListeners(memoryListeners, storageKey);
645
939
  return;
646
940
  }
647
941
 
648
942
  removeStoredRaw();
649
943
  };
650
944
 
945
+ const hasItem = (): boolean => {
946
+ if (isMemory) return memoryStore.has(storageKey);
947
+ if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
948
+ return WebStorage.has(storageKey, config.scope);
949
+ };
950
+
651
951
  const subscribe = (callback: () => void): (() => void) => {
652
952
  ensureSubscription();
653
953
  listeners.add(callback);
@@ -660,10 +960,11 @@ export function createStorageItem<T = undefined>(
660
960
  };
661
961
  };
662
962
 
663
- const storageItem: StorageItem<T> = {
963
+ const storageItem: StorageItemInternal<T> = {
664
964
  get,
665
965
  set,
666
966
  delete: deleteItem,
967
+ has: hasItem,
667
968
  subscribe,
668
969
  serialize,
669
970
  deserialize,
@@ -674,15 +975,17 @@ export function createStorageItem<T = undefined>(
674
975
  _hasValidation: validate !== undefined,
675
976
  _hasExpiration: expiration !== undefined,
676
977
  _readCacheEnabled: readCache,
978
+ _isBiometric: isBiometric,
979
+ _secureAccessControl: secureAccessControl,
677
980
  scope: config.scope,
678
- key: config.key,
981
+ key: storageKey,
679
982
  };
680
983
 
681
- return storageItem;
984
+ return storageItem as StorageItem<T>;
682
985
  }
683
986
 
684
987
  export function useStorage<T>(
685
- item: StorageItem<T>
988
+ item: StorageItem<T>,
686
989
  ): [T, (value: T | ((prev: T) => T)) => void] {
687
990
  const value = useSyncExternalStore(item.subscribe, item.get, item.get);
688
991
  return [value, item.set];
@@ -691,9 +994,11 @@ export function useStorage<T>(
691
994
  export function useStorageSelector<T, TSelected>(
692
995
  item: StorageItem<T>,
693
996
  selector: (value: T) => TSelected,
694
- isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is
997
+ isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is,
695
998
  ): [TSelected, (value: T | ((prev: T) => T)) => void] {
696
- const selectedRef = useRef<{ hasValue: false } | { hasValue: true; value: TSelected }>({
999
+ const selectedRef = useRef<
1000
+ { hasValue: false } | { hasValue: true; value: TSelected }
1001
+ >({
697
1002
  hasValue: false,
698
1003
  });
699
1004
 
@@ -711,7 +1016,7 @@ export function useStorageSelector<T, TSelected>(
711
1016
  const selectedValue = useSyncExternalStore(
712
1017
  item.subscribe,
713
1018
  getSelectedSnapshot,
714
- getSelectedSnapshot
1019
+ getSelectedSnapshot,
715
1020
  );
716
1021
  return [selectedValue, item.set];
717
1022
  }
@@ -722,14 +1027,14 @@ export function useSetStorage<T>(item: StorageItem<T>) {
722
1027
 
723
1028
  type BatchReadItem<T> = Pick<
724
1029
  StorageItem<T>,
725
- | "key"
726
- | "scope"
727
- | "get"
728
- | "deserialize"
729
- | "_hasValidation"
730
- | "_hasExpiration"
731
- | "_readCacheEnabled"
732
- >;
1030
+ "key" | "scope" | "get" | "deserialize"
1031
+ > & {
1032
+ _hasValidation?: boolean;
1033
+ _hasExpiration?: boolean;
1034
+ _readCacheEnabled?: boolean;
1035
+ _isBiometric?: boolean;
1036
+ _secureAccessControl?: AccessControl;
1037
+ };
733
1038
  type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
734
1039
 
735
1040
  export type StorageBatchSetItem<T> = {
@@ -739,7 +1044,7 @@ export type StorageBatchSetItem<T> = {
739
1044
 
740
1045
  export function getBatch(
741
1046
  items: readonly BatchReadItem<unknown>[],
742
- scope: StorageScope
1047
+ scope: StorageScope,
743
1048
  ): unknown[] {
744
1049
  assertBatchScope(items, scope);
745
1050
 
@@ -797,11 +1102,11 @@ export function getBatch(
797
1102
 
798
1103
  export function setBatch<T>(
799
1104
  items: readonly StorageBatchSetItem<T>[],
800
- scope: StorageScope
1105
+ scope: StorageScope,
801
1106
  ): void {
802
1107
  assertBatchScope(
803
1108
  items.map((batchEntry) => batchEntry.item),
804
- scope
1109
+ scope,
805
1110
  );
806
1111
 
807
1112
  if (scope === StorageScope.Memory) {
@@ -809,7 +1114,9 @@ export function setBatch<T>(
809
1114
  return;
810
1115
  }
811
1116
 
812
- const useRawBatchPath = items.every(({ item }) => canUseRawBatchPath(item));
1117
+ const useRawBatchPath = items.every(({ item }) =>
1118
+ canUseRawBatchPath(asInternal(item)),
1119
+ );
813
1120
  if (!useRawBatchPath) {
814
1121
  items.forEach(({ item, value }) => item.set(value));
815
1122
  return;
@@ -826,7 +1133,7 @@ export function setBatch<T>(
826
1133
 
827
1134
  export function removeBatch(
828
1135
  items: readonly BatchRemoveItem[],
829
- scope: StorageScope
1136
+ scope: StorageScope,
830
1137
  ): void {
831
1138
  assertBatchScope(items, scope);
832
1139
 
@@ -855,7 +1162,9 @@ export function registerMigration(version: number, migration: Migration): void {
855
1162
  registeredMigrations.set(version, migration);
856
1163
  }
857
1164
 
858
- export function migrateToLatest(scope: StorageScope = StorageScope.Disk): number {
1165
+ export function migrateToLatest(
1166
+ scope: StorageScope = StorageScope.Disk,
1167
+ ): number {
859
1168
  assertValidScope(scope);
860
1169
  const currentVersion = readMigrationVersion(scope);
861
1170
  const versions = Array.from(registeredMigrations.keys())
@@ -885,7 +1194,7 @@ export function migrateToLatest(scope: StorageScope = StorageScope.Disk): number
885
1194
 
886
1195
  export function runTransaction<T>(
887
1196
  scope: StorageScope,
888
- transaction: (context: TransactionContext) => T
1197
+ transaction: (context: TransactionContext) => T,
889
1198
  ): T {
890
1199
  assertValidScope(scope);
891
1200
  if (scope === StorageScope.Secure) {
@@ -943,3 +1252,35 @@ export function runTransaction<T>(
943
1252
  throw error;
944
1253
  }
945
1254
  }
1255
+
1256
+ export type SecureAuthStorageConfig<K extends string = string> = Record<
1257
+ K,
1258
+ {
1259
+ ttlMs?: number;
1260
+ biometric?: boolean;
1261
+ accessControl?: AccessControl;
1262
+ }
1263
+ >;
1264
+
1265
+ export function createSecureAuthStorage<K extends string>(
1266
+ config: SecureAuthStorageConfig<K>,
1267
+ options?: { namespace?: string },
1268
+ ): Record<K, StorageItem<string>> {
1269
+ const ns = options?.namespace ?? "auth";
1270
+ const result = {} as Record<K, StorageItem<string>>;
1271
+
1272
+ for (const key of Object.keys(config) as K[]) {
1273
+ const itemConfig = config[key];
1274
+ result[key] = createStorageItem<string>({
1275
+ key,
1276
+ scope: StorageScope.Secure,
1277
+ defaultValue: "",
1278
+ namespace: ns,
1279
+ biometric: itemConfig.biometric,
1280
+ accessControl: itemConfig.accessControl,
1281
+ expiration: itemConfig.ttlMs ? { ttlMs: itemConfig.ttlMs } : undefined,
1282
+ });
1283
+ }
1284
+
1285
+ return result;
1286
+ }