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.
@@ -1,41 +1,14 @@
1
1
  "use strict";
2
2
 
3
+ import { assertAccessControlLevel, notifyAllListeners, notifyKeyListeners } from "./shared";
3
4
  import { NitroModules } from "react-native-nitro-modules";
4
- import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
5
- import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, decodeNativeBatchValue, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, toVersionToken, prefixKey, isNamespaced } from "./internal";
6
- import { getStorageErrorCode, isLockedStorageErrorCode } from "./storage-runtime";
7
- import { StorageEventRegistry } from "./storage-events";
5
+ import { StorageScope } from "./Storage.types";
6
+ import { decodeNativeBatchValue } from "./internal";
7
+ import { createStorageCore } from "./storage-core";
8
+ export { isKeychainLockedError } from "./shared";
8
9
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
9
10
  export { migrateFromMMKV } from "./migration";
10
11
  export { getStorageErrorCode } from "./storage-runtime";
11
- function asInternal(item) {
12
- return item;
13
- }
14
- function isUpdater(valueOrFn) {
15
- return typeof valueOrFn === "function";
16
- }
17
- function typedKeys(record) {
18
- return Object.keys(record);
19
- }
20
- function assertEnumInteger(value, min, max, label) {
21
- if (!Number.isFinite(value) || value < min || value > max) {
22
- throw new Error(`NitroStorage: Invalid ${label}`);
23
- }
24
- if (value !== Math.trunc(value)) {
25
- throw new Error(`NitroStorage: Invalid ${label}`);
26
- }
27
- }
28
- function assertAccessControlLevel(level) {
29
- assertEnumInteger(level, 0, 4, "access control level");
30
- }
31
- function assertBiometricLevel(level) {
32
- assertEnumInteger(level, 0, 2, "biometric level");
33
- }
34
- const registeredMigrations = new Map();
35
- const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
36
- Promise.resolve().then(task);
37
- };
38
- const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
39
12
  let _storageModule = null;
