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
@@ -3,3 +3,25 @@ export enum StorageScope {
3
3
  Disk = 1,
4
4
  Secure = 2,
5
5
  }
6
+
7
+ export enum AccessControl {
8
+ /** Accessible when unlocked (default). */
9
+ WhenUnlocked = 0,
10
+ /** Accessible after first unlock until restart. Good for background token refresh. */
11
+ AfterFirstUnlock = 1,
12
+ /** Accessible only when passcode is set, non-migratable. */
13
+ WhenPasscodeSetThisDeviceOnly = 2,
14
+ /** Same as WhenUnlocked but non-migratable between devices. */
15
+ WhenUnlockedThisDeviceOnly = 3,
16
+ /** Same as AfterFirstUnlock but non-migratable. */
17
+ AfterFirstUnlockThisDeviceOnly = 4,
18
+ }
19
+
20
+ export enum BiometricLevel {
21
+ /** No biometric requirement (default). */
22
+ None = 0,
23
+ /** Require biometric or passcode for each access. */
24
+ BiometryOrPasscode = 1,
25
+ /** Require biometric only (no passcode fallback). */
26
+ BiometryOnly = 2,
27
+ }
package/src/index.ts CHANGED
@@ -1,7 +1,7 @@
1
1
  import { useRef, useSyncExternalStore } from "react";
2
2
  import { NitroModules } from "react-native-nitro-modules";
3
3
  import type { Storage } from "./Storage.nitro";
4
- import { StorageScope } from "./Storage.types";
4
+ import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
5
5
  import {
6
6
  MIGRATION_VERSION_KEY,
7
7
  type StoredEnvelope,
@@ -11,9 +11,11 @@ import {
11
11
  decodeNativeBatchValue,
12
12
  serializeWithPrimitiveFastPath,
13
13
  deserializeWithPrimitiveFastPath,
14
+ prefixKey,
15
+ isNamespaced,
14
16
  } from "./internal";
15
17
 
16
- export { StorageScope } from "./Storage.types";
18
+ export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
17
19
  export type { Storage } from "./Storage.nitro";
18
20
  export { migrateFromMMKV } from "./migration";
19
21
 
@@ -37,15 +39,26 @@ export type TransactionContext = {
37
39
  setRaw: (key: string, value: string) => void;
38
40
  removeRaw: (key: string) => void;
39
41
  getItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "get">) => T;
40
- setItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "set">, value: T) => void;
41
- removeItem: (item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">) => void;
42
+ setItem: <T>(
43
+ item: Pick<StorageItem<T>, "scope" | "key" | "set">,
44
+ value: T,
45
+ ) => void;
46
+ removeItem: (
47
+ item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">,
48
+ ) => void;
42
49
  };
43
50
 
44
51
  type KeyListenerRegistry = Map<string, Set<() => void>>;
45
52
  type RawBatchPathItem = {
46
53
  _hasValidation?: boolean;
47
54
  _hasExpiration?: boolean;
55
+ _isBiometric?: boolean;
56
+ _secureAccessControl?: AccessControl;
48
57
  };
58
+
59
+ function asInternal(item: StorageItem<any>): StorageItemInternal<any> {
60
+ return item as unknown as StorageItemInternal<any>;
61
+ }
49
62
  type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
50
63
  type PendingSecureWrite = { key: string; value: string | undefined };
51
64
 
@@ -73,28 +86,37 @@ const scopedListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
73
86
  [StorageScope.Secure, new Map()],
74
87
  ]);
75
88
  const scopedUnsubscribers = new Map<NonMemoryScope, () => void>();
76
- const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>([
77
- [StorageScope.Disk, new Map()],
78
- [StorageScope.Secure, new Map()],
79
- ]);
89
+ const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
90
+ [
91
+ [StorageScope.Disk, new Map()],
92
+ [StorageScope.Secure, new Map()],
93
+ ],
94
+ );
80
95
  const pendingSecureWrites = new Map<string, PendingSecureWrite>();
81
96
  let secureFlushScheduled = false;
97
+ let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
82
98
 
83
99
  function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
84
100
  return scopedListeners.get(scope)!;
