react-native-nitro-storage 0.5.7 → 0.5.9

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