40
13
  function getStorageModule() {
41
14
  if (!_storageModule) {
@@ -43,745 +16,135 @@ function getStorageModule() {
43
16
  }
44
17
  return _storageModule;
45
18
  }
46
- const memoryStore = new Map();
47
- const memoryListeners = new Map();
48
- const scopedListeners = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
49
- const scopedUnsubscribers = new Map();
50
- const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
51
- const pendingDiskWrites = new Map();
52
- let diskFlushScheduled = false;
53
- let diskWritesAsync = false;
54
- const pendingSecureWrites = new Map();
55
- let secureFlushScheduled = false;
56
- let secureDefaultAccessControl = AccessControl.WhenUnlocked;
57
- const suppressedNativeEvents = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
58
- let metricsObserver;
59
- let eventObserver;
60
- let eventObserverRedactSecureValues = true;
61
- const metricsCounters = new Map();
62
- const storageEvents = new StorageEventRegistry();
63
19
  const nativeSecureBackend = "platform-secure-storage";
64
- function recordMetric(operation, scope, durationMs, keysCount = 1) {
65
- const existing = metricsCounters.get(operation);
66
- if (!existing) {
67
- metricsCounters.set(operation, {
68
- count: 1,
69
- totalDurationMs: durationMs,
70
- maxDurationMs: durationMs
71
- });
72
- } else {
73
- existing.count += 1;
74
- existing.totalDurationMs += durationMs;
75
- existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
76
- }
77
- metricsObserver?.({
78
- operation,
79
- scope,
80
- durationMs,
81
- keysCount
82
- });
83
- }
84
- function measureOperation(operation, scope, fn, keysCount = 1) {
85
- if (!metricsObserver) {
86
- return fn();
87
- }
88
- const start = now();
89
- try {
90
- return fn();
91
- } finally {
92
- recordMetric(operation, scope, now() - start, keysCount);
93
- }
94
- }
95
- function getScopedListeners(scope) {
96
- return scopedListeners.get(scope);
97
- }
98
- function getScopeRawCache(scope) {
99
- return scopedRawCache.get(scope);
100
- }
101
- function cacheRawValue(scope, key, value) {
102
- getScopeRawCache(scope).set(key, value);
103
- }
104
- function readCachedRawValue(scope, key) {
105
- return getScopeRawCache(scope).get(key);
106
- }
107
- function hasCachedRawValue(scope, key) {
108
- return getScopeRawCache(scope).has(key);
109
- }
110
- function clearScopeRawCache(scope) {
111
- getScopeRawCache(scope).clear();
112
- }
113
- function suppressNativeEvent(scope, key) {
114
- const suppressedEvents = suppressedNativeEvents.get(scope);
115
- suppressedEvents.set(key, (suppressedEvents.get(key) ?? 0) + 1);
116
- }
117
- function consumeSuppressedNativeEvent(scope, key) {
118
- const suppressedEvents = suppressedNativeEvents.get(scope);
119
- const count = suppressedEvents.get(key);
120
- if (count === undefined) {
121
- return false;
122
- }
123
- if (count <= 1) {
124
- suppressedEvents.delete(key);
125
- } else {
126
- suppressedEvents.set(key, count - 1);
127
- }
128
- return true;
129
- }
130
- function notifyKeyListeners(registry, key) {
131
- const listeners = registry.get(key);
132
- if (listeners) {
133
- for (const listener of listeners) {
134
- listener();
135
- }
136
- }
137
- }
138
- function notifyAllListeners(registry) {
139
- for (const listeners of registry.values()) {
140
- for (const listener of listeners) {
141
- listener();
142
- }
143
- }
144
- }
145
- function addKeyListener(registry, key, listener) {
146
- let listeners = registry.get(key);
147
- if (!listeners) {
148
- listeners = new Set();
149
- registry.set(key, listeners);
150
- }
151
- listeners.add(listener);
152
- return () => {
153
- const scopedListeners = registry.get(key);
154
- if (!scopedListeners) {
155
- return;
20
+ const nativeBackend = {
21
+ get: (key, scope) => getStorageModule().get(key, scope),
22
+ set: (key, value, scope) => getStorageModule().set(key, value, scope),
23
+ remove: (key, scope) => getStorageModule().remove(key, scope),
24
+ clear: scope => getStorageModule().clear(scope),
25
+ has: (key, scope) => getStorageModule().has(key, scope),
26
+ getAllKeys: scope => getStorageModule().getAllKeys(scope) ?? [],
27
+ getKeysByPrefix: (prefix, scope) => getStorageModule().getKeysByPrefix(prefix, scope) ?? [],
28
+ size: scope => getStorageModule().size(scope),
29
+ setBatch: (keys, values, scope) => getStorageModule().setBatch(keys, values, scope),
30
+ getBatch: (keys, scope) => (getStorageModule().getBatch(keys, scope) ?? []).map(value => decodeNativeBatchValue(value)),
31
+ removeBatch: (keys, scope) => getStorageModule().removeBatch(keys, scope),
32
+ removeByPrefix: (prefix, scope) => getStorageModule().removeByPrefix(prefix, scope),
33
+ setSecureAccessControl: level => getStorageModule().setSecureAccessControl(level),
34
+ getSecureBiometric: key => getStorageModule().getSecureBiometric(key),
35
+ setSecureBiometricWithLevel: (key, value, level) => getStorageModule().setSecureBiometricWithLevel(key, value, level),
36
+ deleteSecureBiometric: key => getStorageModule().deleteSecureBiometric(key),
37
+ hasSecureBiometric: key => getStorageModule().hasSecureBiometric(key),
38
+ clearSecureBiometric: () => getStorageModule().clearSecureBiometric()
39
+ };
40
+ function buildNativeAdapter(internals) {
41
+ const scopedUnsubscribers = new Map();
42
+ const suppressedNativeEvents = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
43
+ function suppressNativeEvent(scope, key) {
44
+ const suppressedEvents = suppressedNativeEvents.get(scope);
45
+ suppressedEvents.set(key, (suppressedEvents.get(key) ?? 0) + 1);
46
+ }
47
+ function consumeSuppressedNativeEvent(scope, key) {
48
+ const suppressedEvents = suppressedNativeEvents.get(scope);
49
+ const count = suppressedEvents.get(key);
50
+ if (count === undefined) {
51
+ return false;
156
52
  }
157
- scopedListeners.delete(listener);
158
- if (scopedListeners.size === 0) {
159
- registry.delete(key);
53
+ if (count <= 1) {
54
+ suppressedEvents.delete(key);
55
+ } else {
56
+ suppressedEvents.set(key, count - 1);
160
57
  }
161
- };
162
- }
163
- function getEventRawValue(scope, key) {
164
- if (scope === StorageScope.Memory) {
165
- const value = memoryStore.get(key);
166
- return typeof value === "string" ? value : undefined;
167
- }
168
- return getRawValue(key, scope);
169
- }
170
- function createKeyChange(scope, key, oldValue, newValue, operation, source) {
171
- return {
172
- type: "key",
173
- scope,
174
- key,
175
- oldValue,
176
- newValue,
177
- operation,
178
- source
179
- };
180
- }
181
- function hasStorageChangeObservers(scope) {
182
- return storageEvents.hasListeners(scope) || eventObserver !== undefined;
183
- }
184
- function shouldReadPreviousEventValues(scope) {
185
- if (storageEvents.hasListeners(scope)) {
186
58
  return true;
187
59
  }
188
- if (!eventObserver) {
189
- return false;
190
- }
191
- return scope !== StorageScope.Secure || !eventObserverRedactSecureValues;
192
- }
193
- const SECURE_EVENT_REDACTED_VALUE = "[secure]";
194
- function redactSecureKeyChange(event) {
195
- if (event.scope !== StorageScope.Secure) {
196
- return event;
197
- }
198
- return {
199
- ...event,
200
- oldValue: event.oldValue === undefined ? undefined : SECURE_EVENT_REDACTED_VALUE,
201
- newValue: event.newValue === undefined ? undefined : SECURE_EVENT_REDACTED_VALUE
202
- };
203
- }
204
- function eventForGlobalObserver(event) {
205
- if (!eventObserverRedactSecureValues || event.scope !== StorageScope.Secure) {
206
- return event;
207
- }
208
- if (event.type === "key") {
209
- return redactSecureKeyChange(event);
210
- }
211
- return {
212
- ...event,
213
- changes: event.changes.map(redactSecureKeyChange)
214
- };
215
- }
216
- function emitKeyChange(scope, key, oldValue, newValue, operation, source) {
217
- if (source === "native" && operation !== "external" && scope !== StorageScope.Memory && scopedUnsubscribers.has(scope)) {
218
- suppressNativeEvent(scope, key);
219
- }
220
- const event = createKeyChange(scope, key, oldValue, newValue, operation, source);
221
- storageEvents.emitKey(event);
222
- eventObserver?.(eventForGlobalObserver(event));
223
- }
224
- function emitBatchChange(scope, operation, source, changes) {
225
- if (changes.length === 0) {
226
- return;
227
- }
228
- if (source === "native" && operation !== "external" && scope !== StorageScope.Memory && scopedUnsubscribers.has(scope)) {
229
- changes.forEach(change => suppressNativeEvent(scope, change.key));
230
- }
231
- const event = {
232
- type: "batch",
233
- scope,
234
- operation,
235
- source,
236
- changes
237
- };
238
- storageEvents.emitBatch(event);
239
- eventObserver?.(eventForGlobalObserver(event));
240
- }
241
- function readPendingSecureWrite(key) {
242
- return pendingSecureWrites.get(key)?.value;
243
- }
244
- function readPendingDiskWrite(key) {
245
- return pendingDiskWrites.get(key)?.value;
246
- }
247
- function hasPendingDiskWrite(key) {
248
- return pendingDiskWrites.has(key);
249
- }
250
- function hasPendingSecureWrite(key) {
251
- return pendingSecureWrites.has(key);
252
- }
253
- function clearPendingDiskWrite(key) {
254
- pendingDiskWrites.delete(key);
255
- }
256
- function clearPendingSecureWrite(key) {
257
- pendingSecureWrites.delete(key);
258
- }
259
- function flushDiskWrites() {
260
- diskFlushScheduled = false;
261
- if (pendingDiskWrites.size === 0) {
262
- return;
263
- }
264
- const writes = Array.from(pendingDiskWrites.values());
265
- pendingDiskWrites.clear();
266
- const keysToSet = [];
267
- const valuesToSet = [];
268
- const keysToRemove = [];
269
- writes.forEach(({
270
- key,
271
- value
272
- }) => {
273
- if (value === undefined) {
274
- keysToRemove.push(key);
60
+ function ensureNativeScopeSubscription(scope) {
61
+ if (scopedUnsubscribers.has(scope)) {
275
62
  return;
276
63
  }
277
- keysToSet.push(key);
278
- valuesToSet.push(value);
279
- });
280
- const storageModule = getStorageModule();
281
- if (keysToSet.length > 0) {
282
- storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Disk);
283
- }
284
- if (keysToRemove.length > 0) {
285
- storageModule.removeBatch(keysToRemove, StorageScope.Disk);
286
- }
287
- }
288
- function flushSecureWrites() {
289
- secureFlushScheduled = false;
290
- if (pendingSecureWrites.size === 0) {
291
- return;
292
- }
293
- const writes = Array.from(pendingSecureWrites.values());
294
- pendingSecureWrites.clear();
295
- const groupedSetWrites = new Map();
296
- const keysToRemove = [];
297
- writes.forEach(({
298
- key,
299
- value,
300
- accessControl
301
- }) => {
302
- if (value === undefined) {
303
- keysToRemove.push(key);
304
- } else {
305
- const resolvedAccessControl = accessControl ?? secureDefaultAccessControl;
306
- const existingGroup = groupedSetWrites.get(resolvedAccessControl);
307
- const group = existingGroup ?? {
308
- keys: [],
309
- values: []
310
- };
311
- group.keys.push(key);
312
- group.values.push(value);
313
- if (!existingGroup) {
314
- groupedSetWrites.set(resolvedAccessControl, group);
64
+ const unsubscribe = getStorageModule().addOnChange(scope, (key, value) => {
65
+ if (scope === StorageScope.Disk) {
66
+ if (key === "") {
67
+ internals.clearAllPendingDiskWrites();
68
+ } else {
69
+ internals.clearPendingDiskWrite(key);
70
+ }
315
71
  }
316
- }
317
- });
318
- const storageModule = getStorageModule();
319
- groupedSetWrites.forEach((group, accessControl) => {
320
- storageModule.setSecureAccessControl(accessControl);
321
- storageModule.setBatch(group.keys, group.values, StorageScope.Secure);
322
- });
323
- if (keysToRemove.length > 0) {
324
- storageModule.removeBatch(keysToRemove, StorageScope.Secure);
325
- }
326
- }
327
- function scheduleDiskWrite(key, value) {
328
- pendingDiskWrites.set(key, {
329
- key,
330
- value
331
- });
332
- if (diskFlushScheduled) {
333
- return;
334
- }
335
- diskFlushScheduled = true;
336
- runMicrotask(flushDiskWrites);
337
- }
338
- function scheduleSecureWrite(key, value, accessControl) {
339
- const pendingWrite = {
340
- key,
341
- value
342
- };
343
- if (accessControl !== undefined) {
344
- pendingWrite.accessControl = accessControl;
345
- }
346
- pendingSecureWrites.set(key, pendingWrite);
347
- if (secureFlushScheduled) {
348
- return;
349
- }
350
- secureFlushScheduled = true;
351
- runMicrotask(flushSecureWrites);
352
- }
353
- function ensureNativeScopeSubscription(scope) {
354
- if (scopedUnsubscribers.has(scope)) {
355
- return;
356
- }
357
- const unsubscribe = getStorageModule().addOnChange(scope, (key, value) => {
358
- if (scope === StorageScope.Disk) {
359
- if (key === "") {
360
- pendingDiskWrites.clear();
361
- } else {
362
- clearPendingDiskWrite(key);
72
+ if (scope === StorageScope.Secure) {
73
+ if (key === "") {
74
+ internals.clearAllPendingSecureWrites();
75
+ } else {
76
+ internals.clearPendingSecureWrite(key);
77
+ }
363
78
  }
364
- }
365
- if (scope === StorageScope.Secure) {
366
79
  if (key === "") {
367
- pendingSecureWrites.clear();
368
- } else {
369
- clearPendingSecureWrite(key);
80
+ internals.clearScopeRawCache(scope);
81
+ notifyAllListeners(internals.getScopedListeners(scope));
82
+ return;
370
83
  }
371
- }
372
- if (key === "") {
373
- clearScopeRawCache(scope);
374
- notifyAllListeners(getScopedListeners(scope));
375
- return;
376
- }
377
- const oldValue = readCachedRawValue(scope, key);
378
- cacheRawValue(scope, key, value);
379
- notifyKeyListeners(getScopedListeners(scope), key);
380
- if (consumeSuppressedNativeEvent(scope, key)) {
381
- return;
382
- }
383
- emitKeyChange(scope, key, oldValue, value, "external", "native");
384
- });
385
- scopedUnsubscribers.set(scope, typeof unsubscribe === "function" ? unsubscribe : () => {});
386
- }
387
- function maybeCleanupNativeScopeSubscription(scope) {
388
- const listeners = getScopedListeners(scope);
389
- if (listeners.size > 0 || storageEvents.hasListeners(scope) || eventObserver !== undefined) {
390
- return;
391
- }
392
- const unsubscribe = scopedUnsubscribers.get(scope);
393
- if (!unsubscribe) {
394
- return;
395
- }
396
- unsubscribe();
397
- scopedUnsubscribers.delete(scope);
398
- }
399
- function getRawValue(key, scope) {
400
- assertValidScope(scope);
401
- if (scope === StorageScope.Memory) {
402
- const value = memoryStore.get(key);
403
- return typeof value === "string" ? value : undefined;
404
- }
405
- if (scope === StorageScope.Disk && hasPendingDiskWrite(key)) {
406
- return readPendingDiskWrite(key);
407
- }
408
- if (scope === StorageScope.Secure && hasPendingSecureWrite(key)) {
409
- return readPendingSecureWrite(key);
410
- }
411
- return getStorageModule().get(key, scope);
412
- }
413
- function setRawValue(key, value, scope) {
414
- assertValidScope(scope);
415
- const oldValue = scope === StorageScope.Memory ? getEventRawValue(scope, key) : undefined;
416
- if (scope === StorageScope.Memory) {
417
- memoryStore.set(key, value);
418
- notifyKeyListeners(memoryListeners, key);
419
- emitKeyChange(scope, key, oldValue, value, "set", "memory");
420
- return;
84
+ const oldValue = internals.readCachedRawValue(scope, key);
85
+ internals.cacheRawValue(scope, key, value);
86
+ notifyKeyListeners(internals.getScopedListeners(scope), key);
87
+ if (consumeSuppressedNativeEvent(scope, key)) {
88
+ return;
89
+ }
90
+ internals.emitKeyChange(scope, key, oldValue, value, "external", "native");
91
+ });
92
+ scopedUnsubscribers.set(scope, typeof unsubscribe === "function" ? unsubscribe : () => {});
421
93
  }
422
- if (scope === StorageScope.Disk) {
423
- cacheRawValue(scope, key, value);
424
- if (diskWritesAsync) {
425
- scheduleDiskWrite(key, value);
426
- emitKeyChange(scope, key, oldValue, value, "set", "native");
94
+ function maybeCleanupNativeScopeSubscription(scope) {
95
+ const listeners = internals.getScopedListeners(scope);
96
+ if (listeners.size > 0 || internals.hasScopeEventListeners(scope) || internals.hasEventObserver()) {
427
97
  return;
428
98
  }
429
- flushDiskWrites();
430
- clearPendingDiskWrite(key);
431
- }
432
- if (scope === StorageScope.Secure) {
433
- flushSecureWrites();
434
- clearPendingSecureWrite(key);
435
- getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
436
- }
437
- getStorageModule().set(key, value, scope);
438
- cacheRawValue(scope, key, value);
439
- emitKeyChange(scope, key, oldValue, value, "set", "native");
440
- }
441
- function removeRawValue(key, scope) {
442
- assertValidScope(scope);
443
- const oldValue = getEventRawValue(scope, key);
444
- if (scope === StorageScope.Memory) {
445
- memoryStore.delete(key);
446
- notifyKeyListeners(memoryListeners, key);
447
- emitKeyChange(scope, key, oldValue, undefined, "remove", "memory");
448
- return;
449
- }
450
- if (scope === StorageScope.Disk) {
451
- cacheRawValue(scope, key, undefined);
452
- if (diskWritesAsync) {
453
- scheduleDiskWrite(key, undefined);
454
- emitKeyChange(scope, key, oldValue, undefined, "remove", "native");
99
+ const unsubscribe = scopedUnsubscribers.get(scope);
100
+ if (!unsubscribe) {
455
101
  return;
456
102
  }
457
- flushDiskWrites();
458
- clearPendingDiskWrite(key);
459
- }
460
- if (scope === StorageScope.Secure) {
461
- flushSecureWrites();
462
- clearPendingSecureWrite(key);
103
+ unsubscribe();
104
+ scopedUnsubscribers.delete(scope);
463
105
  }
464
- getStorageModule().remove(key, scope);
465
- cacheRawValue(scope, key, undefined);
466
- emitKeyChange(scope, key, oldValue, undefined, "remove", "native");
467
- }
468
- function readMigrationVersion(scope) {
469
- const raw = getRawValue(MIGRATION_VERSION_KEY, scope);
470
- if (raw === undefined) {
471
- return 0;
472
- }
473
- const parsed = Number.parseInt(raw, 10);
474
- return Number.isFinite(parsed) && parsed > 0 ? parsed : 0;
475
- }
476
- function writeMigrationVersion(scope, version) {
477
- setRawValue(MIGRATION_VERSION_KEY, String(version), scope);
106
+ return {
107
+ backend: nativeBackend,
108
+ changeSource: "native",
109
+ applyAccessControlOnSecureRawWrite: true,
110
+ flushDiskWritesOnImport: false,
111
+ ensureScopeSubscription: ensureNativeScopeSubscription,
112
+ maybeCleanupScopeSubscription: maybeCleanupNativeScopeSubscription,
113
+ onWillEmitChanges: (scope, keys, operation, source) => {
114
+ if (source === "native" && operation !== "external" && scope !== StorageScope.Memory && scopedUnsubscribers.has(scope)) {
115
+ keys.forEach(key => suppressNativeEvent(scope, key));
116
+ }
117
+ },
118
+ getSecureMetadataProfile: () => ({
119
+ backend: nativeSecureBackend,
120
+ encrypted: "available",
121
+ hardwareBacked: "unknown"
122
+ })
123
+ };
478
124
  }
125
+ const core = createStorageCore(buildNativeAdapter);
126
+ const {
127
+ internals
128
+ } = core;
479
129
  export const storage = {
480
- subscribe: (scope, listener) => {
481
- assertValidScope(scope);
482
- if (scope !== StorageScope.Memory) {
483
- ensureNativeScopeSubscription(scope);
484
- const unsubscribe = storageEvents.subscribe(scope, listener);
485
- return () => {
486
- unsubscribe();
487
- maybeCleanupNativeScopeSubscription(scope);
488
- };
489
- }
490
- return storageEvents.subscribe(scope, listener);
491
- },
492
- subscribeKey: (scope, key, listener) => {
493
- assertValidScope(scope);
494
- if (scope !== StorageScope.Memory) {
495
- ensureNativeScopeSubscription(scope);
496
- const unsubscribe = storageEvents.subscribeKey(scope, key, listener);
497
- return () => {
498
- unsubscribe();
499
- maybeCleanupNativeScopeSubscription(scope);
500
- };
501
- }
502
- return storageEvents.subscribeKey(scope, key, listener);
503
- },
504
- subscribePrefix: (scope, prefix, listener) => {
505
- assertValidScope(scope);
506
- if (scope !== StorageScope.Memory) {
507
- ensureNativeScopeSubscription(scope);
508
- const unsubscribe = storageEvents.subscribePrefix(scope, prefix, listener);
509
- return () => {
510
- unsubscribe();
511
- maybeCleanupNativeScopeSubscription(scope);
512
- };
513
- }
514
- return storageEvents.subscribePrefix(scope, prefix, listener);
515
- },
516
- subscribeNamespace: (namespace, scope, listener) => {
517
- return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
518
- },
519
- setEventObserver: (observer, options = {}) => {
520
- eventObserver = observer;
521
- eventObserverRedactSecureValues = options.redactSecureValues !== false;
522
- if (observer) {
523
- ensureNativeScopeSubscription(StorageScope.Disk);
524
- ensureNativeScopeSubscription(StorageScope.Secure);
525
- return;
526
- }
527
- maybeCleanupNativeScopeSubscription(StorageScope.Disk);
528
- maybeCleanupNativeScopeSubscription(StorageScope.Secure);
529
- },
530
- clear: scope => {
531
- measureOperation("storage:clear", scope, () => {
532
- const previousValues = shouldReadPreviousEventValues(scope) ? storage.getAll(scope) : {};
533
- if (scope === StorageScope.Memory) {
534
- memoryStore.clear();
535
- notifyAllListeners(memoryListeners);
536
- emitBatchChange(scope, "clear", "memory", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "memory")));
537
- return;
538
- }
539
- if (scope === StorageScope.Disk) {
540
- flushDiskWrites();
541
- pendingDiskWrites.clear();
542
- }
543
- if (scope === StorageScope.Secure) {
544
- flushSecureWrites();
545
- pendingSecureWrites.clear();
546
- }
547
- clearScopeRawCache(scope);
548
- getStorageModule().clear(scope);
549
- emitBatchChange(scope, "clear", "native", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clear", "native")));
550
- });
551
- },
552
- clearAll: () => {
553
- measureOperation("storage:clearAll", StorageScope.Memory, () => {
554
- storage.clear(StorageScope.Memory);
555
- storage.clear(StorageScope.Disk);
556
- storage.clear(StorageScope.Secure);
557
- }, 3);
558
- },
559
- clearNamespace: (namespace, scope) => {
560
- measureOperation("storage:clearNamespace", scope, () => {
561
- assertValidScope(scope);
562
- if (scope === StorageScope.Memory) {
563
- const affectedKeys = Array.from(memoryStore.keys()).filter(key => isNamespaced(key, namespace));
564
- const previousValues = affectedKeys.map(key => ({
565
- key,
566
- value: getEventRawValue(scope, key)
567
- }));
568
- if (affectedKeys.length === 0) {
569
- return;
570
- }
571
- affectedKeys.forEach(key => {
572
- memoryStore.delete(key);
573
- });
574
- affectedKeys.forEach(key => notifyKeyListeners(memoryListeners, key));
575
- emitBatchChange(scope, "clearNamespace", "memory", previousValues.map(({
576
- key,
577
- value
578
- }) => createKeyChange(scope, key, value, undefined, "clearNamespace", "memory")));
579
- return;
580
- }
581
- const keyPrefix = prefixKey(namespace, "");
582
- const previousValues = shouldReadPreviousEventValues(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
583
- if (scope === StorageScope.Disk) {
584
- flushDiskWrites();
585
- }
586
- if (scope === StorageScope.Secure) {
587
- flushSecureWrites();
588
- }
589
- const scopeCache = getScopeRawCache(scope);
590
- for (const key of scopeCache.keys()) {
591
- if (isNamespaced(key, namespace)) {
592
- scopeCache.delete(key);
593
- }
594
- }
595
- getStorageModule().removeByPrefix(keyPrefix, scope);
596
- emitBatchChange(scope, "clearNamespace", "native", Object.keys(previousValues).map(key => createKeyChange(scope, key, previousValues[key], undefined, "clearNamespace", "native")));
597
- });
598
- },
599
- clearBiometric: () => {
600
- measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
601
- getStorageModule().clearSecureBiometric();
602
- });
603
- },
604
- has: (key, scope) => {
605
- return measureOperation("storage:has", scope, () => {
606
- assertValidScope(scope);
607
- if (scope === StorageScope.Memory) {
608
- return memoryStore.has(key);
609
- }
610
- if (scope === StorageScope.Disk) {
611
- flushDiskWrites();
612
- }
613
- if (scope === StorageScope.Secure) {
614
- flushSecureWrites();
615
- }
616
- return getStorageModule().has(key, scope);
617
- });
618
- },
619
- getAllKeys: scope => {
620
- return measureOperation("storage:getAllKeys", scope, () => {
621
- assertValidScope(scope);
622
- if (scope === StorageScope.Memory) {
623
- return Array.from(memoryStore.keys());
624
- }
625
- if (scope === StorageScope.Disk) {
626
- flushDiskWrites();
627
- }
628
- if (scope === StorageScope.Secure) {
629
- flushSecureWrites();
630
- }
631
- return getStorageModule().getAllKeys(scope);
632
- });
633
- },
634
- getKeysByPrefix: (prefix, scope) => {
635
- return measureOperation("storage:getKeysByPrefix", scope, () => {
636
- assertValidScope(scope);
637
- if (scope === StorageScope.Memory) {
638
- return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
639
- }
640
- if (scope === StorageScope.Disk) {
641
- flushDiskWrites();
642
- }
643
- if (scope === StorageScope.Secure) {
644
- flushSecureWrites();
645
- }
646
- return getStorageModule().getKeysByPrefix(prefix, scope) ?? [];
647
- });
648
- },
649
- getByPrefix: (prefix, scope) => {
650
- return measureOperation("storage:getByPrefix", scope, () => {
651
- const result = {};
652
- const keys = storage.getKeysByPrefix(prefix, scope);
653
- if (keys.length === 0) {
654
- return result;
655
- }
656
- if (scope === StorageScope.Memory) {
657
- keys.forEach(key => {
658
- const value = memoryStore.get(key);
659
- if (typeof value === "string") {
660
- result[key] = value;
661
- }
662
- });
663
- return result;
664
- }
665
- if (scope === StorageScope.Disk) {
666
- flushDiskWrites();
667
- }
668
- if (scope === StorageScope.Secure) {
669
- flushSecureWrites();
670
- }
671
- const values = getStorageModule().getBatch(keys, scope) ?? [];
672
- keys.forEach((key, idx) => {
673
- const value = decodeNativeBatchValue(values[idx]);
674
- if (value !== undefined) {
675
- result[key] = value;
676
- }
677
- });
678
- return result;
679
- });
680
- },
681
- getAll: scope => {
682
- return measureOperation("storage:getAll", scope, () => {
683
- assertValidScope(scope);
684
- const result = {};
685
- if (scope === StorageScope.Memory) {
686
- for (const key of memoryStore.keys()) {
687
- const value = memoryStore.get(key);
688
- if (typeof value === "string") result[key] = value;
689
- }
690
- return result;
691
- }
692
- if (scope === StorageScope.Disk) {
693
- flushDiskWrites();
694
- }
695
- if (scope === StorageScope.Secure) {
696
- flushSecureWrites();
697
- }
698
- const keys = getStorageModule().getAllKeys(scope) ?? [];
699
- if (keys.length === 0) return result;
700
- const values = getStorageModule().getBatch(keys, scope) ?? [];
701
- keys.forEach((key, idx) => {
702
- const val = decodeNativeBatchValue(values[idx]);
703
- if (val !== undefined) result[key] = val;
704
- });
705
- return result;
706
- });
707
- },
708
- export: (scope, options = {}) => {
709
- if (scope === StorageScope.Secure && options.includeSecureValues !== true) {
710
- throw new Error("NitroStorage: exporting Secure scope exposes raw secret values. Pass { includeSecureValues: true } or use exportSecureUnsafe().");
711
- }
712
- return measureOperation("storage:export", scope, () => storage.getAll(scope));
713
- },
714
- exportSecureUnsafe: () => {
715
- return measureOperation("storage:exportSecureUnsafe", StorageScope.Secure, () => storage.getAll(StorageScope.Secure));
716
- },
717
- size: scope => {
718
- return measureOperation("storage:size", scope, () => {
719
- assertValidScope(scope);
720
- if (scope === StorageScope.Memory) {
721
- return memoryStore.size;
722
- }
723
- if (scope === StorageScope.Disk) {
724
- flushDiskWrites();
725
- }
726
- if (scope === StorageScope.Secure) {
727
- flushSecureWrites();
728
- }
729
- return getStorageModule().size(scope);
730
- });
731
- },
130
+ ...core.storage,
732
131
  setAccessControl: level => {
733
- measureOperation("storage:setAccessControl", StorageScope.Secure, () => {
132
+ internals.measureOperation("storage:setAccessControl", StorageScope.Secure, () => {
734
133
  assertAccessControlLevel(level);
735
- secureDefaultAccessControl = level;
134
+ internals.setSecureDefaultAccessControl(level);
736
135
  getStorageModule().setSecureAccessControl(level);
737
136
  });
738
137
  },
739
138
  setSecureWritesAsync: enabled => {
740
- measureOperation("storage:setSecureWritesAsync", StorageScope.Secure, () => {
139
+ internals.measureOperation("storage:setSecureWritesAsync", StorageScope.Secure, () => {
741
140
  getStorageModule().setSecureWritesAsync(enabled);
742
141
  });
743
142
  },
744
- setDiskWritesAsync: enabled => {
745
- measureOperation("storage:setDiskWritesAsync", StorageScope.Disk, () => {
746
- diskWritesAsync = enabled;
747
- if (!enabled) {
748
- flushDiskWrites();
749
- }
750
- });
751
- },
752
- flushDiskWrites: () => {
753
- measureOperation("storage:flushDiskWrites", StorageScope.Disk, () => {
754
- flushDiskWrites();
755
- });
756
- },
757
- flushSecureWrites: () => {
758
- measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
759
- flushSecureWrites();
760
- });
761
- },
762
143
  setKeychainAccessGroup: group => {
763
- measureOperation("storage:setKeychainAccessGroup", StorageScope.Secure, () => {
144
+ internals.measureOperation("storage:setKeychainAccessGroup", StorageScope.Secure, () => {
764
145
  getStorageModule().setKeychainAccessGroup(group);
765
146
  });
766
147
  },
767
- setMetricsObserver: observer => {
768
- metricsObserver = observer;
769
- },
770
- getMetricsSnapshot: () => {
771
- const snapshot = {};
772
- metricsCounters.forEach((value, key) => {
773
- snapshot[key] = {
774
- count: value.count,
775
- totalDurationMs: value.totalDurationMs,
776
- avgDurationMs: value.count === 0 ? 0 : value.totalDurationMs / value.count,
777
- maxDurationMs: value.maxDurationMs
778
- };
779
- });
780
- return snapshot;
781
- },
782
- resetMetrics: () => {
783
- metricsCounters.clear();
784
- },
785
148
  getCapabilities: () => ({
786
149
  platform: "native",
787
150
  backend: {
@@ -814,75 +177,16 @@ export const storage = {
814
177
  listsWithoutValues: true,
815
178
  persistsTimestamps: false
816
179
  }
817
- }),
818
- getSecureMetadata: key => {
819
- return measureOperation("storage:getSecureMetadata", StorageScope.Secure, () => {
820
- flushSecureWrites();
821
- const storageModule = getStorageModule();
822
- const biometricProtected = storageModule.hasSecureBiometric(key);
823
- const exists = biometricProtected || storageModule.has(key, StorageScope.Secure);
824
- let kind = "missing";
825
- if (exists) {
826
- kind = biometricProtected ? "biometric" : "secure";
827
- }
828
- return {
829
- key,
830
- exists,
831
- kind,
832
- backend: nativeSecureBackend,
833
- encrypted: "available",
834
- hardwareBacked: "unknown",
835
- biometricProtected,
836
- valueExposed: false
837
- };
838
- });
839
- },
840
- getAllSecureMetadata: () => {
841
- return measureOperation("storage:getAllSecureMetadata", StorageScope.Secure, () => {
842
- flushSecureWrites();
843
- return getStorageModule().getAllKeys(StorageScope.Secure).map(key => storage.getSecureMetadata(key));
844
- });
845
- },
846
- getString: (key, scope) => {
847
- return measureOperation("storage:getString", scope, () => {
848
- return getRawValue(key, scope);
849
- });
850
- },
851
- setString: (key, value, scope) => {
852
- measureOperation("storage:setString", scope, () => {
853
- setRawValue(key, value, scope);
854
- });
855
- },
856
- deleteString: (key, scope) => {
857
- measureOperation("storage:deleteString", scope, () => {
858
- removeRawValue(key, scope);
859
- });
860
- },
861
- import: (data, scope) => {
862
- const keys = Object.keys(data);
863
- measureOperation("storage:import", scope, () => {
864
- assertValidScope(scope);
865
- if (keys.length === 0) return;
866
- const values = keys.map(k => data[k]);
867
- const changes = keys.map((key, index) => createKeyChange(scope, key, getEventRawValue(scope, key), values[index], "import", scope === StorageScope.Memory ? "memory" : "native"));
868
- if (scope === StorageScope.Memory) {
869
- keys.forEach((key, index) => {
870
- memoryStore.set(key, values[index]);
871
- });
872
- keys.forEach(key => notifyKeyListeners(memoryListeners, key));
873
- emitBatchChange(scope, "import", "memory", changes);
874
- return;
875
- }
876
- if (scope === StorageScope.Secure) {
877
- flushSecureWrites();
878
- getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
879
- }
880
- getStorageModule().setBatch(keys, values, scope);
881
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
882
- emitBatchChange(scope, "import", "native", changes);
883
- }, keys.length);
884
- }
180
+ })
885
181
  };
182
+ export const createStorageItem = core.createStorageItem;
183
+ export const getBatch = core.getBatch;
184
+ export const setBatch = core.setBatch;
185
+ export const removeBatch = core.removeBatch;
186
+ export const registerMigration = core.registerMigration;
187
+ export const migrateToLatest = core.migrateToLatest;
188
+ export const runTransaction = core.runTransaction;
189
+ export const createSecureAuthStorage = core.createSecureAuthStorage;
886
190
  export function setWebSecureStorageBackend(_backend) {
887
191
  // Native platforms do not use web secure backends.
888
192
  }
@@ -898,790 +202,6 @@ export function getWebDiskStorageBackend() {
898
202
  export async function flushWebStorageBackends() {
899
203
  // Native platforms do not use web storage backends.
900
204
  }
901
- function canUseRawBatchPath(item) {
902
- return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
903
- }
904
- function canUseSecureRawBatchPath(item) {
905
- return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true;
906
- }
907
- function defaultSerialize(value) {
908
- return serializeWithPrimitiveFastPath(value);
909
- }
910
- function defaultDeserialize(value) {
911
- return deserializeWithPrimitiveFastPath(value);
912
- }
913
- export function createStorageItem(config) {
914
- const storageKey = prefixKey(config.namespace, config.key);
915
- const serialize = config.serialize ?? defaultSerialize;
916
- const deserialize = config.deserialize ?? defaultDeserialize;
917
- const isMemory = config.scope === StorageScope.Memory;
918
- const resolvedBiometricLevel = config.scope === StorageScope.Secure ? config.biometricLevel ?? (config.biometric === true ? BiometricLevel.BiometryOnly : BiometricLevel.None) : BiometricLevel.None;
919
- const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
920
- const secureAccessControl = config.accessControl;
921
- const validate = config.validate;
922
- const onValidationError = config.onValidationError;
923
- const expiration = config.expiration;
924
- const onExpired = config.onExpired;
925
- const expirationTtlMs = expiration?.ttlMs;
926
- const memoryExpiration = expiration && isMemory ? new Map() : null;
927
- const readCache = !isMemory && config.readCache === true;
928
- const coalesceDiskWrites = config.scope === StorageScope.Disk && config.coalesceDiskWrites === true;
929
- const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
930
- const defaultValue = config.defaultValue;
931
- const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
932
- if (expiration && expiration.ttlMs <= 0) {
933
- throw new Error("expiration.ttlMs must be greater than 0.");
934
- }
935
- if (config.scope === StorageScope.Secure) {
936
- assertBiometricLevel(resolvedBiometricLevel);
937
- if (secureAccessControl !== undefined) {
938
- assertAccessControlLevel(secureAccessControl);
939
- }
940
- }
941
- const listeners = new Set();
942
- let unsubscribe = null;
943
- let lastRaw = undefined;
944
- let lastValue;
945
- let hasLastValue = false;
946
- let lastExpiresAt = undefined;
947
- const invalidateParsedCache = () => {
948
- lastRaw = undefined;
949
- lastValue = undefined;
950
- hasLastValue = false;
951
- lastExpiresAt = undefined;
952
- };
953
- const ensureSubscription = () => {
954
- if (unsubscribe) {
955
- return;
956
- }
957
- const listener = () => {
958
- invalidateParsedCache();
959
- listeners.forEach(callback => callback());
960
- };
961
- if (isMemory) {
962
- unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
963
- return;
964
- }
965
- ensureNativeScopeSubscription(nonMemoryScope);
966
- unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
967
- };
968
- const readStoredRaw = () => {
969
- if (isMemory) {
970
- if (memoryExpiration) {
971
- const expiresAt = memoryExpiration.get(storageKey);
972
- if (expiresAt !== undefined && expiresAt <= Date.now()) {
973
- memoryExpiration.delete(storageKey);
974
- memoryStore.delete(storageKey);
975
- notifyKeyListeners(memoryListeners, storageKey);
976
- onExpired?.(storageKey);
977
- return undefined;
978
- }
979
- }
980
- return memoryStore.get(storageKey);
981
- }
982
- if (nonMemoryScope === StorageScope.Disk) {
983
- const pending = pendingDiskWrites.get(storageKey);
984
- if (pending !== undefined) {
985
- return pending.value;
986
- }
987
- }
988
- if (nonMemoryScope === StorageScope.Secure && !isBiometric) {
989
- const pending = pendingSecureWrites.get(storageKey);
990
- if (pending !== undefined) {
991
- return pending.value;
992
- }
993
- }
994
- if (readCache) {
995
- const cache = getScopeRawCache(nonMemoryScope);
996
- const cached = cache.get(storageKey);
997
- if (cached !== undefined || cache.has(storageKey)) {
998
- return cached;
999
- }
1000
- }
1001
- if (isBiometric) {
1002
- return getStorageModule().getSecureBiometric(storageKey);
1003
- }
1004
- const raw = getStorageModule().get(storageKey, config.scope);
1005
- cacheRawValue(nonMemoryScope, storageKey, raw);
1006
- return raw;
1007
- };
1008
- const writeStoredRaw = rawValue => {
1009
- const oldValue = undefined;
1010
- if (isBiometric) {
1011
- getStorageModule().setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
1012
- emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
1013
- return;
1014
- }
1015
- cacheRawValue(nonMemoryScope, storageKey, rawValue);
1016
- if (nonMemoryScope === StorageScope.Disk) {
1017
- if (coalesceDiskWrites || diskWritesAsync) {
1018
- scheduleDiskWrite(storageKey, rawValue);
1019
- emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
1020
- return;
1021
- }
1022
- clearPendingDiskWrite(storageKey);
1023
- }
1024
- if (coalesceSecureWrites) {
1025
- scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
1026
- emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
1027
- return;
1028
- }
1029
- if (nonMemoryScope === StorageScope.Secure) {
1030
- clearPendingSecureWrite(storageKey);
1031
- getStorageModule().setSecureAccessControl(secureAccessControl ?? secureDefaultAccessControl);
1032
- }
1033
- getStorageModule().set(storageKey, rawValue, config.scope);
1034
- emitKeyChange(config.scope, storageKey, oldValue, rawValue, "set", "native");
1035
- };
1036
- const removeStoredRaw = () => {
1037
- const oldValue = getEventRawValue(config.scope, storageKey);
1038
- if (isBiometric) {
1039
- getStorageModule().deleteSecureBiometric(storageKey);
1040
- emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
1041
- return;
1042
- }
1043
- cacheRawValue(nonMemoryScope, storageKey, undefined);
1044
- if (nonMemoryScope === StorageScope.Disk) {
1045
- if (coalesceDiskWrites || diskWritesAsync) {
1046
- scheduleDiskWrite(storageKey, undefined);
1047
- emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
1048
- return;
1049
- }
1050
- clearPendingDiskWrite(storageKey);
1051
- }
1052
- if (coalesceSecureWrites) {
1053
- scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
1054
- emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
1055
- return;
1056
- }
1057
- if (nonMemoryScope === StorageScope.Secure) {
1058
- clearPendingSecureWrite(storageKey);
1059
- }
1060
- getStorageModule().remove(storageKey, config.scope);
1061
- emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "native");
1062
- };
1063
- const writeValueWithoutValidation = value => {
1064
- if (isMemory) {
1065
- const oldValue = getEventRawValue(config.scope, storageKey);
1066
- if (memoryExpiration) {
1067
- memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
1068
- }
1069
- memoryStore.set(storageKey, value);
1070
- notifyKeyListeners(memoryListeners, storageKey);
1071
- emitKeyChange(config.scope, storageKey, oldValue, typeof value === "string" ? value : undefined, "set", "memory");
1072
- return;
1073
- }
1074
- const serialized = serialize(value);
1075
- if (expiration) {
1076
- const envelope = {
1077
- __nitroStorageEnvelope: true,
1078
- expiresAt: Date.now() + expiration.ttlMs,
1079
- payload: serialized
1080
- };
1081
- writeStoredRaw(JSON.stringify(envelope));
1082
- return;
1083
- }
1084
- writeStoredRaw(serialized);
1085
- };
1086
- const resolveInvalidValue = invalidValue => {
1087
- if (onValidationError) {
1088
- return onValidationError(invalidValue);
1089
- }
1090
- return defaultValue;
1091
- };
1092
- const ensureValidatedValue = (candidate, hadStoredValue) => {
1093
- if (!validate || validate(candidate)) {
1094
- return candidate;
1095
- }
1096
- const resolved = resolveInvalidValue(candidate);
1097
- if (validate && !validate(resolved)) {
1098
- return defaultValue;
1099
- }
1100
- if (hadStoredValue) {
1101
- writeValueWithoutValidation(resolved);
1102
- }
1103
- return resolved;
1104
- };
1105
- const getInternal = () => {
1106
- const raw = readStoredRaw();
1107
- if (!memoryExpiration && raw === lastRaw && hasLastValue) {
1108
- if (!expiration || lastExpiresAt === null) {
1109
- return lastValue;
1110
- }
1111
- if (typeof lastExpiresAt === "number") {
1112
- if (lastExpiresAt > Date.now()) {
1113
- return lastValue;
1114
- }
1115
- removeStoredRaw();
1116
- invalidateParsedCache();
1117
- onExpired?.(storageKey);
1118
- lastValue = ensureValidatedValue(defaultValue, false);
1119
- hasLastValue = true;
1120
- listeners.forEach(cb => cb());
1121
- return lastValue;
1122
- }
1123
- }
1124
- lastRaw = raw;
1125
- if (raw === undefined) {
1126
- lastExpiresAt = undefined;
1127
- lastValue = ensureValidatedValue(defaultValue, false);
1128
- hasLastValue = true;
1129
- return lastValue;
1130
- }
1131
- if (isMemory) {
1132
- lastExpiresAt = undefined;
1133
- lastValue = ensureValidatedValue(raw, true);
1134
- hasLastValue = true;
1135
- return lastValue;
1136
- }
1137
- if (typeof raw !== "string") {
1138
- lastExpiresAt = undefined;
1139
- lastValue = ensureValidatedValue(defaultValue, false);
1140
- hasLastValue = true;
1141
- return lastValue;
1142
- }
1143
- let deserializableRaw = raw;
1144
- if (expiration) {
1145
- let envelopeExpiresAt = null;
1146
- try {
1147
- const parsed = JSON.parse(raw);
1148
- if (isStoredEnvelope(parsed)) {
1149
- envelopeExpiresAt = parsed.expiresAt;
1150
- if (parsed.expiresAt <= Date.now()) {
1151
- removeStoredRaw();
1152
- invalidateParsedCache();
1153
- onExpired?.(storageKey);
1154
- lastValue = ensureValidatedValue(defaultValue, false);
1155
- hasLastValue = true;
1156
- listeners.forEach(cb => cb());
1157
- return lastValue;
1158
- }
1159
- deserializableRaw = parsed.payload;
1160
- }
1161
- } catch {
1162
- // Keep backward compatibility with legacy raw values.
1163
- }
1164
- lastExpiresAt = envelopeExpiresAt;
1165
- } else {
1166
- lastExpiresAt = undefined;
1167
- }
1168
- lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
1169
- hasLastValue = true;
1170
- return lastValue;
1171
- };
1172
- const getCurrentVersion = () => {
1173
- const raw = readStoredRaw();
1174
- return toVersionToken(raw);
1175
- };
1176
- const get = () => measureOperation("item:get", config.scope, () => getInternal());
1177
- const getWithVersion = () => measureOperation("item:getWithVersion", config.scope, () => ({
1178
- value: getInternal(),
1179
- version: getCurrentVersion()
1180
- }));
1181
- const set = valueOrFn => {
1182
- measureOperation("item:set", config.scope, () => {
1183
- const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
1184
- if (validate && !validate(newValue)) {
1185
- throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
1186
- }
1187
- invalidateParsedCache();
1188
- writeValueWithoutValidation(newValue);
1189
- });
1190
- };
1191
- const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
1192
- const currentVersion = getCurrentVersion();
1193
- if (currentVersion !== version) {
1194
- return false;
1195
- }
1196
- set(valueOrFn);
1197
- return true;
1198
- });
1199
- const deleteItem = () => {
1200
- measureOperation("item:delete", config.scope, () => {
1201
- invalidateParsedCache();
1202
- if (isMemory) {
1203
- const oldValue = getEventRawValue(config.scope, storageKey);
1204
- if (memoryExpiration) {
1205
- memoryExpiration.delete(storageKey);
1206
- }
1207
- memoryStore.delete(storageKey);
1208
- notifyKeyListeners(memoryListeners, storageKey);
1209
- emitKeyChange(config.scope, storageKey, oldValue, undefined, "remove", "memory");
1210
- return;
1211
- }
1212
- removeStoredRaw();
1213
- });
1214
- };
1215
- const hasItem = () => measureOperation("item:has", config.scope, () => {
1216
- if (isMemory) return memoryStore.has(storageKey);
1217
- if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
1218
- if (nonMemoryScope === StorageScope.Disk) {
1219
- const pending = pendingDiskWrites.get(storageKey);
1220
- if (pending !== undefined) {
1221
- return pending.value !== undefined;
1222
- }
1223
- }
1224
- if (nonMemoryScope === StorageScope.Secure) {
1225
- const pending = pendingSecureWrites.get(storageKey);
1226
- if (pending !== undefined) {
1227
- return pending.value !== undefined;
1228
- }
1229
- }
1230
- return getStorageModule().has(storageKey, config.scope);
1231
- });
1232
- const subscribe = callback => {
1233
- ensureSubscription();
1234
- listeners.add(callback);
1235
- return () => {
1236
- listeners.delete(callback);
1237
- if (listeners.size === 0 && unsubscribe) {
1238
- unsubscribe();
1239
- if (!isMemory) {
1240
- maybeCleanupNativeScopeSubscription(nonMemoryScope);
1241
- }
1242
- unsubscribe = null;
1243
- }
1244
- };
1245
- };
1246
- const subscribeSelector = (selector, listener, options = {}) => {
1247
- const isEqual = options.isEqual ?? Object.is;
1248
- let currentValue = selector(getInternal());
1249
- if (options.fireImmediately === true) {
1250
- listener(currentValue, currentValue);
1251
- }
1252
- return subscribe(() => {
1253
- const nextValue = selector(getInternal());
1254
- if (isEqual(currentValue, nextValue)) {
1255
- return;
1256
- }
1257
- const previousValue = currentValue;
1258
- currentValue = nextValue;
1259
- listener(nextValue, previousValue);
1260
- });
1261
- };
1262
- const storageItem = {
1263
- get,
1264
- getWithVersion,
1265
- set,
1266
- setIfVersion,
1267
- delete: deleteItem,
1268
- has: hasItem,
1269
- subscribe,
1270
- subscribeSelector,
1271
- serialize,
1272
- deserialize,
1273
- _triggerListeners: () => {
1274
- invalidateParsedCache();
1275
- listeners.forEach(listener => listener());
1276
- },
1277
- _invalidateParsedCacheOnly: () => {
1278
- invalidateParsedCache();
1279
- },
1280
- _hasValidation: validate !== undefined,
1281
- _hasExpiration: expiration !== undefined,
1282
- _readCacheEnabled: readCache,
1283
- _isBiometric: isBiometric,
1284
- _biometricLevel: resolvedBiometricLevel,
1285
- _defaultValue: defaultValue,
1286
- ...(secureAccessControl !== undefined ? {
1287
- _secureAccessControl: secureAccessControl
1288
- } : {}),
1289
- scope: config.scope,
1290
- key: storageKey
1291
- };
1292
- return storageItem;
1293
- }
1294
205
  export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
1295
206
  export { createIndexedDBBackend } from "./indexeddb-backend";
1296
- export function getBatch(items, scope) {
1297
- return measureOperation("batch:get", scope, () => {
1298
- assertBatchScope(items, scope);
1299
- if (scope === StorageScope.Memory) {
1300
- return items.map(item => item.get());
1301
- }
1302
- const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
1303
- if (!useRawBatchPath) {
1304
- return items.map(item => item.get());
1305
- }
1306
- const rawValues = new Array(items.length);
1307
- const keysToFetch = [];
1308
- const keyIndexes = [];
1309
- items.forEach((item, index) => {
1310
- if (scope === StorageScope.Disk) {
1311
- const pending = pendingDiskWrites.get(item.key);
1312
- if (pending !== undefined) {
1313
- rawValues[index] = pending.value;
1314
- return;
1315
- }
1316
- }
1317
- if (scope === StorageScope.Secure) {
1318
- const pending = pendingSecureWrites.get(item.key);
1319
- if (pending !== undefined) {
1320
- rawValues[index] = pending.value;
1321
- return;
1322
- }
1323
- }
1324
- if (item._readCacheEnabled === true) {
1325
- const cache = getScopeRawCache(scope);
1326
- const cached = cache.get(item.key);
1327
- if (cached !== undefined || cache.has(item.key)) {
1328
- rawValues[index] = cached;
1329
- return;
1330
- }
1331
- }
1332
- keysToFetch.push(item.key);
1333
- keyIndexes.push(index);
1334
- });
1335
- if (keysToFetch.length > 0) {
1336
- const fetchedValues = getStorageModule().getBatch(keysToFetch, scope).map(value => decodeNativeBatchValue(value));
1337
- fetchedValues.forEach((value, index) => {
1338
- const key = keysToFetch[index];
1339
- const targetIndex = keyIndexes[index];
1340
- if (key === undefined || targetIndex === undefined) {
1341
- return;
1342
- }
1343
- rawValues[targetIndex] = value;
1344
- cacheRawValue(scope, key, value);
1345
- });
1346
- }
1347
- return items.map((item, index) => {
1348
- const raw = rawValues[index];
1349
- if (raw === undefined) {
1350
- return asInternal(item)._defaultValue;
1351
- }
1352
- return item.deserialize(raw);
1353
- });
1354
- }, items.length);
1355
- }
1356
- export function setBatch(items, scope) {
1357
- measureOperation("batch:set", scope, () => {
1358
- assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
1359
- if (scope === StorageScope.Memory) {
1360
- // Determine if any item needs per-item handling (validation or TTL)
1361
- const needsIndividualSets = items.some(({
1362
- item
1363
- }) => {
1364
- const internal = asInternal(item);
1365
- return internal._hasValidation || internal._hasExpiration;
1366
- });
1367
- if (needsIndividualSets) {
1368
- // Fall back to individual sets to preserve validation and TTL semantics
1369
- items.forEach(({
1370
- item,
1371
- value
1372
- }) => item.set(value));
1373
- return;
1374
- }
1375
- const changes = items.map(({
1376
- item,
1377
- value
1378
- }) => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), typeof value === "string" ? value : undefined, "setBatch", "memory"));
1379
-
1380
- // Atomic write: update all values in memoryStore, invalidate caches, then batch-notify
1381
- items.forEach(({
1382
- item,
1383
- value
1384
- }) => {
1385
- memoryStore.set(item.key, value);
1386
- asInternal(item)._invalidateParsedCacheOnly();
1387
- });
1388
- items.forEach(({
1389
- item
1390
- }) => notifyKeyListeners(memoryListeners, item.key));
1391
- emitBatchChange(scope, "setBatch", "memory", changes);
1392
- return;
1393
- }
1394
- if (scope === StorageScope.Secure) {
1395
- const secureEntries = items.map(({
1396
- item,
1397
- value
1398
- }) => ({
1399
- item,
1400
- value,
1401
- internal: asInternal(item)
1402
- }));
1403
- const canUseSecureBatchPath = secureEntries.every(({
1404
- internal
1405
- }) => canUseSecureRawBatchPath(internal));
1406
- if (!canUseSecureBatchPath) {
1407
- items.forEach(({
1408
- item,
1409
- value
1410
- }) => item.set(value));
1411
- return;
1412
- }
1413
- flushSecureWrites();
1414
- const storageModule = getStorageModule();
1415
- const keys = secureEntries.map(({
1416
- item
1417
- }) => item.key);
1418
- const oldValues = shouldReadPreviousEventValues(scope) ? storageModule.getBatch(keys, scope) ?? [] : [];
1419
- const groupedByAccessControl = new Map();
1420
- secureEntries.forEach(({
1421
- item,
1422
- value,
1423
- internal
1424
- }) => {
1425
- const accessControl = internal._secureAccessControl ?? secureDefaultAccessControl;
1426
- const existingGroup = groupedByAccessControl.get(accessControl);
1427
- const group = existingGroup ?? {
1428
- keys: [],
1429
- values: []
1430
- };
1431
- group.keys.push(item.key);
1432
- group.values.push(item.serialize(value));
1433
- if (!existingGroup) {
1434
- groupedByAccessControl.set(accessControl, group);
1435
- }
1436
- });
1437
- groupedByAccessControl.forEach((group, accessControl) => {
1438
- storageModule.setSecureAccessControl(accessControl);
1439
- storageModule.setBatch(group.keys, group.values, scope);
1440
- group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1441
- });
1442
- emitBatchChange(scope, "setBatch", "native", secureEntries.map(({
1443
- item,
1444
- value
1445
- }, index) => createKeyChange(scope, item.key, oldValues[index], item.serialize(value), "setBatch", "native")));
1446
- return;
1447
- }
1448
- flushDiskWrites();
1449
- const useRawBatchPath = items.every(({
1450
- item
1451
- }) => canUseRawBatchPath(asInternal(item)));
1452
- if (!useRawBatchPath) {
1453
- items.forEach(({
1454
- item,
1455
- value
1456
- }) => item.set(value));
1457
- return;
1458
- }
1459
- const keys = items.map(entry => entry.item.key);
1460
- const values = items.map(entry => entry.item.serialize(entry.value));
1461
- const oldValues = shouldReadPreviousEventValues(scope) ? getStorageModule().getBatch(keys, scope) ?? [] : [];
1462
- getStorageModule().setBatch(keys, values, scope);
1463
- keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1464
- emitBatchChange(scope, "setBatch", "native", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], values[index], "setBatch", "native")));
1465
- }, items.length);
1466
- }
1467
- export function removeBatch(items, scope) {
1468
- measureOperation("batch:remove", scope, () => {
1469
- assertBatchScope(items, scope);
1470
- if (scope === StorageScope.Memory) {
1471
- const changes = items.map(item => createKeyChange(scope, item.key, getEventRawValue(scope, item.key), undefined, "removeBatch", "memory"));
1472
- items.forEach(item => item.delete());
1473
- emitBatchChange(scope, "removeBatch", "memory", changes);
1474
- return;
1475
- }
1476
- const keys = items.map(item => item.key);
1477
- if (scope === StorageScope.Disk) {
1478
- flushDiskWrites();
1479
- }
1480
- if (scope === StorageScope.Secure) {
1481
- flushSecureWrites();
1482
- }
1483
- const oldValues = shouldReadPreviousEventValues(scope) ? getStorageModule().getBatch(keys, scope) ?? [] : [];
1484
- getStorageModule().removeBatch(keys, scope);
1485
- keys.forEach(key => cacheRawValue(scope, key, undefined));
1486
- emitBatchChange(scope, "removeBatch", "native", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], undefined, "removeBatch", "native")));
1487
- }, items.length);
1488
- }
1489
- export function registerMigration(version, migration) {
1490
- if (!Number.isInteger(version) || version <= 0) {
1491
- throw new Error("Migration version must be a positive integer.");
1492
- }
1493
- if (registeredMigrations.has(version)) {
1494
- throw new Error(`Migration version ${version} is already registered.`);
1495
- }
1496
- registeredMigrations.set(version, migration);
1497
- }
1498
- export function migrateToLatest(scope = StorageScope.Disk) {
1499
- return measureOperation("migration:run", scope, () => {
1500
- assertValidScope(scope);
1501
- const currentVersion = readMigrationVersion(scope);
1502
- const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
1503
- let appliedVersion = currentVersion;
1504
- const context = {
1505
- scope,
1506
- getRaw: key => getRawValue(key, scope),
1507
- setRaw: (key, value) => setRawValue(key, value, scope),
1508
- removeRaw: key => removeRawValue(key, scope)
1509
- };
1510
- versions.forEach(version => {
1511
- const migration = registeredMigrations.get(version);
1512
- if (!migration) {
1513
- return;
1514
- }
1515
- migration(context);
1516
- appliedVersion = version;
1517
- });
1518
- if (appliedVersion !== currentVersion) {
1519
- writeMigrationVersion(scope, appliedVersion);
1520
- }
1521
- return appliedVersion;
1522
- });
1523
- }
1524
- export function runTransaction(scope, transaction) {
1525
- return measureOperation("transaction:run", scope, () => {
1526
- assertValidScope(scope);
1527
- if (scope === StorageScope.Disk) {
1528
- flushDiskWrites();
1529
- }
1530
- if (scope === StorageScope.Secure) {
1531
- flushSecureWrites();
1532
- }
1533
- const NOT_SET = Symbol();
1534
- const rollback = new Map();
1535
- const rememberRollback = (key, item) => {
1536
- if (rollback.has(key)) {
1537
- return;
1538
- }
1539
- if (scope === StorageScope.Memory) {
1540
- rollback.set(key, {
1541
- kind: "memory",
1542
- value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET
1543
- });
1544
- } else {
1545
- const internal = item ? item : undefined;
1546
- if (scope === StorageScope.Secure && internal?._isBiometric === true) {
1547
- rollback.set(key, {
1548
- kind: "biometric",
1549
- value: getStorageModule().getSecureBiometric(key),
1550
- level: internal._biometricLevel
1551
- });
1552
- return;
1553
- }
1554
- rollback.set(key, {
1555
- kind: "raw",
1556
- value: getRawValue(key, scope),
1557
- ...(scope === StorageScope.Secure && internal?._secureAccessControl !== undefined ? {
1558
- accessControl: internal._secureAccessControl
1559
- } : {})
1560
- });
1561
- }
1562
- };
1563
- const tx = {
1564
- scope,
1565
- getRaw: key => getRawValue(key, scope),
1566
- setRaw: (key, value) => {
1567
- rememberRollback(key);
1568
- setRawValue(key, value, scope);
1569
- },
1570
- removeRaw: key => {
1571
- rememberRollback(key);
1572
- removeRawValue(key, scope);
1573
- },
1574
- getItem: item => {
1575
- assertBatchScope([item], scope);
1576
- return item.get();
1577
- },
1578
- setItem: (item, value) => {
1579
- assertBatchScope([item], scope);
1580
- rememberRollback(item.key, item);
1581
- item.set(value);
1582
- },
1583
- removeItem: item => {
1584
- assertBatchScope([item], scope);
1585
- rememberRollback(item.key, item);
1586
- item.delete();
1587
- }
1588
- };
1589
- try {
1590
- return transaction(tx);
1591
- } catch (error) {
1592
- const rollbackEntries = Array.from(rollback.entries()).reverse();
1593
- if (scope === StorageScope.Memory) {
1594
- rollbackEntries.forEach(([key, record]) => {
1595
- if (record.value === NOT_SET) {
1596
- memoryStore.delete(key);
1597
- } else {
1598
- memoryStore.set(key, record.value);
1599
- }
1600
- notifyKeyListeners(memoryListeners, key);
1601
- });
1602
- } else {
1603
- const groupedKeysToSet = new Map();
1604
- const keysToRemove = [];
1605
- rollbackEntries.forEach(([key, record]) => {
1606
- if (record.kind === "biometric") {
1607
- if (record.value === undefined) {
1608
- getStorageModule().deleteSecureBiometric(key);
1609
- } else {
1610
- getStorageModule().setSecureBiometricWithLevel(key, record.value, record.level);
1611
- }
1612
- return;
1613
- }
1614
- if (record.kind !== "raw") {
1615
- return;
1616
- }
1617
- if (record.value === undefined) {
1618
- keysToRemove.push(key);
1619
- } else {
1620
- const accessControl = record.accessControl ?? secureDefaultAccessControl;
1621
- const existingGroup = groupedKeysToSet.get(accessControl);
1622
- const group = existingGroup ?? {
1623
- keys: [],
1624
- values: []
1625
- };
1626
- group.keys.push(key);
1627
- group.values.push(record.value);
1628
- if (!existingGroup) {
1629
- groupedKeysToSet.set(accessControl, group);
1630
- }
1631
- }
1632
- });
1633
- if (scope === StorageScope.Disk) {
1634
- flushDiskWrites();
1635
- }
1636
- if (scope === StorageScope.Secure) {
1637
- flushSecureWrites();
1638
- }
1639
- groupedKeysToSet.forEach((group, accessControl) => {
1640
- if (scope === StorageScope.Secure) {
1641
- getStorageModule().setSecureAccessControl(accessControl);
1642
- }
1643
- getStorageModule().setBatch(group.keys, group.values, scope);
1644
- group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1645
- });
1646
- if (keysToRemove.length > 0) {
1647
- getStorageModule().removeBatch(keysToRemove, scope);
1648
- keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));
1649
- }
1650
- }
1651
- throw error;
1652
- }
1653
- });
1654
- }
1655
- export function isKeychainLockedError(err) {
1656
- return isLockedStorageErrorCode(getStorageErrorCode(err));
1657
- }
1658
- export function createSecureAuthStorage(config, options) {
1659
- const ns = options?.namespace ?? "auth";
1660
- const result = {};
1661
- for (const key of typedKeys(config)) {
1662
- const itemConfig = config[key];
1663
- const expirationConfig = itemConfig.ttlMs !== undefined ? {
1664
- ttlMs: itemConfig.ttlMs
1665
- } : undefined;
1666
- result[key] = createStorageItem({
1667
- key,
1668
- scope: StorageScope.Secure,
1669
- defaultValue: "",
1670
- namespace: ns,
1671
- ...(itemConfig.biometric !== undefined ? {
1672
- biometric: itemConfig.biometric
1673
- } : {}),
1674
- ...(itemConfig.biometricLevel !== undefined ? {
1675
- biometricLevel: itemConfig.biometricLevel
1676
- } : {}),
1677
- ...(itemConfig.accessControl !== undefined ? {
1678
- accessControl: itemConfig.accessControl
1679
- } : {}),
1680
- ...(expirationConfig !== undefined ? {
1681
- expiration: expirationConfig
1682
- } : {})
1683
- });
1684
- }
1685
- return result;
1686
- }
1687
207
  //# sourceMappingURL=index.js.map