85
101
  }
86
102
 
87
- function getScopeRawCache(scope: NonMemoryScope): Map<string, string | undefined> {
103
+ function getScopeRawCache(
104
+ scope: NonMemoryScope,
105
+ ): Map<string, string | undefined> {
88
106
  return scopedRawCache.get(scope)!;
89
107
  }
90
108
 
91
- function cacheRawValue(scope: NonMemoryScope, key: string, value: string | undefined): void {
109
+ function cacheRawValue(
110
+ scope: NonMemoryScope,
111
+ key: string,
112
+ value: string | undefined,
113
+ ): void {
92
114
  getScopeRawCache(scope).set(key, value);
93
115
  }
94
116
 
95
117
  function readCachedRawValue(
96
118
  scope: NonMemoryScope,
97
- key: string
119
+ key: string,
98
120
  ): string | undefined {
99
121
  return getScopeRawCache(scope).get(key);
100
122
  }
@@ -120,7 +142,7 @@ function notifyAllListeners(registry: KeyListenerRegistry): void {
120
142
  function addKeyListener(
121
143
  registry: KeyListenerRegistry,
122
144
  key: string,
123
- listener: () => void
145
+ listener: () => void,
124
146
  ): () => void {
125
147
  let listeners = registry.get(key);
126
148
  if (!listeners) {
@@ -177,6 +199,7 @@ function flushSecureWrites(): void {
177
199
  });
178
200
 
179
201
  const storageModule = getStorageModule();
202
+ storageModule.setSecureAccessControl(secureDefaultAccessControl);
180
203
  if (keysToSet.length > 0) {
181
204
  storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Secure);
182
205
  }
@@ -260,6 +283,7 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
260
283
  if (scope === StorageScope.Secure) {
261
284
  flushSecureWrites();
262
285
  clearPendingSecureWrite(key);
286
+ getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
263
287
  }
264
288
 
265
289
  getStorageModule().set(key, value, scope);
@@ -312,12 +336,88 @@ export const storage = {
312
336
 
313
337
  clearScopeRawCache(scope);
314
338
  getStorageModule().clear(scope);
339
+ if (scope === StorageScope.Secure) {
340
+ getStorageModule().clearSecureBiometric();
341
+ }
315
342
  },
316
343
  clearAll: () => {
317
344
  storage.clear(StorageScope.Memory);
318
345
  storage.clear(StorageScope.Disk);
319
346
  storage.clear(StorageScope.Secure);
320
347
  },
348
+ clearNamespace: (namespace: string, scope: StorageScope) => {
349
+ assertValidScope(scope);
350
+ if (scope === StorageScope.Memory) {
351
+ for (const key of memoryStore.keys()) {
352
+ if (isNamespaced(key, namespace)) {
353
+ memoryStore.delete(key);
354
+ }
355
+ }
356
+ notifyAllListeners(memoryListeners);
357
+ return;
358
+ }
359
+ if (scope === StorageScope.Secure) {
360
+ flushSecureWrites();
361
+ }
362
+ const keys = getStorageModule().getAllKeys(scope);
363
+ const namespacedKeys = keys.filter((k) => isNamespaced(k, namespace));
364
+ if (namespacedKeys.length > 0) {
365
+ getStorageModule().removeBatch(namespacedKeys, scope);
366
+ namespacedKeys.forEach((k) => cacheRawValue(scope, k, undefined));
367
+ if (scope === StorageScope.Secure) {
368
+ namespacedKeys.forEach((k) => clearPendingSecureWrite(k));
369
+ }
370
+ }
371
+ },
372
+ clearBiometric: () => {
373
+ getStorageModule().clearSecureBiometric();
374
+ },
375
+ has: (key: string, scope: StorageScope): boolean => {
376
+ assertValidScope(scope);
377
+ if (scope === StorageScope.Memory) {
378
+ return memoryStore.has(key);
379
+ }
380
+ return getStorageModule().has(key, scope);
381
+ },
382
+ getAllKeys: (scope: StorageScope): string[] => {
383
+ assertValidScope(scope);
384
+ if (scope === StorageScope.Memory) {
385
+ return Array.from(memoryStore.keys());
386
+ }
387
+ return getStorageModule().getAllKeys(scope);
388
+ },
389
+ getAll: (scope: StorageScope): Record<string, string> => {
390
+ assertValidScope(scope);
391
+ const result: Record<string, string> = {};
392
+ if (scope === StorageScope.Memory) {
393
+ memoryStore.forEach((value, key) => {
394
+ if (typeof value === "string") result[key] = value;
395
+ });
396
+ return result;
397
+ }
398
+ const keys = getStorageModule().getAllKeys(scope);
399
+ if (keys.length === 0) return result;
400
+ const values = getStorageModule().getBatch(keys, scope);
401
+ keys.forEach((key, idx) => {
402
+ const val = decodeNativeBatchValue(values[idx]);
403
+ if (val !== undefined) result[key] = val;
404
+ });
405
+ return result;
406
+ },
407
+ size: (scope: StorageScope): number => {
408
+ assertValidScope(scope);
409
+ if (scope === StorageScope.Memory) {
410
+ return memoryStore.size;
411
+ }
412
+ return getStorageModule().size(scope);
413
+ },
414
+ setAccessControl: (level: AccessControl) => {
415
+ secureDefaultAccessControl = level;
416
+ getStorageModule().setSecureAccessControl(level);
417
+ },
418
+ setKeychainAccessGroup: (group: string) => {
419
+ getStorageModule().setKeychainAccessGroup(group);
420
+ },
321
421
  };
322
422
 
323
423
  export interface StorageItemConfig<T> {
@@ -329,27 +429,42 @@ export interface StorageItemConfig<T> {
329
429
  validate?: Validator<T>;
330
430
  onValidationError?: (invalidValue: unknown) => T;
331
431
  expiration?: ExpirationConfig;
432
+ onExpired?: (key: string) => void;
332
433
  readCache?: boolean;
333
434
  coalesceSecureWrites?: boolean;
435
+ namespace?: string;
436
+ biometric?: boolean;
437
+ accessControl?: AccessControl;
334
438
  }
335
439
 
336
440
  export interface StorageItem<T> {
337
441
  get: () => T;
338
442
  set: (value: T | ((prev: T) => T)) => void;
339
443
  delete: () => void;
444
+ has: () => boolean;
340
445
  subscribe: (callback: () => void) => () => void;
341
446
  serialize: (value: T) => string;
342
447
  deserialize: (value: string) => T;
343
- _triggerListeners: () => void;
344
- _hasValidation?: boolean;
345
- _hasExpiration?: boolean;
346
- _readCacheEnabled?: boolean;
347
448
  scope: StorageScope;
348
449
  key: string;
349
450
  }
350
451
 
452
+ type StorageItemInternal<T> = StorageItem<T> & {
453
+ _triggerListeners: () => void;
454
+ _hasValidation: boolean;
455
+ _hasExpiration: boolean;
456
+ _readCacheEnabled: boolean;
457
+ _isBiometric: boolean;
458
+ _secureAccessControl?: AccessControl;
459
+ };
460
+
351
461
  function canUseRawBatchPath(item: RawBatchPathItem): boolean {
352
- return item._hasExpiration === false && item._hasValidation === false;
462
+ return (
463
+ item._hasExpiration === false &&
464
+ item._hasValidation === false &&
465
+ item._isBiometric !== true &&
466
+ item._secureAccessControl === undefined
467
+ );
353
468
  }
354
469
 
355
470
  function defaultSerialize<T>(value: T): string {
@@ -361,19 +476,28 @@ function defaultDeserialize<T>(value: string): T {
361
476
  }
362
477
 
363
478
  export function createStorageItem<T = undefined>(
364
- config: StorageItemConfig<T>
479
+ config: StorageItemConfig<T>,
365
480
  ): StorageItem<T> {
481
+ const storageKey = prefixKey(config.namespace, config.key);
366
482
  const serialize = config.serialize ?? defaultSerialize;
367
483
  const deserialize = config.deserialize ?? defaultDeserialize;
368
484
  const isMemory = config.scope === StorageScope.Memory;
485
+ const isBiometric =
486
+ config.biometric === true && config.scope === StorageScope.Secure;
487
+ const secureAccessControl = config.accessControl;
369
488
  const validate = config.validate;
370
489
  const onValidationError = config.onValidationError;
371
490
  const expiration = config.expiration;
491
+ const onExpired = config.onExpired;
372
492
  const expirationTtlMs = expiration?.ttlMs;
373
- const memoryExpiration = expiration && isMemory ? new Map<string, number>() : null;
493
+ const memoryExpiration =
494
+ expiration && isMemory ? new Map<string, number>() : null;
374
495
  const readCache = !isMemory && config.readCache === true;
375
496
  const coalesceSecureWrites =
376
- config.scope === StorageScope.Secure && config.coalesceSecureWrites === true;
497
+ config.scope === StorageScope.Secure &&
498
+ config.coalesceSecureWrites === true &&
499
+ !isBiometric &&
500
+ secureAccessControl === undefined;
377
501
  const nonMemoryScope: NonMemoryScope | null =
378
502
  config.scope === StorageScope.Disk
379
503
  ? StorageScope.Disk
@@ -408,80 +532,106 @@ export function createStorageItem<T = undefined>(
408
532
  };
409
533
 
410
534
  if (isMemory) {
411
- unsubscribe = addKeyListener(memoryListeners, config.key, listener);
535
+ unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
412
536
  return;
413
537
  }
414
538
 
415
539
  ensureNativeScopeSubscription(nonMemoryScope!);
416
- unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope!), config.key, listener);
540
+ unsubscribe = addKeyListener(
541
+ getScopedListeners(nonMemoryScope!),
542
+ storageKey,
543
+ listener,
544
+ );
417
545
  };
418
546
 
419
547
  const readStoredRaw = (): unknown => {
420
548
  if (isMemory) {
421
549
  if (memoryExpiration) {
422
- const expiresAt = memoryExpiration.get(config.key);
550
+ const expiresAt = memoryExpiration.get(storageKey);
423
551
  if (expiresAt !== undefined && expiresAt <= Date.now()) {
424
- memoryExpiration.delete(config.key);
425
- memoryStore.delete(config.key);
426
- notifyKeyListeners(memoryListeners, config.key);
552
+ memoryExpiration.delete(storageKey);
553
+ memoryStore.delete(storageKey);
554
+ notifyKeyListeners(memoryListeners, storageKey);
555
+ onExpired?.(storageKey);
427
556
  return undefined;
428
557
  }
429
558
  }
430
- return memoryStore.get(config.key) as T | undefined;
559
+ return memoryStore.get(storageKey) as T | undefined;
431
560
  }
432
561
 
433
- if (nonMemoryScope === StorageScope.Secure && hasPendingSecureWrite(config.key)) {
434
- return readPendingSecureWrite(config.key);
562
+ if (
563
+ nonMemoryScope === StorageScope.Secure &&
564
+ !isBiometric &&
565
+ hasPendingSecureWrite(storageKey)
566
+ ) {
567
+ return readPendingSecureWrite(storageKey);
435
568
  }
436
569
 
437
570
  if (readCache) {
438
- if (hasCachedRawValue(nonMemoryScope!, config.key)) {
439
- return readCachedRawValue(nonMemoryScope!, config.key);
571
+ if (hasCachedRawValue(nonMemoryScope!, storageKey)) {
572
+ return readCachedRawValue(nonMemoryScope!, storageKey);
440
573
  }
441
574
  }
442
575
 
443
- const raw = getStorageModule().get(config.key, config.scope);
444
- cacheRawValue(nonMemoryScope!, config.key, raw);
576
+ if (isBiometric) {
577
+ return getStorageModule().getSecureBiometric(storageKey);
578
+ }
579
+
580
+ const raw = getStorageModule().get(storageKey, config.scope);
581
+ cacheRawValue(nonMemoryScope!, storageKey, raw);
445
582
  return raw;
446
583
  };
447
584
 
448
585
  const writeStoredRaw = (rawValue: string): void => {
449
- cacheRawValue(nonMemoryScope!, config.key, rawValue);
586
+ if (isBiometric) {
587
+ getStorageModule().setSecureBiometric(storageKey, rawValue);
588
+ return;
589
+ }
590
+
591
+ cacheRawValue(nonMemoryScope!, storageKey, rawValue);
450
592
 
451
593
  if (coalesceSecureWrites) {
452
- scheduleSecureWrite(config.key, rawValue);
594
+ scheduleSecureWrite(storageKey, rawValue);
453
595
  return;
454
596
  }
455
597
 
456
598
  if (nonMemoryScope === StorageScope.Secure) {
457
- clearPendingSecureWrite(config.key);
599
+ clearPendingSecureWrite(storageKey);
600
+ getStorageModule().setSecureAccessControl(
601
+ secureAccessControl ?? secureDefaultAccessControl,
602
+ );
458
603
  }
459
604
 
460
- getStorageModule().set(config.key, rawValue, config.scope);
605
+ getStorageModule().set(storageKey, rawValue, config.scope);
461
606
  };
462
607
 
463
608
  const removeStoredRaw = (): void => {
464
- cacheRawValue(nonMemoryScope!, config.key, undefined);
609
+ if (isBiometric) {
610
+ getStorageModule().deleteSecureBiometric(storageKey);
611
+ return;
612
+ }
613
+
614
+ cacheRawValue(nonMemoryScope!, storageKey, undefined);
465
615
 
466
616
  if (coalesceSecureWrites) {
467
- scheduleSecureWrite(config.key, undefined);
617
+ scheduleSecureWrite(storageKey, undefined);
468
618
  return;
469
619
  }
470
620
 
471
621
  if (nonMemoryScope === StorageScope.Secure) {
472
- clearPendingSecureWrite(config.key);
622
+ clearPendingSecureWrite(storageKey);
473
623
  }
474
624
 
475
- getStorageModule().remove(config.key, config.scope);
625
+ getStorageModule().remove(storageKey, config.scope);
476
626
  };
477
627
 
478
628
  const writeValueWithoutValidation = (value: T): void => {
479
629
  if (isMemory) {
480
630
  if (memoryExpiration) {
481
- memoryExpiration.set(config.key, Date.now() + (expirationTtlMs ?? 0));
631
+ memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
482
632
  }
483
- memoryStore.set(config.key, value);
484
- notifyKeyListeners(memoryListeners, config.key);
633
+ memoryStore.set(storageKey, value);
634
+ notifyKeyListeners(memoryListeners, storageKey);
485
635
  return;
486
636
  }
487
637
 
@@ -509,7 +659,7 @@ export function createStorageItem<T = undefined>(
509
659
 
510
660
  const ensureValidatedValue = (
511
661
  candidate: unknown,
512
- hadStoredValue: boolean
662
+ hadStoredValue: boolean,
513
663
  ): T => {
514
664
  if (!validate || validate(candidate)) {
515
665
  return candidate as T;
@@ -556,6 +706,7 @@ export function createStorageItem<T = undefined>(
556
706
  if (parsed.expiresAt <= Date.now()) {
557
707
  removeStoredRaw();
558
708
  invalidateParsedCache();
709
+ onExpired?.(storageKey);
559
710
  lastValue = ensureValidatedValue(config.defaultValue, false);
560
711
  hasLastValue = true;
561
712
  return lastValue;
@@ -584,7 +735,7 @@ export function createStorageItem<T = undefined>(
584
735
 
585
736
  if (validate && !validate(newValue)) {
586
737
  throw new Error(
587
- `Validation failed for key "${config.key}" in scope "${StorageScope[config.scope]}".`
738
+ `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
588
739
  );
589
740
  }
590
741
 
@@ -596,16 +747,22 @@ export function createStorageItem<T = undefined>(
596
747
 
597
748
  if (isMemory) {
598
749
  if (memoryExpiration) {
599
- memoryExpiration.delete(config.key);
750
+ memoryExpiration.delete(storageKey);
600
751
  }
601
- memoryStore.delete(config.key);
602
- notifyKeyListeners(memoryListeners, config.key);
752
+ memoryStore.delete(storageKey);
753
+ notifyKeyListeners(memoryListeners, storageKey);
603
754
  return;
604
755
  }
605
756
 
606
757
  removeStoredRaw();
607
758
  };
608
759
 
760
+ const hasItem = (): boolean => {
761
+ if (isMemory) return memoryStore.has(storageKey);
762
+ if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
763
+ return getStorageModule().has(storageKey, config.scope);
764
+ };
765
+
609
766
  const subscribe = (callback: () => void): (() => void) => {
610
767
  ensureSubscription();
611
768
  listeners.add(callback);
@@ -621,10 +778,11 @@ export function createStorageItem<T = undefined>(
621
778
  };
622
779
  };
623
780
 
624
- const storageItem: StorageItem<T> = {
781
+ const storageItem: StorageItemInternal<T> = {
625
782
  get,
626
783
  set,
627
784
  delete: deleteItem,
785
+ has: hasItem,
628
786
  subscribe,
629
787
  serialize,
630
788
  deserialize,
@@ -635,15 +793,17 @@ export function createStorageItem<T = undefined>(
635
793
  _hasValidation: validate !== undefined,
636
794
  _hasExpiration: expiration !== undefined,
637
795
  _readCacheEnabled: readCache,
796
+ _isBiometric: isBiometric,
797
+ _secureAccessControl: secureAccessControl,
638
798
  scope: config.scope,
639
- key: config.key,
799
+ key: storageKey,
640
800
  };
641
801
 
642
- return storageItem;
802
+ return storageItem as StorageItem<T>;
643
803
  }
644
804
 
645
805
  export function useStorage<T>(
646
- item: StorageItem<T>
806
+ item: StorageItem<T>,
647
807
  ): [T, (value: T | ((prev: T) => T)) => void] {
648
808
  const value = useSyncExternalStore(item.subscribe, item.get, item.get);
649
809
  return [value, item.set];
@@ -652,9 +812,11 @@ export function useStorage<T>(
652
812
  export function useStorageSelector<T, TSelected>(
653
813
  item: StorageItem<T>,
654
814
  selector: (value: T) => TSelected,
655
- isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is
815
+ isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is,
656
816
  ): [TSelected, (value: T | ((prev: T) => T)) => void] {
657
- const selectedRef = useRef<{ hasValue: false } | { hasValue: true; value: TSelected }>({
817
+ const selectedRef = useRef<
818
+ { hasValue: false } | { hasValue: true; value: TSelected }
819
+ >({
658
820
  hasValue: false,
659
821
  });
660
822
 
@@ -672,7 +834,7 @@ export function useStorageSelector<T, TSelected>(
672
834
  const selectedValue = useSyncExternalStore(
673
835
  item.subscribe,
674
836
  getSelectedSnapshot,
675
- getSelectedSnapshot
837
+ getSelectedSnapshot,
676
838
  );
677
839
  return [selectedValue, item.set];
678
840
  }
@@ -683,14 +845,14 @@ export function useSetStorage<T>(item: StorageItem<T>) {
683
845
 
684
846
  type BatchReadItem<T> = Pick<
685
847
  StorageItem<T>,
686
- | "key"
687
- | "scope"
688
- | "get"
689
- | "deserialize"
690
- | "_hasValidation"
691
- | "_hasExpiration"
692
- | "_readCacheEnabled"
693
- >;
848
+ "key" | "scope" | "get" | "deserialize"
849
+ > & {
850
+ _hasValidation?: boolean;
851
+ _hasExpiration?: boolean;
852
+ _readCacheEnabled?: boolean;
853
+ _isBiometric?: boolean;
854
+ _secureAccessControl?: AccessControl;
855
+ };
694
856
  type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
695
857
 
696
858
  export type StorageBatchSetItem<T> = {
@@ -700,7 +862,7 @@ export type StorageBatchSetItem<T> = {
700
862
 
701
863
  export function getBatch(
702
864
  items: readonly BatchReadItem<unknown>[],
703
- scope: StorageScope
865
+ scope: StorageScope,
704
866
  ): unknown[] {
705
867
  assertBatchScope(items, scope);
706
868
 
@@ -761,11 +923,11 @@ export function getBatch(
761
923
 
762
924
  export function setBatch<T>(
763
925
  items: readonly StorageBatchSetItem<T>[],
764
- scope: StorageScope
926
+ scope: StorageScope,
765
927
  ): void {
766
928
  assertBatchScope(
767
929
  items.map((batchEntry) => batchEntry.item),
768
- scope
930
+ scope,
769
931
  );
770
932
 
771
933
  if (scope === StorageScope.Memory) {
@@ -773,7 +935,9 @@ export function setBatch<T>(
773
935
  return;
774
936
  }
775
937
 
776
- const useRawBatchPath = items.every(({ item }) => canUseRawBatchPath(item));
938
+ const useRawBatchPath = items.every(({ item }) =>
939
+ canUseRawBatchPath(asInternal(item)),
940
+ );
777
941
  if (!useRawBatchPath) {
778
942
  items.forEach(({ item, value }) => item.set(value));
779
943
  return;
@@ -784,6 +948,7 @@ export function setBatch<T>(
784
948
 
785
949
  if (scope === StorageScope.Secure) {
786
950
  flushSecureWrites();
951
+ getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
787
952
  }
788
953
  getStorageModule().setBatch(keys, values, scope);
789
954
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
@@ -791,7 +956,7 @@ export function setBatch<T>(
791
956
 
792
957
  export function removeBatch(
793
958
  items: readonly BatchRemoveItem[],
794
- scope: StorageScope
959
+ scope: StorageScope,
795
960
  ): void {
796
961
  assertBatchScope(items, scope);
797
962
 
@@ -820,7 +985,9 @@ export function registerMigration(version: number, migration: Migration): void {
820
985
  registeredMigrations.set(version, migration);
821
986
  }
822
987
 
823
- export function migrateToLatest(scope: StorageScope = StorageScope.Disk): number {
988
+ export function migrateToLatest(
989
+ scope: StorageScope = StorageScope.Disk,
990
+ ): number {
824
991
  assertValidScope(scope);
825
992
  const currentVersion = readMigrationVersion(scope);
826
993
  const versions = Array.from(registeredMigrations.keys())
@@ -850,7 +1017,7 @@ export function migrateToLatest(scope: StorageScope = StorageScope.Disk): number
850
1017
 
851
1018
  export function runTransaction<T>(
852
1019
  scope: StorageScope,
853
- transaction: (context: TransactionContext) => T
1020
+ transaction: (context: TransactionContext) => T,
854
1021
  ): T {
855
1022
  assertValidScope(scope);
856
1023
  if (scope === StorageScope.Secure) {
@@ -908,3 +1075,35 @@ export function runTransaction<T>(
908
1075
  throw error;
909
1076
  }
910
1077
  }
1078
+
1079
+ export type SecureAuthStorageConfig<K extends string = string> = Record<
1080
+ K,
1081
+ {
1082
+ ttlMs?: number;
1083
+ biometric?: boolean;
1084
+ accessControl?: AccessControl;
1085
+ }
1086
+ >;
1087
+
1088
+ export function createSecureAuthStorage<K extends string>(
1089
+ config: SecureAuthStorageConfig<K>,
1090
+ options?: { namespace?: string },
1091
+ ): Record<K, StorageItem<string>> {
1092
+ const ns = options?.namespace ?? "auth";
1093
+ const result = {} as Record<K, StorageItem<string>>;
1094
+
1095
+ for (const key of Object.keys(config) as K[]) {
1096
+ const itemConfig = config[key];
1097
+ result[key] = createStorageItem<string>({
1098
+ key,
1099
+ scope: StorageScope.Secure,
1100
+ defaultValue: "",
1101
+ namespace: ns,
1102
+ biometric: itemConfig.biometric,
1103
+ accessControl: itemConfig.accessControl,
1104
+ expiration: itemConfig.ttlMs ? { ttlMs: itemConfig.ttlMs } : undefined,
1105
+ });
1106
+ }
1107
+
1108
+ return result;
1109
+ }