react-native-nitro-storage 0.5.6 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/index.web.ts CHANGED
@@ -1,16 +1,11 @@
1
- import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
2
1
  import {
3
- MIGRATION_VERSION_KEY,
4
- type StoredEnvelope,
5
- isStoredEnvelope,
6
- assertBatchScope,
7
- assertValidScope,
8
- serializeWithPrimitiveFastPath,
9
- deserializeWithPrimitiveFastPath,
10
- toVersionToken,
11
- prefixKey,
12
- isNamespaced,
13
- } from "./internal";
2
+ assertAccessControlLevel,
3
+ assertBiometricLevel,
4
+ notifyAllListeners,
5
+ notifyKeyListeners,
6
+ type NonMemoryScope,
7
+ } from "./shared";
8
+ import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
14
9
  import {
15
10
  createLocalStorageWebBackend,
16
11
  type WebDiskStorageBackend,
@@ -18,23 +13,32 @@ import {
18
13
  type WebStorageBackend,
19
14
  type WebStorageChangeEvent,
20
15
  } from "./web-storage-backend";
21
- import {
22
- getStorageErrorCode,
23
- isLockedStorageErrorCode,
24
- type SecureStorageMetadata,
25
- type SecurityCapabilities,
26
- type StorageCapabilities,
27
- type StorageErrorCode,
16
+ import type {
17
+ SecurityCapabilities,
18
+ StorageCapabilities,
28
19
  } from "./storage-runtime";
29
20
  import {
30
- StorageEventRegistry,
31
- type StorageBatchChangeEvent,
32
- type StorageChangeEvent,
33
- type StorageChangeOperation,
34
- type StorageChangeSource,
35
- type StorageEventListener,
36
- type StorageKeyChangeEvent,
37
- } from "./storage-events";
21
+ createStorageCore,
22
+ type StorageCoreAdapter,
23
+ type StorageCoreInternals,
24
+ } from "./storage-core";
25
+ export type {
26
+ ExpirationConfig,
27
+ Migration,
28
+ MigrationContext,
29
+ SecureAuthStorageConfig,
30
+ StorageEventObserverOptions,
31
+ StorageExportOptions,
32
+ StorageMetricSummary,
33
+ StorageMetricsEvent,
34
+ StorageMetricsObserver,
35
+ StorageSelectorListener,
36
+ StorageSelectorSubscribeOptions,
37
+ StorageVersion,
38
+ Validator,
39
+ VersionedValue,
40
+ } from "./shared";
41
+ export { isKeychainLockedError } from "./shared";
38
42
 
39
43
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
40
44
  export { migrateFromMMKV } from "./migration";
@@ -53,6 +57,7 @@ export type {
53
57
  StorageEventListener,
54
58
  StorageKeyChangeEvent,
55
59
  } from "./storage-events";
60
+ export type { StorageSetter } from "./storage-hooks";
56
61
  export type {
57
62
  WebDiskStorageBackend,
58
63
  WebSecureStorageBackend,
@@ -60,147 +65,12 @@ export type {
60
65
  WebStorageChangeEvent,
61
66
  WebStorageScope,
62
67
  } from "./web-storage-backend";
63
-
64
- export type Validator<T> = (value: unknown) => value is T;
65
- export type ExpirationConfig = {
66
- ttlMs: number;
67
- };
68
- export type StorageVersion = string;
69
- export type VersionedValue<T> = {
70
- value: T;
71
- version: StorageVersion;
72
- };
73
- export type StorageMetricsEvent = {
74
- operation: string;
75
- scope: StorageScope;
76
- durationMs: number;
77
- keysCount: number;
78
- };
79
- export type StorageMetricsObserver = (event: StorageMetricsEvent) => void;
80
- export type StorageEventObserverOptions = {
81
- redactSecureValues?: boolean;
82
- };
83
- export type StorageExportOptions = {
84
- includeSecureValues?: boolean;
85
- };
86
- export type StorageMetricSummary = {
87
- count: number;
88
- totalDurationMs: number;
89
- avgDurationMs: number;
90
- maxDurationMs: number;
91
- };
92
- export type StorageSelectorListener<TSelected> = (
93
- value: TSelected,
94
- previousValue: TSelected,
95
- ) => void;
96
- export type StorageSelectorSubscribeOptions<TSelected> = {
97
- isEqual?: (previousValue: TSelected, nextValue: TSelected) => boolean;
98
- fireImmediately?: boolean;
99
- };
100
- export type MigrationContext = {
101
- scope: StorageScope;
102
- getRaw: (key: string) => string | undefined;
103
- setRaw: (key: string, value: string) => void;
104
- removeRaw: (key: string) => void;
105
- };
106
-
107
- export type Migration = (context: MigrationContext) => void;
108
-
109
- export type TransactionContext = {
110
- scope: StorageScope;
111
- getRaw: (key: string) => string | undefined;
112
- setRaw: (key: string, value: string) => void;
113
- removeRaw: (key: string) => void;
114
- getItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "get">) => T;
115
- setItem: <T>(
116
- item: Pick<StorageItem<T>, "scope" | "key" | "set">,
117
- value: T,
118
- ) => void;
119
- removeItem: (
120
- item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">,
121
- ) => void;
122
- };
123
-
124
- type KeyListenerRegistry = Map<string, Set<() => void>>;
125
- type RawBatchPathItem = {
126
- _hasValidation?: boolean;
127
- _hasExpiration?: boolean;
128
- _isBiometric?: boolean;
129
- _biometricLevel?: BiometricLevel;
130
- _secureAccessControl?: AccessControl;
131
- };
132
- type RollbackRecord =
133
- | {
134
- kind: "memory";
135
- value: unknown;
136
- }
137
- | {
138
- kind: "raw";
139
- value: string | undefined;
140
- accessControl?: AccessControl;
141
- }
142
- | {
143
- kind: "biometric";
144
- value: string | undefined;
145
- level: BiometricLevel;
146
- };
147
-
148
- function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
149
- return item as StorageItemInternal<T>;
150
- }
151
-
152
- function isUpdater<T>(
153
- valueOrFn: T | ((prev: T) => T),
154
- ): valueOrFn is (prev: T) => T {
155
- return typeof valueOrFn === "function";
156
- }
157
-
158
- function typedKeys<K extends string, V>(record: Record<K, V>): K[] {
159
- return Object.keys(record) as K[];
160
- }
161
- function assertEnumInteger(
162
- value: number,
163
- min: number,
164
- max: number,
165
- label: string,
166
- ): void {
167
- if (!Number.isFinite(value) || value < min || value > max) {
168
- throw new Error(`NitroStorage: Invalid ${label}`);
169
- }
170
- if (value !== Math.trunc(value)) {
171
- throw new Error(`NitroStorage: Invalid ${label}`);
172
- }
173
- }
174
-
175
- function assertAccessControlLevel(level: number): void {
176
- assertEnumInteger(level, 0, 4, "access control level");
177
- }
178
-
179
- function assertBiometricLevel(level: number): void {
180
- assertEnumInteger(level, 0, 2, "biometric level");
181
- }
182
- type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
183
- type PendingDiskWrite = {
184
- key: string;
185
- value: string | undefined;
186
- };
187
- type PendingSecureWrite = {
188
- key: string;
189
- value: string | undefined;
190
- accessControl?: AccessControl;
191
- };
192
-
193
- const registeredMigrations = new Map<number, Migration>();
194
- const runMicrotask =
195
- typeof queueMicrotask === "function"
196
- ? queueMicrotask
197
- : (task: () => void) => {
198
- Promise.resolve().then(task);
199
- };
200
- const now =
201
- typeof performance !== "undefined" && typeof performance.now === "function"
202
- ? () => performance.now()
203
- : () => Date.now();
68
+ export type {
69
+ StorageBatchSetItem,
70
+ StorageItem,
71
+ StorageItemConfig,
72
+ TransactionContext,
73
+ } from "./storage-core";
204
74
 
205
75
  export interface Storage {
206
76
  name: string;
@@ -233,79 +103,17 @@ export interface Storage {
233
103
  clearSecureBiometric(): void;
234
104
  }
235
105
 
236
- const memoryStore = new Map<string, unknown>();
237
- const memoryListeners: KeyListenerRegistry = new Map();
238
- const webScopeListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
239
- [StorageScope.Disk, new Map()],
240
- [StorageScope.Secure, new Map()],
241
- ]);
242
- const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
243
- [
244
- [StorageScope.Disk, new Map()],
245
- [StorageScope.Secure, new Map()],
246
- ],
247
- );
248
106
  const webScopeKeyIndex = new Map<NonMemoryScope, Set<string>>([
249
107
  [StorageScope.Disk, new Set()],
250
108
  [StorageScope.Secure, new Set()],
251
109
  ]);
252
110
  const hydratedWebScopeKeyIndex = new Set<NonMemoryScope>();
253
- const pendingDiskWrites = new Map<string, PendingDiskWrite>();
254
- let diskFlushScheduled = false;
255
- let diskWritesAsync = false;
256
- const pendingSecureWrites = new Map<string, PendingSecureWrite>();
257
- let secureFlushScheduled = false;
258
- let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
259
111
  const SECURE_WEB_PREFIX = "__secure_";
260
112
  const BIOMETRIC_WEB_PREFIX = "__bio_";
261
113
  let hasWarnedAboutWebBiometricFallback = false;
262
114
  let hasWindowStorageEventSubscription = false;
263
- let metricsObserver: StorageMetricsObserver | undefined;
264
- let eventObserver: StorageEventListener | undefined;
265
- let eventObserverRedactSecureValues = true;
266
- const metricsCounters = new Map<
267
- string,
268
- { count: number; totalDurationMs: number; maxDurationMs: number }
269
- >();
270
- const storageEvents = new StorageEventRegistry();
271
-
272
- function recordMetric(
273
- operation: string,
274
- scope: StorageScope,
275
- durationMs: number,
276
- keysCount = 1,
277
- ): void {
278
- const existing = metricsCounters.get(operation);
279
- if (!existing) {
280
- metricsCounters.set(operation, {
281
- count: 1,
282
- totalDurationMs: durationMs,
283
- maxDurationMs: durationMs,
284
- });
285
- } else {
286
- existing.count += 1;
287
- existing.totalDurationMs += durationMs;
288
- existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
289
- }
290
- metricsObserver?.({ operation, scope, durationMs, keysCount });
291
- }
292
115
 
293
- function measureOperation<T>(
294
- operation: string,
295
- scope: StorageScope,
296
- fn: () => T,
297
- keysCount = 1,
298
- ): T {
299
- if (!metricsObserver) {
300
- return fn();
301
- }
302
- const start = now();
303
- try {
304
- return fn();
305
- } finally {
306
- recordMetric(operation, scope, now() - start, keysCount);
307
- }
308
- }
116
+ let internals!: StorageCoreInternals;
309
117
 
310
118
  function createDefaultDiskBackend(): WebDiskStorageBackend {
311
119
  return createLocalStorageWebBackend({
@@ -444,24 +252,30 @@ function applyExternalChangeEvent(
444
252
  newValue: string | null,
445
253
  ): void {
446
254
  if (key === null) {
447
- clearScopeRawCache(scope);
255
+ internals.clearScopeRawCache(scope);
448
256
  ensureWebScopeKeyIndex(scope).clear();
449
- notifyAllListeners(getScopedListeners(scope));
257
+ notifyAllListeners(internals.getScopedListeners(scope));
450
258
  return;
451
259
  }
452
260
 
453
261
  if (scope === StorageScope.Secure && key.startsWith(SECURE_WEB_PREFIX)) {
454
262
  const plainKey = fromSecureStorageKey(key);
455
- const oldValue = readCachedRawValue(StorageScope.Secure, plainKey);
263
+ const oldValue = internals.readCachedRawValue(
264
+ StorageScope.Secure,
265
+ plainKey,
266
+ );
456
267
  if (newValue === null) {
457
268
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
458
- cacheRawValue(StorageScope.Secure, plainKey, undefined);
269
+ internals.cacheRawValue(StorageScope.Secure, plainKey, undefined);
459
270
  } else {
460
271
  ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
461
- cacheRawValue(StorageScope.Secure, plainKey, newValue);
272
+ internals.cacheRawValue(StorageScope.Secure, plainKey, newValue);
462
273
  }
463
- notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
464
- emitKeyChange(
274
+ notifyKeyListeners(
275
+ internals.getScopedListeners(StorageScope.Secure),
276
+ plainKey,
277
+ );
278
+ internals.emitKeyChange(
465
279
  StorageScope.Secure,
466
280
  plainKey,
467
281
  oldValue,
@@ -474,7 +288,10 @@ function applyExternalChangeEvent(
474
288
 
475
289
  if (scope === StorageScope.Secure && key.startsWith(BIOMETRIC_WEB_PREFIX)) {
476
290
  const plainKey = fromBiometricStorageKey(key);
477
- const oldValue = readCachedRawValue(StorageScope.Secure, plainKey);
291
+ const oldValue = internals.readCachedRawValue(
292
+ StorageScope.Secure,
293
+ plainKey,
294
+ );
478
295
  if (newValue === null) {
479
296
  if (
480
297
  withWebBackendOperation(
@@ -485,13 +302,16 @@ function applyExternalChangeEvent(
485
302
  ) {
486
303
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
487
304
  }
488
- cacheRawValue(StorageScope.Secure, plainKey, undefined);
305
+ internals.cacheRawValue(StorageScope.Secure, plainKey, undefined);
489
306
  } else {
490
307
  ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
491
- cacheRawValue(StorageScope.Secure, plainKey, newValue);
308
+ internals.cacheRawValue(StorageScope.Secure, plainKey, newValue);
492
309
  }
493
- notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
494
- emitKeyChange(
310
+ notifyKeyListeners(
311
+ internals.getScopedListeners(StorageScope.Secure),
312
+ plainKey,
313
+ );
314
+ internals.emitKeyChange(
495
315
  StorageScope.Secure,
496
316
  plainKey,
497
317
  oldValue,
@@ -502,16 +322,16 @@ function applyExternalChangeEvent(
502
322
  return;
503
323
  }
504
324
 
505
- const oldValue = readCachedRawValue(scope, key);
325
+ const oldValue = internals.readCachedRawValue(scope, key);
506
326
  if (newValue === null) {
507
327
  ensureWebScopeKeyIndex(scope).delete(key);
508
- cacheRawValue(scope, key, undefined);
328
+ internals.cacheRawValue(scope, key, undefined);
509
329
  } else {
510
330
  ensureWebScopeKeyIndex(scope).add(key);
511
- cacheRawValue(scope, key, newValue);
331
+ internals.cacheRawValue(scope, key, newValue);
512
332
  }
513
- notifyKeyListeners(getScopedListeners(scope), key);
514
- emitKeyChange(
333
+ notifyKeyListeners(internals.getScopedListeners(scope), key);
334
+ internals.emitKeyChange(
515
335
  scope,
516
336
  key,
517
337
  oldValue,
@@ -590,321 +410,6 @@ function ensureExternalSyncSubscriptions(): void {
590
410
  subscribeToBackendChanges(StorageScope.Secure);
591
411
  }
592
412
 
593
- function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
594
- return webScopeListeners.get(scope)!;
595
- }
596
-
597
- function getScopeRawCache(
598
- scope: NonMemoryScope,
599
- ): Map<string, string | undefined> {
600
- return scopedRawCache.get(scope)!;
601
- }
602
-
603
- function cacheRawValue(
604
- scope: NonMemoryScope,
605
- key: string,
606
- value: string | undefined,
607
- ): void {
608
- getScopeRawCache(scope).set(key, value);
609
- }
610
-
611
- function readCachedRawValue(
612
- scope: NonMemoryScope,
613
- key: string,
614
- ): string | undefined {
615
- return getScopeRawCache(scope).get(key);
616
- }
617
-
618
- function hasCachedRawValue(scope: NonMemoryScope, key: string): boolean {
619
- return getScopeRawCache(scope).has(key);
620
- }
621
-
622
- function clearScopeRawCache(scope: NonMemoryScope): void {
623
- getScopeRawCache(scope).clear();
624
- }
625
-
626
- function notifyKeyListeners(registry: KeyListenerRegistry, key: string): void {
627
- const listeners = registry.get(key);
628
- if (listeners) {
629
- for (const listener of listeners) {
630
- listener();
631
- }
632
- }
633
- }
634
-
635
- function notifyAllListeners(registry: KeyListenerRegistry): void {
636
- for (const listeners of registry.values()) {
637
- for (const listener of listeners) {
638
- listener();
639
- }
640
- }
641
- }
642
-
643
- function addKeyListener(
644
- registry: KeyListenerRegistry,
645
- key: string,
646
- listener: () => void,
647
- ): () => void {
648
- let listeners = registry.get(key);
649
- if (!listeners) {
650
- listeners = new Set();
651
- registry.set(key, listeners);
652
- }
653
- listeners.add(listener);
654
-
655
- return () => {
656
- const scopedListeners = registry.get(key);
657
- if (!scopedListeners) {
658
- return;
659
- }
660
- scopedListeners.delete(listener);
661
- if (scopedListeners.size === 0) {
662
- registry.delete(key);
663
- }
664
- };
665
- }
666
-
667
- function getEventRawValue(
668
- scope: StorageScope,
669
- key: string,
670
- ): string | undefined {
671
- if (scope === StorageScope.Memory) {
672
- const value = memoryStore.get(key);
673
- return typeof value === "string" ? value : undefined;
674
- }
675
-
676
- return getRawValue(key, scope);
677
- }
678
-
679
- function createKeyChange(
680
- scope: StorageScope,
681
- key: string,
682
- oldValue: string | undefined,
683
- newValue: string | undefined,
684
- operation: StorageChangeOperation,
685
- source: StorageChangeSource,
686
- ): StorageKeyChangeEvent {
687
- return {
688
- type: "key",
689
- scope,
690
- key,
691
- oldValue,
692
- newValue,
693
- operation,
694
- source,
695
- };
696
- }
697
-
698
- function hasStorageChangeObservers(scope: StorageScope): boolean {
699
- return storageEvents.hasListeners(scope) || eventObserver !== undefined;
700
- }
701
-
702
- function shouldReadPreviousEventValues(scope: StorageScope): boolean {
703
- if (storageEvents.hasListeners(scope)) {
704
- return true;
705
- }
706
- if (!eventObserver) {
707
- return false;
708
- }
709
- return scope !== StorageScope.Secure || !eventObserverRedactSecureValues;
710
- }
711
-
712
- const SECURE_EVENT_REDACTED_VALUE = "[secure]";
713
-
714
- function redactSecureKeyChange(
715
- event: StorageKeyChangeEvent,
716
- ): StorageKeyChangeEvent {
717
- if (event.scope !== StorageScope.Secure) {
718
- return event;
719
- }
720
-
721
- return {
722
- ...event,
723
- oldValue:
724
- event.oldValue === undefined ? undefined : SECURE_EVENT_REDACTED_VALUE,
725
- newValue:
726
- event.newValue === undefined ? undefined : SECURE_EVENT_REDACTED_VALUE,
727
- };
728
- }
729
-
730
- function eventForGlobalObserver(event: StorageChangeEvent): StorageChangeEvent {
731
- if (!eventObserverRedactSecureValues || event.scope !== StorageScope.Secure) {
732
- return event;
733
- }
734
-
735
- if (event.type === "key") {
736
- return redactSecureKeyChange(event);
737
- }
738
-
739
- return {
740
- ...event,
741
- changes: event.changes.map(redactSecureKeyChange),
742
- };
743
- }
744
-
745
- function emitKeyChange(
746
- scope: StorageScope,
747
- key: string,
748
- oldValue: string | undefined,
749
- newValue: string | undefined,
750
- operation: StorageChangeOperation,
751
- source: StorageChangeSource,
752
- ): void {
753
- const event = createKeyChange(
754
- scope,
755
- key,
756
- oldValue,
757
- newValue,
758
- operation,
759
- source,
760
- );
761
- storageEvents.emitKey(event);
762
- eventObserver?.(eventForGlobalObserver(event));
763
- }
764
-
765
- function emitBatchChange(
766
- scope: StorageScope,
767
- operation: StorageChangeOperation,
768
- source: StorageChangeSource,
769
- changes: StorageKeyChangeEvent[],
770
- ): void {
771
- if (changes.length === 0) {
772
- return;
773
- }
774
-
775
- const event: StorageBatchChangeEvent = {
776
- type: "batch",
777
- scope,
778
- operation,
779
- source,
780
- changes,
781
- };
782
- storageEvents.emitBatch(event);
783
- eventObserver?.(eventForGlobalObserver(event));
784
- }
785
-
786
- function readPendingSecureWrite(key: string): string | undefined {
787
- return pendingSecureWrites.get(key)?.value;
788
- }
789
-
790
- function readPendingDiskWrite(key: string): string | undefined {
791
- return pendingDiskWrites.get(key)?.value;
792
- }
793
-
794
- function hasPendingDiskWrite(key: string): boolean {
795
- return pendingDiskWrites.has(key);
796
- }
797
-
798
- function hasPendingSecureWrite(key: string): boolean {
799
- return pendingSecureWrites.has(key);
800
- }
801
-
802
- function clearPendingDiskWrite(key: string): void {
803
- pendingDiskWrites.delete(key);
804
- }
805
-
806
- function clearPendingSecureWrite(key: string): void {
807
- pendingSecureWrites.delete(key);
808
- }
809
-
810
- function flushDiskWrites(): void {
811
- diskFlushScheduled = false;
812
-
813
- if (pendingDiskWrites.size === 0) {
814
- return;
815
- }
816
-
817
- const writes = Array.from(pendingDiskWrites.values());
818
- pendingDiskWrites.clear();
819
-
820
- const keysToSet: string[] = [];
821
- const valuesToSet: string[] = [];
822
- const keysToRemove: string[] = [];
823
-
824
- writes.forEach(({ key, value }) => {
825
- if (value === undefined) {
826
- keysToRemove.push(key);
827
- return;
828
- }
829
-
830
- keysToSet.push(key);
831
- valuesToSet.push(value);
832
- });
833
-
834
- if (keysToSet.length > 0) {
835
- WebStorage.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
836
- }
837
- if (keysToRemove.length > 0) {
838
- WebStorage.removeBatch(keysToRemove, StorageScope.Disk);
839
- }
840
- }
841
-
842
- function flushSecureWrites(): void {
843
- secureFlushScheduled = false;
844
-
845
- if (pendingSecureWrites.size === 0) {
846
- return;
847
- }
848
-
849
- const writes = Array.from(pendingSecureWrites.values());
850
- pendingSecureWrites.clear();
851
-
852
- const groupedSetWrites = new Map<
853
- AccessControl,
854
- { keys: string[]; values: string[] }
855
- >();
856
- const keysToRemove: string[] = [];
857
-
858
- writes.forEach(({ key, value, accessControl }) => {
859
- if (value === undefined) {
860
- keysToRemove.push(key);
861
- } else {
862
- const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
863
- const existingGroup = groupedSetWrites.get(resolvedAccessControl);
864
- const group = existingGroup ?? { keys: [], values: [] };
865
- group.keys.push(key);
866
- group.values.push(value);
867
- if (!existingGroup) {
868
- groupedSetWrites.set(resolvedAccessControl, group);
869
- }
870
- }
871
- });
872
-
873
- groupedSetWrites.forEach((group, accessControl) => {
874
- WebStorage.setSecureAccessControl(accessControl);
875
- WebStorage.setBatch(group.keys, group.values, StorageScope.Secure);
876
- });
877
- if (keysToRemove.length > 0) {
878
- WebStorage.removeBatch(keysToRemove, StorageScope.Secure);
879
- }
880
- }
881
-
882
- function scheduleDiskWrite(key: string, value: string | undefined): void {
883
- pendingDiskWrites.set(key, { key, value });
884
- if (diskFlushScheduled) {
885
- return;
886
- }
887
- diskFlushScheduled = true;
888
- runMicrotask(flushDiskWrites);
889
- }
890
-
891
- function scheduleSecureWrite(
892
- key: string,
893
- value: string | undefined,
894
- accessControl?: AccessControl,
895
- ): void {
896
- const pendingWrite: PendingSecureWrite = { key, value };
897
- if (accessControl !== undefined) {
898
- pendingWrite.accessControl = accessControl;
899
- }
900
- pendingSecureWrites.set(key, pendingWrite);
901
- if (secureFlushScheduled) {
902
- return;
903
- }
904
- secureFlushScheduled = true;
905
- runMicrotask(flushSecureWrites);
906
- }
907
-
908
413
  const WebStorage: Storage = {
909
414
  name: "Storage",
910
415
  equals: (other) => other === WebStorage,
@@ -919,7 +424,7 @@ const WebStorage: Storage = {
919
424
  backend.setItem(storageKey, value);
920
425
  });
921
426
  ensureWebScopeKeyIndex(scope).add(key);
922
- notifyKeyListeners(getScopedListeners(scope), key);
427
+ notifyKeyListeners(internals.getScopedListeners(scope), key);
923
428
  },
924
429
  get: (key: string, scope: number) => {
925
430
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
@@ -954,7 +459,7 @@ const WebStorage: Storage = {
954
459
  });
955
460
  }
956
461
  ensureWebScopeKeyIndex(scope).delete(key);
957
- notifyKeyListeners(getScopedListeners(scope), key);
462
+ notifyKeyListeners(internals.getScopedListeners(scope), key);
958
463
  },
959
464
  clear: (scope: number) => {
960
465
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
@@ -964,7 +469,7 @@ const WebStorage: Storage = {
964
469
  backend.clear();
965
470
  });
966
471
  ensureWebScopeKeyIndex(scope).clear();
967
- notifyAllListeners(getScopedListeners(scope));
472
+ notifyAllListeners(internals.getScopedListeners(scope));
968
473
  },
969
474
  setBatch: (keys: string[], values: string[], scope: number) => {
970
475
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
@@ -1004,7 +509,7 @@ const WebStorage: Storage = {
1004
509
  : storageKey,
1005
510
  ),
1006
511
  );
1007
- const listeners = getScopedListeners(scope);
512
+ const listeners = internals.getScopedListeners(scope);
1008
513
  keys.forEach((key) => notifyKeyListeners(listeners, key));
1009
514
  },
1010
515
  getBatch: (keys: string[], scope: number) => {
@@ -1055,7 +560,7 @@ const WebStorage: Storage = {
1055
560
 
1056
561
  const keyIndex = ensureWebScopeKeyIndex(scope);
1057
562
  keys.forEach((key) => keyIndex.delete(key));
1058
- const listeners = getScopedListeners(scope);
563
+ const listeners = internals.getScopedListeners(scope);
1059
564
  keys.forEach((key) => notifyKeyListeners(listeners, key));
1060
565
  },
1061
566
  removeByPrefix: (prefix: string, scope: number) => {
@@ -1123,7 +628,10 @@ const WebStorage: Storage = {
1123
628
  backend.setItem(toSecureStorageKey(key), value);
1124
629
  });
1125
630
  ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
1126
- notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
631
+ notifyKeyListeners(
632
+ internals.getScopedListeners(StorageScope.Secure),
633
+ key,
634
+ );
1127
635
  return;
1128
636
  }
1129
637
  if (
@@ -1142,7 +650,7 @@ const WebStorage: Storage = {
1142
650
  (backend) => backend.setItem(toBiometricStorageKey(key), value),
1143
651
  );
1144
652
  ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
1145
- notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
653
+ notifyKeyListeners(internals.getScopedListeners(StorageScope.Secure), key);
1146
654
  },
1147
655
  getSecureBiometric: (key: string) => {
1148
656
  const value = withWebBackendOperation(
@@ -1167,7 +675,7 @@ const WebStorage: Storage = {
1167
675
  ) {
1168
676
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
1169
677
  }
1170
- notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
678
+ notifyKeyListeners(internals.getScopedListeners(StorageScope.Secure), key);
1171
679
  },
1172
680
  hasSecureBiometric: (key: string) => {
1173
681
  return (
@@ -1218,490 +726,55 @@ const WebStorage: Storage = {
1218
726
  keyIndex.delete(key);
1219
727
  }
1220
728
  });
1221
- const listeners = getScopedListeners(StorageScope.Secure);
729
+ const listeners = internals.getScopedListeners(StorageScope.Secure);
1222
730
  keysToNotify.forEach((key) => notifyKeyListeners(listeners, key));
1223
731
  },
1224
732
  };
1225
733
 
1226
- function getRawValue(key: string, scope: StorageScope): string | undefined {
1227
- assertValidScope(scope);
1228
- if (scope === StorageScope.Memory) {
1229
- const value = memoryStore.get(key);
1230
- return typeof value === "string" ? value : undefined;
1231
- }
1232
-
1233
- if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
1234
- return readPendingDiskWrite(key);
1235
- }
1236
-
1237
- if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
1238
- return readPendingSecureWrite(key);
1239
- }
1240
-
1241
- return WebStorage.get(key, scope);
1242
- }
1243
-
1244
- function setRawValue(key: string, value: string, scope: StorageScope): void {
1245
- assertValidScope(scope);
1246
- const oldValue =
1247
- scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
1248
- if (scope === StorageScope.Memory) {
1249
- memoryStore.set(key, value);
1250
- notifyKeyListeners(memoryListeners, key);
1251
- emitKeyChange(scope, key, oldValue, value, "set", "memory");
1252
- return;
1253
- }
1254
-
1255
- if (scope === StorageScope.Disk) {
1256
- cacheRawValue(scope, key, value);
1257
- if (diskWritesAsync) {
1258
- scheduleDiskWrite(key, value);
1259
- emitKeyChange(scope, key, oldValue, value, "set", "web");
1260
- return;
1261
- }
1262
-
1263
- flushDiskWrites();
1264
- clearPendingDiskWrite(key);
1265
- }
1266
-
1267
- if (scope === StorageScope.Secure) {
1268
- flushSecureWrites();
1269
- clearPendingSecureWrite(key);
1270
- }
1271
-
1272
- WebStorage.set(key, value, scope);
1273
- cacheRawValue(scope, key, value);
1274
- emitKeyChange(scope, key, oldValue, value, "set", "web");
1275
- }
1276
-
1277
- function removeRawValue(key: string, scope: StorageScope): void {
1278
- assertValidScope(scope);
1279
- const oldValue = getEventRawValue(scope, key);
1280
- if (scope === StorageScope.Memory) {
1281
- memoryStore.delete(key);
1282
- notifyKeyListeners(memoryListeners, key);
1283
- emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
1284
- return;
1285
- }
1286
-
1287
- if (scope === StorageScope.Disk) {
1288
- cacheRawValue(scope, key, undefined);
1289
- if (diskWritesAsync) {
1290
- scheduleDiskWrite(key, undefined);
1291
- emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
1292
- return;
1293
- }
1294
-
1295
- flushDiskWrites();
1296
- clearPendingDiskWrite(key);
1297
- }
1298
-
1299
- if (scope === StorageScope.Secure) {
1300
- flushSecureWrites();
1301
- clearPendingSecureWrite(key);
1302
- }
1303
-
1304
- WebStorage.remove(key, scope);
1305
- cacheRawValue(scope, key, undefined);
1306
- emitKeyChange(scope, key, oldValue, undefined, "remove", "web");
1307
- }
1308
-
1309
- function readMigrationVersion(scope: StorageScope): number {
1310
- const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
1311
- if (raw === undefined) {
1312
- return 0;
1313
- }
1314
-
1315
- const parsed = Number.parseInt(raw, 10);
1316
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
734
+ function buildWebAdapter(
735
+ coreInternals: StorageCoreInternals,
736
+ ): StorageCoreAdapter {
737
+ internals = coreInternals;
738
+ return {
739
+ backend: WebStorage,
740
+ changeSource: "web",
741
+ applyAccessControlOnSecureRawWrite: false,
742
+ flushDiskWritesOnImport: true,
743
+ ensureScopeSubscription: () => {
744
+ ensureExternalSyncSubscriptions();
745
+ },
746
+ maybeCleanupScopeSubscription: () => {},
747
+ onWillEmitChanges: () => {},
748
+ getSecureMetadataProfile: () => ({
749
+ backend: getBackendName(StorageScope.Secure, webSecureStorageBackend),
750
+ encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
751
+ hardwareBacked: "unavailable",
752
+ }),
753
+ };
1317
754
  }
1318
755
 
1319
- function writeMigrationVersion(scope: StorageScope, version: number): void {
1320
- setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
1321
- }
756
+ const core = createStorageCore(buildWebAdapter);
1322
757
 
1323
758
  export const storage = {
1324
- subscribe: (
1325
- scope: StorageScope,
1326
- listener: StorageEventListener,
1327
- ): (() => void) => {
1328
- assertValidScope(scope);
1329
- if (scope !== StorageScope.Memory) {
1330
- ensureExternalSyncSubscriptions();
1331
- }
1332
- return storageEvents.subscribe(scope, listener);
759
+ ...core.storage,
760
+ setAccessControl: (level: AccessControl) => {
761
+ assertAccessControlLevel(level);
762
+ internals.setSecureDefaultAccessControl(level);
763
+ internals.recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
1333
764
  },
1334
- subscribeKey: (
1335
- scope: StorageScope,
1336
- key: string,
1337
- listener: StorageEventListener,
1338
- ): (() => void) => {
1339
- assertValidScope(scope);
1340
- if (scope !== StorageScope.Memory) {
1341
- ensureExternalSyncSubscriptions();
1342
- }
1343
- return storageEvents.subscribeKey(scope, key, listener);
765
+ setSecureWritesAsync: (_enabled: boolean) => {
766
+ internals.recordMetric(
767
+ "storage:setSecureWritesAsync",
768
+ StorageScope.Secure,
769
+ 0,
770
+ );
1344
771
  },
1345
- subscribePrefix: (
1346
- scope: StorageScope,
1347
- prefix: string,
1348
- listener: StorageEventListener,
1349
- ): (() => void) => {
1350
- assertValidScope(scope);
1351
- if (scope !== StorageScope.Memory) {
1352
- ensureExternalSyncSubscriptions();
1353
- }
1354
- return storageEvents.subscribePrefix(scope, prefix, listener);
1355
- },
1356
- subscribeNamespace: (
1357
- namespace: string,
1358
- scope: StorageScope,
1359
- listener: StorageEventListener,
1360
- ): (() => void) => {
1361
- return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
1362
- },
1363
- setEventObserver: (
1364
- observer?: StorageEventListener,
1365
- options: StorageEventObserverOptions = {},
1366
- ) => {
1367
- eventObserver = observer;
1368
- eventObserverRedactSecureValues = options.redactSecureValues !== false;
1369
- if (observer) {
1370
- ensureExternalSyncSubscriptions();
1371
- }
1372
- },
1373
- clear: (scope: StorageScope) => {
1374
- measureOperation("storage:clear", scope, () => {
1375
- const previousValues = shouldReadPreviousEventValues(scope)
1376
- ? storage.getAll(scope)
1377
- : {};
1378
- if (scope === StorageScope.Memory) {
1379
- memoryStore.clear();
1380
- notifyAllListeners(memoryListeners);
1381
- emitBatchChange(
1382
- scope,
1383
- "clear",
1384
- "memory",
1385
- Object.keys(previousValues).map((key) =>
1386
- createKeyChange(
1387
- scope,
1388
- key,
1389
- previousValues[key],
1390
- undefined,
1391
- "clear",
1392
- "memory",
1393
- ),
1394
- ),
1395
- );
1396
- return;
1397
- }
1398
-
1399
- if (scope === StorageScope.Disk) {
1400
- flushDiskWrites();
1401
- pendingDiskWrites.clear();
1402
- }
1403
-
1404
- if (scope === StorageScope.Secure) {
1405
- flushSecureWrites();
1406
- pendingSecureWrites.clear();
1407
- }
1408
-
1409
- clearScopeRawCache(scope);
1410
- WebStorage.clear(scope);
1411
- emitBatchChange(
1412
- scope,
1413
- "clear",
1414
- "web",
1415
- Object.keys(previousValues).map((key) =>
1416
- createKeyChange(
1417
- scope,
1418
- key,
1419
- previousValues[key],
1420
- undefined,
1421
- "clear",
1422
- "web",
1423
- ),
1424
- ),
1425
- );
1426
- });
1427
- },
1428
- clearAll: () => {
1429
- measureOperation(
1430
- "storage:clearAll",
1431
- StorageScope.Memory,
1432
- () => {
1433
- storage.clear(StorageScope.Memory);
1434
- storage.clear(StorageScope.Disk);
1435
- storage.clear(StorageScope.Secure);
1436
- },
1437
- 3,
1438
- );
1439
- },
1440
- clearNamespace: (namespace: string, scope: StorageScope) => {
1441
- measureOperation("storage:clearNamespace", scope, () => {
1442
- assertValidScope(scope);
1443
- if (scope === StorageScope.Memory) {
1444
- const affectedKeys = Array.from(memoryStore.keys()).filter((key) =>
1445
- isNamespaced(key, namespace),
1446
- );
1447
- const previousValues = affectedKeys.map((key) => ({
1448
- key,
1449
- value: getEventRawValue(scope, key),
1450
- }));
1451
-
1452
- if (affectedKeys.length === 0) {
1453
- return;
1454
- }
1455
-
1456
- affectedKeys.forEach((key) => {
1457
- memoryStore.delete(key);
1458
- });
1459
- affectedKeys.forEach((key) => notifyKeyListeners(memoryListeners, key));
1460
- emitBatchChange(
1461
- scope,
1462
- "clearNamespace",
1463
- "memory",
1464
- previousValues.map(({ key, value }) =>
1465
- createKeyChange(
1466
- scope,
1467
- key,
1468
- value,
1469
- undefined,
1470
- "clearNamespace",
1471
- "memory",
1472
- ),
1473
- ),
1474
- );
1475
- return;
1476
- }
1477
-
1478
- const keyPrefix = prefixKey(namespace, "");
1479
- const previousValues = shouldReadPreviousEventValues(scope)
1480
- ? storage.getByPrefix(keyPrefix, scope)
1481
- : {};
1482
- if (scope === StorageScope.Disk) {
1483
- flushDiskWrites();
1484
- }
1485
- if (scope === StorageScope.Secure) {
1486
- flushSecureWrites();
1487
- }
1488
- const scopeCache = getScopeRawCache(scope);
1489
- for (const key of scopeCache.keys()) {
1490
- if (isNamespaced(key, namespace)) {
1491
- scopeCache.delete(key);
1492
- }
1493
- }
1494
- WebStorage.removeByPrefix(keyPrefix, scope);
1495
- emitBatchChange(
1496
- scope,
1497
- "clearNamespace",
1498
- "web",
1499
- Object.keys(previousValues).map((key) =>
1500
- createKeyChange(
1501
- scope,
1502
- key,
1503
- previousValues[key],
1504
- undefined,
1505
- "clearNamespace",
1506
- "web",
1507
- ),
1508
- ),
1509
- );
1510
- });
1511
- },
1512
- clearBiometric: () => {
1513
- measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
1514
- WebStorage.clearSecureBiometric();
1515
- });
1516
- },
1517
- has: (key: string, scope: StorageScope): boolean => {
1518
- return measureOperation("storage:has", scope, () => {
1519
- assertValidScope(scope);
1520
- if (scope === StorageScope.Memory) return memoryStore.has(key);
1521
- if (scope === StorageScope.Disk) {
1522
- flushDiskWrites();
1523
- }
1524
- if (scope === StorageScope.Secure) {
1525
- flushSecureWrites();
1526
- }
1527
- return WebStorage.has(key, scope);
1528
- });
1529
- },
1530
- getAllKeys: (scope: StorageScope): string[] => {
1531
- return measureOperation("storage:getAllKeys", scope, () => {
1532
- assertValidScope(scope);
1533
- if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
1534
- if (scope === StorageScope.Disk) {
1535
- flushDiskWrites();
1536
- }
1537
- if (scope === StorageScope.Secure) {
1538
- flushSecureWrites();
1539
- }
1540
- return WebStorage.getAllKeys(scope);
1541
- });
1542
- },
1543
- getKeysByPrefix: (prefix: string, scope: StorageScope): string[] => {
1544
- return measureOperation("storage:getKeysByPrefix", scope, () => {
1545
- assertValidScope(scope);
1546
- if (scope === StorageScope.Memory) {
1547
- return Array.from(memoryStore.keys()).filter((key) =>
1548
- key.startsWith(prefix),
1549
- );
1550
- }
1551
- if (scope === StorageScope.Disk) {
1552
- flushDiskWrites();
1553
- }
1554
- if (scope === StorageScope.Secure) {
1555
- flushSecureWrites();
1556
- }
1557
- return WebStorage.getKeysByPrefix(prefix, scope);
1558
- });
1559
- },
1560
- getByPrefix: (
1561
- prefix: string,
1562
- scope: StorageScope,
1563
- ): Record<string, string> => {
1564
- return measureOperation("storage:getByPrefix", scope, () => {
1565
- const result: Record<string, string> = {};
1566
- const keys = storage.getKeysByPrefix(prefix, scope);
1567
- if (keys.length === 0) {
1568
- return result;
1569
- }
1570
-
1571
- if (scope === StorageScope.Memory) {
1572
- keys.forEach((key) => {
1573
- const value = memoryStore.get(key);
1574
- if (typeof value === "string") {
1575
- result[key] = value;
1576
- }
1577
- });
1578
- return result;
1579
- }
1580
-
1581
- if (scope === StorageScope.Disk) {
1582
- flushDiskWrites();
1583
- }
1584
- if (scope === StorageScope.Secure) {
1585
- flushSecureWrites();
1586
- }
1587
- const values = WebStorage.getBatch(keys, scope);
1588
- keys.forEach((key, index) => {
1589
- const value = values[index];
1590
- if (value !== undefined) {
1591
- result[key] = value;
1592
- }
1593
- });
1594
- return result;
1595
- });
1596
- },
1597
- getAll: (scope: StorageScope): Record<string, string> => {
1598
- return measureOperation("storage:getAll", scope, () => {
1599
- assertValidScope(scope);
1600
- const result: Record<string, string> = {};
1601
- if (scope === StorageScope.Memory) {
1602
- memoryStore.forEach((value, key) => {
1603
- if (typeof value === "string") result[key] = value;
1604
- });
1605
- return result;
1606
- }
1607
- if (scope === StorageScope.Disk) {
1608
- flushDiskWrites();
1609
- }
1610
- if (scope === StorageScope.Secure) {
1611
- flushSecureWrites();
1612
- }
1613
- const keys = WebStorage.getAllKeys(scope);
1614
- if (keys.length === 0) return {};
1615
- const values = WebStorage.getBatch(keys, scope);
1616
- keys.forEach((key, index) => {
1617
- const val = values[index];
1618
- if (val !== undefined && val !== null) {
1619
- result[key] = val;
1620
- }
1621
- });
1622
- return result;
1623
- });
1624
- },
1625
- export: (
1626
- scope: StorageScope,
1627
- options: StorageExportOptions = {},
1628
- ): Record<string, string> => {
1629
- if (scope === StorageScope.Secure && options.includeSecureValues !== true) {
1630
- throw new Error(
1631
- "NitroStorage: exporting Secure scope exposes raw secret values. Pass { includeSecureValues: true } or use exportSecureUnsafe().",
1632
- );
1633
- }
1634
- return measureOperation("storage:export", scope, () =>
1635
- storage.getAll(scope),
1636
- );
1637
- },
1638
- exportSecureUnsafe: (): Record<string, string> => {
1639
- return measureOperation(
1640
- "storage:exportSecureUnsafe",
1641
- StorageScope.Secure,
1642
- () => storage.getAll(StorageScope.Secure),
1643
- );
1644
- },
1645
- size: (scope: StorageScope): number => {
1646
- return measureOperation("storage:size", scope, () => {
1647
- assertValidScope(scope);
1648
- if (scope === StorageScope.Memory) return memoryStore.size;
1649
- if (scope === StorageScope.Disk) {
1650
- flushDiskWrites();
1651
- }
1652
- if (scope === StorageScope.Secure) {
1653
- flushSecureWrites();
1654
- }
1655
- return WebStorage.size(scope);
1656
- });
1657
- },
1658
- setAccessControl: (level: AccessControl) => {
1659
- assertAccessControlLevel(level);
1660
- secureDefaultAccessControl = level;
1661
- recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
1662
- },
1663
- setSecureWritesAsync: (_enabled: boolean) => {
1664
- recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
1665
- },
1666
- setDiskWritesAsync: (enabled: boolean) => {
1667
- measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
1668
- diskWritesAsync = enabled;
1669
- if (!enabled) {
1670
- flushDiskWrites();
1671
- }
1672
- });
1673
- },
1674
- flushDiskWrites: () => {
1675
- measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
1676
- flushDiskWrites();
1677
- });
1678
- },
1679
- flushSecureWrites: () => {
1680
- measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
1681
- flushSecureWrites();
1682
- });
1683
- },
1684
- setKeychainAccessGroup: (_group: string) => {
1685
- recordMetric("storage:setKeychainAccessGroup", StorageScope.Secure, 0);
1686
- },
1687
- setMetricsObserver: (observer?: StorageMetricsObserver) => {
1688
- metricsObserver = observer;
1689
- },
1690
- getMetricsSnapshot: (): Record<string, StorageMetricSummary> => {
1691
- const snapshot: Record<string, StorageMetricSummary> = {};
1692
- metricsCounters.forEach((value, key) => {
1693
- snapshot[key] = {
1694
- count: value.count,
1695
- totalDurationMs: value.totalDurationMs,
1696
- avgDurationMs:
1697
- value.count === 0 ? 0 : value.totalDurationMs / value.count,
1698
- maxDurationMs: value.maxDurationMs,
1699
- };
1700
- });
1701
- return snapshot;
1702
- },
1703
- resetMetrics: () => {
1704
- metricsCounters.clear();
772
+ setKeychainAccessGroup: (_group: string) => {
773
+ internals.recordMetric(
774
+ "storage:setKeychainAccessGroup",
775
+ StorageScope.Secure,
776
+ 0,
777
+ );
1705
778
  },
1706
779
  getCapabilities: (): StorageCapabilities => ({
1707
780
  platform: "web",
@@ -1742,116 +815,27 @@ export const storage = {
1742
815
  },
1743
816
  };
1744
817
  },
1745
- getSecureMetadata: (key: string): SecureStorageMetadata => {
1746
- return measureOperation(
1747
- "storage:getSecureMetadata",
1748
- StorageScope.Secure,
1749
- () => {
1750
- flushSecureWrites();
1751
- const biometricProtected = WebStorage.hasSecureBiometric(key);
1752
- const exists =
1753
- biometricProtected || WebStorage.has(key, StorageScope.Secure);
1754
- let kind: SecureStorageMetadata["kind"] = "missing";
1755
- if (exists) {
1756
- kind = biometricProtected ? "biometric" : "secure";
1757
- }
1758
-
1759
- return {
1760
- key,
1761
- exists,
1762
- kind,
1763
- backend: getBackendName(StorageScope.Secure, webSecureStorageBackend),
1764
- encrypted: getWebSecureEncryptionStatus(webSecureStorageBackend),
1765
- hardwareBacked: "unavailable",
1766
- biometricProtected,
1767
- valueExposed: false,
1768
- };
1769
- },
1770
- );
1771
- },
1772
- getAllSecureMetadata: (): SecureStorageMetadata[] => {
1773
- return measureOperation(
1774
- "storage:getAllSecureMetadata",
1775
- StorageScope.Secure,
1776
- () => {
1777
- flushSecureWrites();
1778
- return WebStorage.getAllKeys(StorageScope.Secure).map((key) =>
1779
- storage.getSecureMetadata(key),
1780
- );
1781
- },
1782
- );
1783
- },
1784
- getString: (key: string, scope: StorageScope): string | undefined => {
1785
- return measureOperation("storage:getString", scope, () => {
1786
- return getRawValue(key, scope);
1787
- });
1788
- },
1789
- setString: (key: string, value: string, scope: StorageScope): void => {
1790
- measureOperation("storage:setString", scope, () => {
1791
- setRawValue(key, value, scope);
1792
- });
1793
- },
1794
- deleteString: (key: string, scope: StorageScope): void => {
1795
- measureOperation("storage:deleteString", scope, () => {
1796
- removeRawValue(key, scope);
1797
- });
1798
- },
1799
- import: (data: Record<string, string>, scope: StorageScope): void => {
1800
- const keys = Object.keys(data);
1801
- measureOperation(
1802
- "storage:import",
1803
- scope,
1804
- () => {
1805
- assertValidScope(scope);
1806
- if (keys.length === 0) return;
1807
- const values = keys.map((k) => data[k]!);
1808
- const changes = keys.map((key, index) =>
1809
- createKeyChange(
1810
- scope,
1811
- key,
1812
- getEventRawValue(scope, key),
1813
- values[index],
1814
- "import",
1815
- scope === StorageScope.Memory ? "memory" : "web",
1816
- ),
1817
- );
1818
-
1819
- if (scope === StorageScope.Memory) {
1820
- keys.forEach((key, index) => {
1821
- memoryStore.set(key, values[index]);
1822
- });
1823
- keys.forEach((key) => notifyKeyListeners(memoryListeners, key));
1824
- emitBatchChange(scope, "import", "memory", changes);
1825
- return;
1826
- }
1827
-
1828
- if (scope === StorageScope.Secure) {
1829
- flushSecureWrites();
1830
- WebStorage.setSecureAccessControl(secureDefaultAccessControl);
1831
- }
1832
- if (scope === StorageScope.Disk) {
1833
- flushDiskWrites();
1834
- }
1835
-
1836
- WebStorage.setBatch(keys, values, scope);
1837
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1838
- emitBatchChange(scope, "import", "web", changes);
1839
- },
1840
- keys.length,
1841
- );
1842
- },
1843
818
  };
1844
819
 
820
+ export const createStorageItem = core.createStorageItem;
821
+ export const getBatch = core.getBatch;
822
+ export const setBatch = core.setBatch;
823
+ export const removeBatch = core.removeBatch;
824
+ export const registerMigration = core.registerMigration;
825
+ export const migrateToLatest = core.migrateToLatest;
826
+ export const runTransaction = core.runTransaction;
827
+ export const createSecureAuthStorage = core.createSecureAuthStorage;
828
+
1845
829
  export function setWebSecureStorageBackend(
1846
830
  backend?: WebSecureStorageBackend,
1847
831
  ): void {
1848
832
  const previousBackend = webSecureStorageBackend;
1849
833
  const nextBackend = backend ?? createDefaultSecureBackend();
1850
- pendingSecureWrites.clear();
834
+ internals.clearAllPendingSecureWrites();
1851
835
  resetBackendChangeSubscription(StorageScope.Secure);
1852
836
  webSecureStorageBackend = nextBackend;
1853
837
  hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
1854
- clearScopeRawCache(StorageScope.Secure);
838
+ internals.clearScopeRawCache(StorageScope.Secure);
1855
839
  ensureExternalSyncSubscriptions();
1856
840
  if (previousBackend !== nextBackend) {
1857
841
  closeWebBackend(StorageScope.Secure, previousBackend);
@@ -1869,11 +853,11 @@ export function setWebDiskStorageBackend(
1869
853
  ): void {
1870
854
  const previousBackend = webDiskStorageBackend;
1871
855
  const nextBackend = backend ?? createDefaultDiskBackend();
1872
- pendingDiskWrites.clear();
856
+ internals.clearAllPendingDiskWrites();
1873
857
  resetBackendChangeSubscription(StorageScope.Disk);
1874
858
  webDiskStorageBackend = nextBackend;
1875
859
  hydratedWebScopeKeyIndex.delete(StorageScope.Disk);
1876
- clearScopeRawCache(StorageScope.Disk);
860
+ internals.clearScopeRawCache(StorageScope.Disk);
1877
861
  ensureExternalSyncSubscriptions();
1878
862
  if (previousBackend !== nextBackend) {
1879
863
  closeWebBackend(StorageScope.Disk, previousBackend);
@@ -1885,8 +869,8 @@ export function getWebDiskStorageBackend(): WebDiskStorageBackend | undefined {
1885
869
  }
1886
870
 
1887
871
  export async function flushWebStorageBackends(): Promise<void> {
1888
- flushDiskWrites();
1889
- flushSecureWrites();
872
+ internals.flushDiskWrites();
873
+ internals.flushSecureWrites();
1890
874
 
1891
875
  const flushes: Promise<void>[] = [];
1892
876
  const diskFlush = webDiskStorageBackend?.flush;
@@ -1902,1182 +886,5 @@ export async function flushWebStorageBackends(): Promise<void> {
1902
886
  await Promise.all(flushes);
1903
887
  }
1904
888
 
1905
- export interface StorageItemConfig<T> {
1906
- key: string;
1907
- scope: StorageScope;
1908
- defaultValue?: T;
1909
- serialize?: (value: T) => string;
1910
- deserialize?: (value: string) => T;
1911
- validate?: Validator<T>;
1912
- onValidationError?: (invalidValue: unknown) => T;
1913
- expiration?: ExpirationConfig;
1914
- onExpired?: (key: string) => void;
1915
- readCache?: boolean;
1916
- coalesceDiskWrites?: boolean;
1917
- coalesceSecureWrites?: boolean;
1918
- namespace?: string;
1919
- biometric?: boolean;
1920
- biometricLevel?: BiometricLevel;
1921
- accessControl?: AccessControl;
1922
- }
1923
-
1924
- export interface StorageItem<T> {
1925
- get: () => T;
1926
- getWithVersion: () => VersionedValue<T>;
1927
- set: (value: T | ((prev: T) => T)) => void;
1928
- setIfVersion: (
1929
- version: StorageVersion,
1930
- value: T | ((prev: T) => T),
1931
- ) => boolean;
1932
- delete: () => void;
1933
- has: () => boolean;
1934
- subscribe: (callback: () => void) => () => void;
1935
- subscribeSelector: <TSelected>(
1936
- selector: (value: T) => TSelected,
1937
- listener: StorageSelectorListener<TSelected>,
1938
- options?: StorageSelectorSubscribeOptions<TSelected>,
1939
- ) => () => void;
1940
- serialize: (value: T) => string;
1941
- deserialize: (value: string) => T;
1942
- scope: StorageScope;
1943
- key: string;
1944
- }
1945
-
1946
- type StorageItemInternal<T> = StorageItem<T> & {
1947
- _triggerListeners: () => void;
1948
- _invalidateParsedCacheOnly: () => void;
1949
- _hasValidation: boolean;
1950
- _hasExpiration: boolean;
1951
- _readCacheEnabled: boolean;
1952
- _isBiometric: boolean;
1953
- _biometricLevel: BiometricLevel;
1954
- _defaultValue: T;
1955
- _secureAccessControl?: AccessControl;
1956
- };
1957
-
1958
- function canUseRawBatchPath(item: RawBatchPathItem): boolean {
1959
- return (
1960
- item._hasExpiration === false &&
1961
- item._hasValidation === false &&
1962
- item._isBiometric !== true &&
1963
- item._secureAccessControl === undefined
1964
- );
1965
- }
1966
-
1967
- function canUseSecureRawBatchPath(item: RawBatchPathItem): boolean {
1968
- return (
1969
- item._hasExpiration === false &&
1970
- item._hasValidation === false &&
1971
- item._isBiometric !== true
1972
- );
1973
- }
1974
-
1975
- function defaultSerialize<T>(value: T): string {
1976
- return serializeWithPrimitiveFastPath(value);
1977
- }
1978
-
1979
- function defaultDeserialize<T>(value: string): T {
1980
- return deserializeWithPrimitiveFastPath(value);
1981
- }
1982
-
1983
- export function createStorageItem<T = undefined>(
1984
- config: StorageItemConfig<T>,
1985
- ): StorageItem<T> {
1986
- const storageKey = prefixKey(config.namespace, config.key);
1987
- const serialize = config.serialize ?? defaultSerialize;
1988
- const deserialize = config.deserialize ?? defaultDeserialize;
1989
- const isMemory = config.scope === StorageScope.Memory;
1990
- const resolvedBiometricLevel =
1991
- config.scope === StorageScope.Secure
1992
- ? (config.biometricLevel ??
1993
- (config.biometric === true
1994
- ? BiometricLevel.BiometryOnly
1995
- : BiometricLevel.None))
1996
- : BiometricLevel.None;
1997
- const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
1998
- const secureAccessControl = config.accessControl;
1999
- const validate = config.validate;
2000
- const onValidationError = config.onValidationError;
2001
- const expiration = config.expiration;
2002
- const onExpired = config.onExpired;
2003
- const expirationTtlMs = expiration?.ttlMs;
2004
- const memoryExpiration =
2005
- expiration && isMemory ? new Map<string, number>() : null;
2006
- const readCache = !isMemory && config.readCache === true;
2007
- const coalesceDiskWrites =
2008
- config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
2009
- const coalesceSecureWrites =
2010
- config.scope === StorageScope.Secure &&
2011
- config.coalesceSecureWrites === true &&
2012
- !isBiometric;
2013
- const defaultValue = config.defaultValue as T;
2014
- const nonMemoryScope: NonMemoryScope | null =
2015
- config.scope === StorageScope.Disk
2016
- ? StorageScope.Disk
2017
- : config.scope === StorageScope.Secure
2018
- ? StorageScope.Secure
2019
- : null;
2020
-
2021
- if (expiration && expiration.ttlMs <= 0) {
2022
- throw new Error("expiration.ttlMs must be greater than 0.");
2023
- }
2024
- if (config.scope === StorageScope.Secure) {
2025
- assertBiometricLevel(resolvedBiometricLevel);
2026
- if (secureAccessControl !== undefined) {
2027
- assertAccessControlLevel(secureAccessControl);
2028
- }
2029
- }
2030
-
2031
- const listeners = new Set<() => void>();
2032
- let unsubscribe: (() => void) | null = null;
2033
- let lastRaw: unknown = undefined;
2034
- let lastValue: T | undefined;
2035
- let hasLastValue = false;
2036
- let lastExpiresAt: number | null | undefined = undefined;
2037
-
2038
- const invalidateParsedCache = () => {
2039
- lastRaw = undefined;
2040
- lastValue = undefined;
2041
- hasLastValue = false;
2042
- lastExpiresAt = undefined;
2043
- };
2044
-
2045
- const ensureSubscription = () => {
2046
- if (unsubscribe) {
2047
- return;
2048
- }
2049
-
2050
- const listener = () => {
2051
- invalidateParsedCache();
2052
- listeners.forEach((callback) => callback());
2053
- };
2054
-
2055
- if (isMemory) {
2056
- unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
2057
- return;
2058
- }
2059
-
2060
- ensureExternalSyncSubscriptions();
2061
- unsubscribe = addKeyListener(
2062
- getScopedListeners(nonMemoryScope!),
2063
- storageKey,
2064
- listener,
2065
- );
2066
- };
2067
-
2068
- const readStoredRaw = (): unknown => {
2069
- if (isMemory) {
2070
- if (memoryExpiration) {
2071
- const expiresAt = memoryExpiration.get(storageKey);
2072
- if (expiresAt !== undefined && expiresAt <= Date.now()) {
2073
- memoryExpiration.delete(storageKey);
2074
- memoryStore.delete(storageKey);
2075
- notifyKeyListeners(memoryListeners, storageKey);
2076
- onExpired?.(storageKey);
2077
- return undefined;
2078
- }
2079
- }
2080
- return memoryStore.get(storageKey);
2081
- }
2082
-
2083
- if (nonMemoryScope === StorageScope.Disk) {
2084
- const pending = pendingDiskWrites.get(storageKey);
2085
- if (pending !== undefined) {
2086
- return pending.value;
2087
- }
2088
- }
2089
-
2090
- if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
2091
- const pending = pendingSecureWrites.get(storageKey);
2092
- if (pending !== undefined) {
2093
- return pending.value;
2094
- }
2095
- }
2096
-
2097
- if (readCache) {
2098
- const cache = getScopeRawCache(nonMemoryScope!);
2099
- const cached = cache.get(storageKey);
2100
- if (cached !== undefined || cache.has(storageKey)) {
2101
- return cached;
2102
- }
2103
- }
2104
-
2105
- if (isBiometric) {
2106
- return WebStorage.getSecureBiometric(storageKey);
2107
- }
2108
-
2109
- const raw = WebStorage.get(storageKey, config.scope);
2110
- cacheRawValue(nonMemoryScope!, storageKey, raw);
2111
- return raw;
2112
- };
2113
-
2114
- const writeStoredRaw = (rawValue: string): void => {
2115
- const oldValue =
2116
- config.scope === StorageScope.Memory
2117
- ? getEventRawValue(config.scope, storageKey)
2118
- : undefined;
2119
- if (isBiometric) {
2120
- WebStorage.setSecureBiometricWithLevel(
2121
- storageKey,
2122
- rawValue,
2123
- resolvedBiometricLevel,
2124
- );
2125
- emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
2126
- return;
2127
- }
2128
-
2129
- cacheRawValue(nonMemoryScope!, storageKey, rawValue);
2130
-
2131
- if (nonMemoryScope === StorageScope.Disk) {
2132
- if (coalesceDiskWrites || diskWritesAsync) {
2133
- scheduleDiskWrite(storageKey, rawValue);
2134
- emitKeyChange(
2135
- config.scope,
2136
- storageKey,
2137
- oldValue,
2138
- rawValue,
2139
- "set",
2140
- "web",
2141
- );
2142
- return;
2143
- }
2144
-
2145
- clearPendingDiskWrite(storageKey);
2146
- }
2147
-
2148
- if (coalesceSecureWrites) {
2149
- scheduleSecureWrite(
2150
- storageKey,
2151
- rawValue,
2152
- secureAccessControl ?? secureDefaultAccessControl,
2153
- );
2154
- emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
2155
- return;
2156
- }
2157
-
2158
- if (nonMemoryScope === StorageScope.Secure) {
2159
- clearPendingSecureWrite(storageKey);
2160
- }
2161
-
2162
- WebStorage.set(storageKey, rawValue, config.scope);
2163
- emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "web");
2164
- };
2165
-
2166
- const removeStoredRaw = (): void => {
2167
- const oldValue = getEventRawValue(config.scope, storageKey);
2168
- if (isBiometric) {
2169
- WebStorage.deleteSecureBiometric(storageKey);
2170
- emitKeyChange(
2171
- config.scope,
2172
- storageKey,
2173
- oldValue,
2174
- undefined,
2175
- "remove",
2176
- "web",
2177
- );
2178
- return;
2179
- }
2180
-
2181
- cacheRawValue(nonMemoryScope!, storageKey, undefined);
2182
-
2183
- if (nonMemoryScope === StorageScope.Disk) {
2184
- if (coalesceDiskWrites || diskWritesAsync) {
2185
- scheduleDiskWrite(storageKey, undefined);
2186
- emitKeyChange(
2187
- config.scope,
2188
- storageKey,
2189
- oldValue,
2190
- undefined,
2191
- "remove",
2192
- "web",
2193
- );
2194
- return;
2195
- }
2196
-
2197
- clearPendingDiskWrite(storageKey);
2198
- }
2199
-
2200
- if (coalesceSecureWrites) {
2201
- scheduleSecureWrite(
2202
- storageKey,
2203
- undefined,
2204
- secureAccessControl ?? secureDefaultAccessControl,
2205
- );
2206
- emitKeyChange(
2207
- config.scope,
2208
- storageKey,
2209
- oldValue,
2210
- undefined,
2211
- "remove",
2212
- "web",
2213
- );
2214
- return;
2215
- }
2216
-
2217
- if (nonMemoryScope === StorageScope.Secure) {
2218
- clearPendingSecureWrite(storageKey);
2219
- }
2220
-
2221
- WebStorage.remove(storageKey, config.scope);
2222
- emitKeyChange(
2223
- config.scope,
2224
- storageKey,
2225
- oldValue,
2226
- undefined,
2227
- "remove",
2228
- "web",
2229
- );
2230
- };
2231
-
2232
- const writeValueWithoutValidation = (value: T): void => {
2233
- if (isMemory) {
2234
- const oldValue = getEventRawValue(config.scope, storageKey);
2235
- if (memoryExpiration) {
2236
- memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
2237
- }
2238
- memoryStore.set(storageKey, value);
2239
- notifyKeyListeners(memoryListeners, storageKey);
2240
- emitKeyChange(
2241
- config.scope,
2242
- storageKey,
2243
- oldValue,
2244
- typeof value === "string" ? value : undefined,
2245
- "set",
2246
- "memory",
2247
- );
2248
- return;
2249
- }
2250
-
2251
- const serialized = serialize(value);
2252
- if (expiration) {
2253
- const envelope: StoredEnvelope = {
2254
- __nitroStorageEnvelope: true,
2255
- expiresAt: Date.now() + expiration.ttlMs,
2256
- payload: serialized,
2257
- };
2258
- writeStoredRaw(JSON.stringify(envelope));
2259
- return;
2260
- }
2261
-
2262
- writeStoredRaw(serialized);
2263
- };
2264
-
2265
- const resolveInvalidValue = (invalidValue: unknown): T => {
2266
- if (onValidationError) {
2267
- return onValidationError(invalidValue);
2268
- }
2269
-
2270
- return defaultValue;
2271
- };
2272
-
2273
- const ensureValidatedValue = (
2274
- candidate: unknown,
2275
- hadStoredValue: boolean,
2276
- ): T => {
2277
- if (!validate || validate(candidate)) {
2278
- return candidate as T;
2279
- }
2280
-
2281
- const resolved = resolveInvalidValue(candidate);
2282
- if (validate && !validate(resolved)) {
2283
- return defaultValue;
2284
- }
2285
- if (hadStoredValue) {
2286
- writeValueWithoutValidation(resolved);
2287
- }
2288
- return resolved;
2289
- };
2290
-
2291
- const getInternal = (): T => {
2292
- const raw = readStoredRaw();
2293
-
2294
- if (!memoryExpiration && raw === lastRaw && hasLastValue) {
2295
- if (!expiration || lastExpiresAt === null) {
2296
- return lastValue as T;
2297
- }
2298
-
2299
- if (typeof lastExpiresAt === "number") {
2300
- if (lastExpiresAt > Date.now()) {
2301
- return lastValue as T;
2302
- }
2303
-
2304
- removeStoredRaw();
2305
- invalidateParsedCache();
2306
- onExpired?.(storageKey);
2307
- lastValue = ensureValidatedValue(defaultValue, false);
2308
- hasLastValue = true;
2309
- listeners.forEach((cb) => cb());
2310
- return lastValue;
2311
- }
2312
- }
2313
-
2314
- lastRaw = raw;
2315
-
2316
- if (raw === undefined) {
2317
- lastExpiresAt = undefined;
2318
- lastValue = ensureValidatedValue(defaultValue, false);
2319
- hasLastValue = true;
2320
- return lastValue;
2321
- }
2322
-
2323
- if (isMemory) {
2324
- lastExpiresAt = undefined;
2325
- lastValue = ensureValidatedValue(raw, true);
2326
- hasLastValue = true;
2327
- return lastValue;
2328
- }
2329
-
2330
- if (typeof raw !== "string") {
2331
- lastExpiresAt = undefined;
2332
- lastValue = ensureValidatedValue(defaultValue, false);
2333
- hasLastValue = true;
2334
- return lastValue;
2335
- }
2336
-
2337
- let deserializableRaw = raw;
2338
-
2339
- if (expiration) {
2340
- let envelopeExpiresAt: number | null = null;
2341
- try {
2342
- const parsed = JSON.parse(raw) as unknown;
2343
- if (isStoredEnvelope(parsed)) {
2344
- envelopeExpiresAt = parsed.expiresAt;
2345
- if (parsed.expiresAt <= Date.now()) {
2346
- removeStoredRaw();
2347
- invalidateParsedCache();
2348
- onExpired?.(storageKey);
2349
- lastValue = ensureValidatedValue(defaultValue, false);
2350
- hasLastValue = true;
2351
- listeners.forEach((cb) => cb());
2352
- return lastValue;
2353
- }
2354
-
2355
- deserializableRaw = parsed.payload;
2356
- }
2357
- } catch {
2358
- // Keep backward compatibility with legacy raw values.
2359
- }
2360
- lastExpiresAt = envelopeExpiresAt;
2361
- } else {
2362
- lastExpiresAt = undefined;
2363
- }
2364
-
2365
- lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
2366
- hasLastValue = true;
2367
- return lastValue;
2368
- };
2369
-
2370
- const getCurrentVersion = (): StorageVersion => {
2371
- const raw = readStoredRaw();
2372
- return toVersionToken(raw);
2373
- };
2374
-
2375
- const get = (): T =>
2376
- measureOperation("item:get", config.scope, () => getInternal());
2377
-
2378
- const getWithVersion = (): VersionedValue<T> =>
2379
- measureOperation("item:getWithVersion", config.scope, () => ({
2380
- value: getInternal(),
2381
- version: getCurrentVersion(),
2382
- }));
2383
-
2384
- const set = (valueOrFn: T | ((prev: T) => T)): void => {
2385
- measureOperation("item:set", config.scope, () => {
2386
- const newValue = isUpdater(valueOrFn)
2387
- ? valueOrFn(getInternal())
2388
- : valueOrFn;
2389
-
2390
- if (validate && !validate(newValue)) {
2391
- throw new Error(
2392
- `Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
2393
- );
2394
- }
2395
-
2396
- invalidateParsedCache();
2397
- writeValueWithoutValidation(newValue);
2398
- });
2399
- };
2400
-
2401
- const setIfVersion = (
2402
- version: StorageVersion,
2403
- valueOrFn: T | ((prev: T) => T),
2404
- ): boolean =>
2405
- measureOperation("item:setIfVersion", config.scope, () => {
2406
- const currentVersion = getCurrentVersion();
2407
- if (currentVersion !== version) {
2408
- return false;
2409
- }
2410
- set(valueOrFn);
2411
- return true;
2412
- });
2413
-
2414
- const deleteItem = (): void => {
2415
- measureOperation("item:delete", config.scope, () => {
2416
- invalidateParsedCache();
2417
-
2418
- if (isMemory) {
2419
- const oldValue = getEventRawValue(config.scope, storageKey);
2420
- if (memoryExpiration) {
2421
- memoryExpiration.delete(storageKey);
2422
- }
2423
- memoryStore.delete(storageKey);
2424
- notifyKeyListeners(memoryListeners, storageKey);
2425
- emitKeyChange(
2426
- config.scope,
2427
- storageKey,
2428
- oldValue,
2429
- undefined,
2430
- "remove",
2431
- "memory",
2432
- );
2433
- return;
2434
- }
2435
-
2436
- removeStoredRaw();
2437
- });
2438
- };
2439
-
2440
- const hasItem = (): boolean =>
2441
- measureOperation("item:has", config.scope, () => {
2442
- if (isMemory) return memoryStore.has(storageKey);
2443
- if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
2444
- if (nonMemoryScope === StorageScope.Disk) {
2445
- const pending = pendingDiskWrites.get(storageKey);
2446
- if (pending !== undefined) {
2447
- return pending.value !== undefined;
2448
- }
2449
- }
2450
- if (nonMemoryScope === StorageScope.Secure) {
2451
- const pending = pendingSecureWrites.get(storageKey);
2452
- if (pending !== undefined) {
2453
- return pending.value !== undefined;
2454
- }
2455
- }
2456
- return WebStorage.has(storageKey, config.scope);
2457
- });
2458
-
2459
- const subscribe = (callback: () => void): (() => void) => {
2460
- ensureSubscription();
2461
- listeners.add(callback);
2462
- return () => {
2463
- listeners.delete(callback);
2464
- if (listeners.size === 0 && unsubscribe) {
2465
- unsubscribe();
2466
- unsubscribe = null;
2467
- }
2468
- };
2469
- };
2470
-
2471
- const subscribeSelector = <TSelected>(
2472
- selector: (value: T) => TSelected,
2473
- listener: StorageSelectorListener<TSelected>,
2474
- options: StorageSelectorSubscribeOptions<TSelected> = {},
2475
- ): (() => void) => {
2476
- const isEqual = options.isEqual ?? Object.is;
2477
- let currentValue = selector(getInternal());
2478
-
2479
- if (options.fireImmediately === true) {
2480
- listener(currentValue, currentValue);
2481
- }
2482
-
2483
- return subscribe(() => {
2484
- const nextValue = selector(getInternal());
2485
- if (isEqual(currentValue, nextValue)) {
2486
- return;
2487
- }
2488
-
2489
- const previousValue = currentValue;
2490
- currentValue = nextValue;
2491
- listener(nextValue, previousValue);
2492
- });
2493
- };
2494
-
2495
- const storageItem: StorageItemInternal<T> = {
2496
- get,
2497
- getWithVersion,
2498
- set,
2499
- setIfVersion,
2500
- delete: deleteItem,
2501
- has: hasItem,
2502
- subscribe,
2503
- subscribeSelector,
2504
- serialize,
2505
- deserialize,
2506
- _triggerListeners: () => {
2507
- invalidateParsedCache();
2508
- listeners.forEach((listener) => listener());
2509
- },
2510
- _invalidateParsedCacheOnly: () => {
2511
- invalidateParsedCache();
2512
- },
2513
- _hasValidation: validate !== undefined,
2514
- _hasExpiration: expiration !== undefined,
2515
- _readCacheEnabled: readCache,
2516
- _isBiometric: isBiometric,
2517
- _biometricLevel: resolvedBiometricLevel,
2518
- _defaultValue: defaultValue,
2519
- ...(secureAccessControl !== undefined
2520
- ? { _secureAccessControl: secureAccessControl }
2521
- : {}),
2522
- scope: config.scope,
2523
- key: storageKey,
2524
- };
2525
-
2526
- return storageItem;
2527
- }
2528
-
2529
889
  export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
2530
890
  export { createIndexedDBBackend } from "./indexeddb-backend";
2531
-
2532
- type BatchReadItem<T> = Pick<
2533
- StorageItem<T>,
2534
- "key" | "scope" | "get" | "deserialize"
2535
- > & {
2536
- _hasValidation?: boolean;
2537
- _hasExpiration?: boolean;
2538
- _readCacheEnabled?: boolean;
2539
- _isBiometric?: boolean;
2540
- _defaultValue?: unknown;
2541
- _secureAccessControl?: AccessControl;
2542
- };
2543
- type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
2544
-
2545
- export type StorageBatchSetItem<T> = {
2546
- item: StorageItem<T>;
2547
- value: T;
2548
- };
2549
-
2550
- export function getBatch(
2551
- items: readonly BatchReadItem<unknown>[],
2552
- scope: StorageScope,
2553
- ): unknown[] {
2554
- return measureOperation(
2555
- "batch:get",
2556
- scope,
2557
- () => {
2558
- assertBatchScope(items, scope);
2559
-
2560
- if (scope === StorageScope.Memory) {
2561
- return items.map((item) => item.get());
2562
- }
2563
-
2564
- const useRawBatchPath = items.every((item) =>
2565
- scope === StorageScope.Secure
2566
- ? canUseSecureRawBatchPath(item)
2567
- : canUseRawBatchPath(item),
2568
- );
2569
- if (!useRawBatchPath) {
2570
- return items.map((item) => item.get());
2571
- }
2572
-
2573
- const rawValues = new Array<string | undefined>(items.length);
2574
- const keysToFetch: string[] = [];
2575
- const keyIndexes: number[] = [];
2576
-
2577
- items.forEach((item, index) => {
2578
- if (scope === StorageScope.Disk) {
2579
- const pending = pendingDiskWrites.get(item.key);
2580
- if (pending !== undefined) {
2581
- rawValues[index] = pending.value;
2582
- return;
2583
- }
2584
- }
2585
-
2586
- if (scope === StorageScope.Secure) {
2587
- const pending = pendingSecureWrites.get(item.key);
2588
- if (pending !== undefined) {
2589
- rawValues[index] = pending.value;
2590
- return;
2591
- }
2592
- }
2593
-
2594
- if (item._readCacheEnabled === true) {
2595
- const cache = getScopeRawCache(scope);
2596
- const cached = cache.get(item.key);
2597
- if (cached !== undefined || cache.has(item.key)) {
2598
- rawValues[index] = cached;
2599
- return;
2600
- }
2601
- }
2602
-
2603
- keysToFetch.push(item.key);
2604
- keyIndexes.push(index);
2605
- });
2606
-
2607
- if (keysToFetch.length > 0) {
2608
- const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
2609
- fetchedValues.forEach((value, index) => {
2610
- const key = keysToFetch[index];
2611
- const targetIndex = keyIndexes[index];
2612
- if (key === undefined || targetIndex === undefined) {
2613
- return;
2614
- }
2615
- rawValues[targetIndex] = value;
2616
- cacheRawValue(scope, key, value);
2617
- });
2618
- }
2619
-
2620
- return items.map((item, index) => {
2621
- const raw = rawValues[index];
2622
- if (raw === undefined) {
2623
- return asInternal(item as StorageItem<unknown>)._defaultValue;
2624
- }
2625
- return item.deserialize(raw);
2626
- });
2627
- },
2628
- items.length,
2629
- );
2630
- }
2631
-
2632
- export function setBatch<T>(
2633
- items: readonly StorageBatchSetItem<T>[],
2634
- scope: StorageScope,
2635
- ): void {
2636
- measureOperation(
2637
- "batch:set",
2638
- scope,
2639
- () => {
2640
- assertBatchScope(
2641
- items.map((batchEntry) => batchEntry.item),
2642
- scope,
2643
- );
2644
-
2645
- if (scope === StorageScope.Memory) {
2646
- // Determine if any item needs per-item handling (validation or TTL)
2647
- const needsIndividualSets = items.some(({ item }) => {
2648
- const internal = asInternal(item as StorageItem<unknown>);
2649
- return internal._hasValidation || internal._hasExpiration;
2650
- });
2651
-
2652
- if (needsIndividualSets) {
2653
- items.forEach(({ item, value }) => item.set(value));
2654
- return;
2655
- }
2656
-
2657
- const changes = items.map(({ item, value }) =>
2658
- createKeyChange(
2659
- scope,
2660
- item.key,
2661
- getEventRawValue(scope, item.key),
2662
- typeof value === "string" ? value : undefined,
2663
- "setBatch",
2664
- "memory",
2665
- ),
2666
- );
2667
-
2668
- // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
2669
- items.forEach(({ item, value }) => {
2670
- memoryStore.set(item.key, value);
2671
- asInternal(item as StorageItem<unknown>)._invalidateParsedCacheOnly();
2672
- });
2673
- items.forEach(({ item }) =>
2674
- notifyKeyListeners(memoryListeners, item.key),
2675
- );
2676
- emitBatchChange(scope, "setBatch", "memory", changes);
2677
- return;
2678
- }
2679
-
2680
- if (scope === StorageScope.Secure) {
2681
- const secureEntries = items.map(({ item, value }) => ({
2682
- item,
2683
- value,
2684
- internal: asInternal(item),
2685
- }));
2686
- const canUseSecureBatchPath = secureEntries.every(({ internal }) =>
2687
- canUseSecureRawBatchPath(internal),
2688
- );
2689
- if (!canUseSecureBatchPath) {
2690
- items.forEach(({ item, value }) => item.set(value));
2691
- return;
2692
- }
2693
-
2694
- flushSecureWrites();
2695
- const keys = secureEntries.map(({ item }) => item.key);
2696
- const oldValues = shouldReadPreviousEventValues(scope)
2697
- ? WebStorage.getBatch(keys, scope)
2698
- : [];
2699
- const groupedByAccessControl = new Map<
2700
- number,
2701
- { keys: string[]; values: string[] }
2702
- >();
2703
-
2704
- secureEntries.forEach(({ item, value, internal }) => {
2705
- const accessControl =
2706
- internal._secureAccessControl ?? secureDefaultAccessControl;
2707
- const existingGroup = groupedByAccessControl.get(accessControl);
2708
- const group = existingGroup ?? { keys: [], values: [] };
2709
- group.keys.push(item.key);
2710
- group.values.push(item.serialize(value));
2711
- if (!existingGroup) {
2712
- groupedByAccessControl.set(accessControl, group);
2713
- }
2714
- });
2715
-
2716
- groupedByAccessControl.forEach((group, accessControl) => {
2717
- WebStorage.setSecureAccessControl(accessControl);
2718
- WebStorage.setBatch(group.keys, group.values, scope);
2719
- group.keys.forEach((key, index) =>
2720
- cacheRawValue(scope, key, group.values[index]),
2721
- );
2722
- });
2723
- emitBatchChange(
2724
- scope,
2725
- "setBatch",
2726
- "web",
2727
- secureEntries.map(({ item, value }, index) =>
2728
- createKeyChange(
2729
- scope,
2730
- item.key,
2731
- oldValues[index],
2732
- item.serialize(value),
2733
- "setBatch",
2734
- "web",
2735
- ),
2736
- ),
2737
- );
2738
- return;
2739
- }
2740
-
2741
- flushDiskWrites();
2742
-
2743
- const useRawBatchPath = items.every(({ item }) =>
2744
- canUseRawBatchPath(asInternal(item)),
2745
- );
2746
- if (!useRawBatchPath) {
2747
- items.forEach(({ item, value }) => item.set(value));
2748
- return;
2749
- }
2750
-
2751
- const keys = items.map((entry) => entry.item.key);
2752
- const values = items.map((entry) => entry.item.serialize(entry.value));
2753
- const oldValues = shouldReadPreviousEventValues(scope)
2754
- ? WebStorage.getBatch(keys, scope)
2755
- : [];
2756
- WebStorage.setBatch(keys, values, scope);
2757
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
2758
- emitBatchChange(
2759
- scope,
2760
- "setBatch",
2761
- "web",
2762
- keys.map((key, index) =>
2763
- createKeyChange(
2764
- scope,
2765
- key,
2766
- oldValues[index],
2767
- values[index],
2768
- "setBatch",
2769
- "web",
2770
- ),
2771
- ),
2772
- );
2773
- },
2774
- items.length,
2775
- );
2776
- }
2777
-
2778
- export function removeBatch(
2779
- items: readonly BatchRemoveItem[],
2780
- scope: StorageScope,
2781
- ): void {
2782
- measureOperation(
2783
- "batch:remove",
2784
- scope,
2785
- () => {
2786
- assertBatchScope(items, scope);
2787
-
2788
- if (scope === StorageScope.Memory) {
2789
- const changes = items.map((item) =>
2790
- createKeyChange(
2791
- scope,
2792
- item.key,
2793
- getEventRawValue(scope, item.key),
2794
- undefined,
2795
- "removeBatch",
2796
- "memory",
2797
- ),
2798
- );
2799
- items.forEach((item) => item.delete());
2800
- emitBatchChange(scope, "removeBatch", "memory", changes);
2801
- return;
2802
- }
2803
-
2804
- const keys = items.map((item) => item.key);
2805
- if (scope === StorageScope.Disk) {
2806
- flushDiskWrites();
2807
- }
2808
- if (scope === StorageScope.Secure) {
2809
- flushSecureWrites();
2810
- }
2811
- const oldValues = shouldReadPreviousEventValues(scope)
2812
- ? WebStorage.getBatch(keys, scope)
2813
- : [];
2814
- WebStorage.removeBatch(keys, scope);
2815
- keys.forEach((key) => cacheRawValue(scope, key, undefined));
2816
- emitBatchChange(
2817
- scope,
2818
- "removeBatch",
2819
- "web",
2820
- keys.map((key, index) =>
2821
- createKeyChange(
2822
- scope,
2823
- key,
2824
- oldValues[index],
2825
- undefined,
2826
- "removeBatch",
2827
- "web",
2828
- ),
2829
- ),
2830
- );
2831
- },
2832
- items.length,
2833
- );
2834
- }
2835
-
2836
- export function registerMigration(version: number, migration: Migration): void {
2837
- if (!Number.isInteger(version) || version <= 0) {
2838
- throw new Error("Migration version must be a positive integer.");
2839
- }
2840
-
2841
- if (registeredMigrations.has(version)) {
2842
- throw new Error(`Migration version ${version} is already registered.`);
2843
- }
2844
-
2845
- registeredMigrations.set(version, migration);
2846
- }
2847
-
2848
- export function migrateToLatest(
2849
- scope: StorageScope = StorageScope.Disk,
2850
- ): number {
2851
- return measureOperation("migration:run", scope, () => {
2852
- assertValidScope(scope);
2853
- const currentVersion = readMigrationVersion(scope);
2854
- const versions = Array.from(registeredMigrations.keys())
2855
- .filter((version) => version > currentVersion)
2856
- .sort((a, b) => a - b);
2857
-
2858
- let appliedVersion = currentVersion;
2859
- const context: MigrationContext = {
2860
- scope,
2861
- getRaw: (key) => getRawValue(key, scope),
2862
- setRaw: (key, value) => setRawValue(key, value, scope),
2863
- removeRaw: (key) => removeRawValue(key, scope),
2864
- };
2865
-
2866
- versions.forEach((version) => {
2867
- const migration = registeredMigrations.get(version);
2868
- if (!migration) {
2869
- return;
2870
- }
2871
- migration(context);
2872
- appliedVersion = version;
2873
- });
2874
-
2875
- if (appliedVersion !== currentVersion) {
2876
- writeMigrationVersion(scope, appliedVersion);
2877
- }
2878
-
2879
- return appliedVersion;
2880
- });
2881
- }
2882
-
2883
- export function runTransaction<T>(
2884
- scope: StorageScope,
2885
- transaction: (context: TransactionContext) => T,
2886
- ): T {
2887
- return measureOperation("transaction:run", scope, () => {
2888
- assertValidScope(scope);
2889
- if (scope === StorageScope.Disk) {
2890
- flushDiskWrites();
2891
- }
2892
- if (scope === StorageScope.Secure) {
2893
- flushSecureWrites();
2894
- }
2895
-
2896
- const NOT_SET = Symbol();
2897
- const rollback = new Map<string, RollbackRecord>();
2898
-
2899
- const rememberRollback = (
2900
- key: string,
2901
- item?: Pick<StorageItem<unknown>, "key" | "scope">,
2902
- ) => {
2903
- if (rollback.has(key)) {
2904
- return;
2905
- }
2906
- if (scope === StorageScope.Memory) {
2907
- rollback.set(key, {
2908
- kind: "memory",
2909
- value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET,
2910
- });
2911
- } else {
2912
- const internal = item
2913
- ? (item as StorageItemInternal<unknown>)
2914
- : undefined;
2915
- if (scope === StorageScope.Secure && internal?._isBiometric === true) {
2916
- rollback.set(key, {
2917
- kind: "biometric",
2918
- value: WebStorage.getSecureBiometric(key),
2919
- level: internal._biometricLevel,
2920
- });
2921
- return;
2922
- }
2923
- rollback.set(key, {
2924
- kind: "raw",
2925
- value: getRawValue(key, scope),
2926
- ...(scope === StorageScope.Secure &&
2927
- internal?._secureAccessControl !== undefined
2928
- ? { accessControl: internal._secureAccessControl }
2929
- : {}),
2930
- });
2931
- }
2932
- };
2933
-
2934
- const tx: TransactionContext = {
2935
- scope,
2936
- getRaw: (key) => getRawValue(key, scope),
2937
- setRaw: (key, value) => {
2938
- rememberRollback(key);
2939
- setRawValue(key, value, scope);
2940
- },
2941
- removeRaw: (key) => {
2942
- rememberRollback(key);
2943
- removeRawValue(key, scope);
2944
- },
2945
- getItem: (item) => {
2946
- assertBatchScope([item], scope);
2947
- return item.get();
2948
- },
2949
- setItem: (item, value) => {
2950
- assertBatchScope([item], scope);
2951
- rememberRollback(item.key, item);
2952
- item.set(value);
2953
- },
2954
- removeItem: (item) => {
2955
- assertBatchScope([item], scope);
2956
- rememberRollback(item.key, item);
2957
- item.delete();
2958
- },
2959
- };
2960
-
2961
- try {
2962
- return transaction(tx);
2963
- } catch (error) {
2964
- const rollbackEntries = Array.from(rollback.entries()).reverse();
2965
- if (scope === StorageScope.Memory) {
2966
- rollbackEntries.forEach(([key, record]) => {
2967
- if (record.value === NOT_SET) {
2968
- memoryStore.delete(key);
2969
- } else {
2970
- memoryStore.set(key, record.value);
2971
- }
2972
- notifyKeyListeners(memoryListeners, key);
2973
- });
2974
- } else {
2975
- const groupedKeysToSet = new Map<
2976
- AccessControl,
2977
- { keys: string[]; values: string[] }
2978
- >();
2979
- const keysToRemove: string[] = [];
2980
-
2981
- rollbackEntries.forEach(([key, record]) => {
2982
- if (record.kind === "biometric") {
2983
- if (record.value === undefined) {
2984
- WebStorage.deleteSecureBiometric(key);
2985
- } else {
2986
- WebStorage.setSecureBiometricWithLevel(
2987
- key,
2988
- record.value,
2989
- record.level,
2990
- );
2991
- }
2992
- return;
2993
- }
2994
- if (record.kind !== "raw") {
2995
- return;
2996
- }
2997
- if (record.value === undefined) {
2998
- keysToRemove.push(key);
2999
- } else {
3000
- const accessControl =
3001
- record.accessControl ?? secureDefaultAccessControl;
3002
- const existingGroup = groupedKeysToSet.get(accessControl);
3003
- const group = existingGroup ?? { keys: [], values: [] };
3004
- group.keys.push(key);
3005
- group.values.push(record.value);
3006
- if (!existingGroup) {
3007
- groupedKeysToSet.set(accessControl, group);
3008
- }
3009
- }
3010
- });
3011
-
3012
- if (scope === StorageScope.Disk) {
3013
- flushDiskWrites();
3014
- }
3015
- if (scope === StorageScope.Secure) {
3016
- flushSecureWrites();
3017
- }
3018
- groupedKeysToSet.forEach((group, accessControl) => {
3019
- if (scope === StorageScope.Secure) {
3020
- WebStorage.setSecureAccessControl(accessControl);
3021
- }
3022
- WebStorage.setBatch(group.keys, group.values, scope);
3023
- group.keys.forEach((key, index) =>
3024
- cacheRawValue(scope, key, group.values[index]),
3025
- );
3026
- });
3027
- if (keysToRemove.length > 0) {
3028
- WebStorage.removeBatch(keysToRemove, scope);
3029
- keysToRemove.forEach((key) => cacheRawValue(scope, key, undefined));
3030
- }
3031
- }
3032
- throw error;
3033
- }
3034
- });
3035
- }
3036
-
3037
- export type SecureAuthStorageConfig<K extends string = string> = Record<
3038
- K,
3039
- {
3040
- ttlMs?: number;
3041
- biometric?: boolean;
3042
- biometricLevel?: BiometricLevel;
3043
- accessControl?: AccessControl;
3044
- }
3045
- >;
3046
-
3047
- export function createSecureAuthStorage<K extends string>(
3048
- config: SecureAuthStorageConfig<K>,
3049
- options?: { namespace?: string },
3050
- ): Record<K, StorageItem<string>> {
3051
- const ns = options?.namespace ?? "auth";
3052
- const result: Partial<Record<K, StorageItem<string>>> = {};
3053
-
3054
- for (const key of typedKeys(config)) {
3055
- const itemConfig = config[key];
3056
- const expirationConfig =
3057
- itemConfig.ttlMs !== undefined ? { ttlMs: itemConfig.ttlMs } : undefined;
3058
- result[key] = createStorageItem<string>({
3059
- key,
3060
- scope: StorageScope.Secure,
3061
- defaultValue: "",
3062
- namespace: ns,
3063
- ...(itemConfig.biometric !== undefined
3064
- ? { biometric: itemConfig.biometric }
3065
- : {}),
3066
- ...(itemConfig.biometricLevel !== undefined
3067
- ? { biometricLevel: itemConfig.biometricLevel }
3068
- : {}),
3069
- ...(itemConfig.accessControl !== undefined
3070
- ? { accessControl: itemConfig.accessControl }
3071
- : {}),
3072
- ...(expirationConfig !== undefined
3073
- ? { expiration: expirationConfig }
3074
- : {}),
3075
- });
3076
- }
3077
-
3078
- return result as Record<K, StorageItem<string>>;
3079
- }
3080
-
3081
- export function isKeychainLockedError(err: unknown): boolean {
3082
- return isLockedStorageErrorCode(getStorageErrorCode(err));
3083
- }