react-native-nitro-storage 0.4.4 → 0.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. package/README.md +237 -862
  2. package/SECURITY.md +26 -0
  3. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +61 -10
  4. package/docs/api-reference.md +217 -0
  5. package/docs/batch-transactions-migrations.md +186 -0
  6. package/docs/benchmarks.md +37 -0
  7. package/docs/mmkv-migration.md +80 -0
  8. package/docs/react-hooks.md +113 -0
  9. package/docs/recipes.md +281 -0
  10. package/docs/secure-storage.md +171 -0
  11. package/docs/web-backends.md +141 -0
  12. package/ios/IOSStorageAdapterCpp.mm +44 -14
  13. package/lib/commonjs/index.js +271 -5
  14. package/lib/commonjs/index.js.map +1 -1
  15. package/lib/commonjs/index.web.js +498 -202
  16. package/lib/commonjs/index.web.js.map +1 -1
  17. package/lib/commonjs/indexeddb-backend.js +129 -7
  18. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  19. package/lib/commonjs/storage-runtime.js +41 -0
  20. package/lib/commonjs/storage-runtime.js.map +1 -0
  21. package/lib/commonjs/web-storage-backend.js +90 -0
  22. package/lib/commonjs/web-storage-backend.js.map +1 -0
  23. package/lib/module/index.js +263 -5
  24. package/lib/module/index.js.map +1 -1
  25. package/lib/module/index.web.js +490 -202
  26. package/lib/module/index.web.js.map +1 -1
  27. package/lib/module/indexeddb-backend.js +129 -7
  28. package/lib/module/indexeddb-backend.js.map +1 -1
  29. package/lib/module/storage-runtime.js +36 -0
  30. package/lib/module/storage-runtime.js.map +1 -0
  31. package/lib/module/web-storage-backend.js +86 -0
  32. package/lib/module/web-storage-backend.js.map +1 -0
  33. package/lib/typescript/index.d.ts +14 -7
  34. package/lib/typescript/index.d.ts.map +1 -1
  35. package/lib/typescript/index.web.d.ts +15 -8
  36. package/lib/typescript/index.web.d.ts.map +1 -1
  37. package/lib/typescript/indexeddb-backend.d.ts +6 -2
  38. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  39. package/lib/typescript/storage-runtime.d.ts +48 -0
  40. package/lib/typescript/storage-runtime.d.ts.map +1 -0
  41. package/lib/typescript/web-storage-backend.d.ts +30 -0
  42. package/lib/typescript/web-storage-backend.d.ts.map +1 -0
  43. package/package.json +21 -8
  44. package/src/index.ts +330 -20
  45. package/src/index.web.ts +673 -245
  46. package/src/indexeddb-backend.ts +147 -6
  47. package/src/storage-runtime.ts +129 -0
  48. package/src/web-storage-backend.ts +129 -0
@@ -9,6 +9,12 @@ static NSString* const kKeychainService = @"com.nitrostorage.keychain";
9
9
  static NSString* const kBiometricKeychainService = @"com.nitrostorage.biometric";
10
10
  static NSString* const kDiskSuiteName = @"com.nitrostorage.disk";
11
11
 
12
+ static std::runtime_error taggedStorageError(const char* code, const std::string& message) {
13
+ return std::runtime_error(
14
+ std::string("[nitro-error:") + code + "] " + message
15
+ );
16
+ }
17
+
12
18
  static NSUserDefaults* NitroDiskDefaults() {
13
19
  static NSUserDefaults* defaults = [[NSUserDefaults alloc] initWithSuiteName:kDiskSuiteName];
14
20
  return defaults ?: [NSUserDefaults standardUserDefaults];
@@ -191,7 +197,8 @@ static std::vector<std::string> keychainAccountsForService(NSString* service, NS
191
197
  std::vector<std::string> keys;
192
198
  OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
193
199
  if (status == errSecInteractionNotAllowed) {
194
- throw std::runtime_error(
200
+ throw taggedStorageError(
201
+ "keychain_locked",
195
202
  "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
196
203
  "The item is not accessible until the device is unlocked."
197
204
  );
@@ -247,7 +254,8 @@ void IOSStorageAdapterCpp::setSecure(const std::string& key, const std::string&
247
254
  const OSStatus addStatus = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
248
255
  if (addStatus != errSecSuccess) {
249
256
  if (addStatus == errSecInteractionNotAllowed) {
250
- throw std::runtime_error(
257
+ throw taggedStorageError(
258
+ "keychain_locked",
251
259
  "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
252
260
  "The item is not accessible until the device is unlocked."
253
261
  );
@@ -261,7 +269,8 @@ void IOSStorageAdapterCpp::setSecure(const std::string& key, const std::string&
261
269
  }
262
270
 
263
271
  if (status == errSecInteractionNotAllowed) {
264
- throw std::runtime_error(
272
+ throw taggedStorageError(
273
+ "keychain_locked",
265
274
  "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
266
275
  "The item is not accessible until the device is unlocked."
267
276
  );
@@ -292,7 +301,8 @@ std::optional<std::string> IOSStorageAdapterCpp::getSecure(const std::string& ke
292
301
  if (str) return std::string([str UTF8String]);
293
302
  }
294
303
  if (status == errSecInteractionNotAllowed) {
295
- throw std::runtime_error(
304
+ throw taggedStorageError(
305
+ "keychain_locked",
296
306
  "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
297
307
  "The item is not accessible until the device is unlocked."
298
308
  );
@@ -312,7 +322,8 @@ void IOSStorageAdapterCpp::deleteSecure(const std::string& key) {
312
322
  NSMutableDictionary* secureQuery = baseKeychainQuery(nsKey, kKeychainService, group);
313
323
  OSStatus secureStatus = SecItemDelete((__bridge CFDictionaryRef)secureQuery);
314
324
  if (secureStatus == errSecInteractionNotAllowed) {
315
- throw std::runtime_error(
325
+ throw taggedStorageError(
326
+ "keychain_locked",
316
327
  "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
317
328
  "The item is not accessible until the device is unlocked."
318
329
  );
@@ -321,7 +332,8 @@ void IOSStorageAdapterCpp::deleteSecure(const std::string& key) {
321
332
  NSMutableDictionary* biometricQuery = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
322
333
  OSStatus biometricStatus = SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
323
334
  if (biometricStatus == errSecInteractionNotAllowed) {
324
- throw std::runtime_error(
335
+ throw taggedStorageError(
336
+ "keychain_locked",
325
337
  "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
326
338
  "The item is not accessible until the device is unlocked."
327
339
  );
@@ -427,7 +439,10 @@ void IOSStorageAdapterCpp::clearSecure() {
427
439
  OSStatus secStatus = SecItemDelete((__bridge CFDictionaryRef)secureQuery);
428
440
  if (secStatus != errSecSuccess && secStatus != errSecItemNotFound) {
429
441
  if (secStatus == errSecInteractionNotAllowed) {
430
- throw std::runtime_error("NitroStorage: Cannot clear secure storage: keychain is locked (errSecInteractionNotAllowed)");
442
+ throw taggedStorageError(
443
+ "keychain_locked",
444
+ "NitroStorage: Cannot clear secure storage: keychain is locked (errSecInteractionNotAllowed)"
445
+ );
431
446
  }
432
447
  throw std::runtime_error(
433
448
  std::string("NitroStorage: clearSecure failed with status ") + std::to_string(secStatus));
@@ -443,7 +458,10 @@ void IOSStorageAdapterCpp::clearSecure() {
443
458
  OSStatus bioStatus = SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
444
459
  if (bioStatus != errSecSuccess && bioStatus != errSecItemNotFound) {
445
460
  if (bioStatus == errSecInteractionNotAllowed) {
446
- throw std::runtime_error("NitroStorage: Cannot clear biometric storage: keychain is locked (errSecInteractionNotAllowed)");
461
+ throw taggedStorageError(
462
+ "keychain_locked",
463
+ "NitroStorage: Cannot clear biometric storage: keychain is locked (errSecInteractionNotAllowed)"
464
+ );
447
465
  }
448
466
  throw std::runtime_error(
449
467
  std::string("NitroStorage: clearSecureBiometric failed with status ") + std::to_string(bioStatus));
@@ -533,7 +551,10 @@ void IOSStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, c
533
551
  if (error || !access) {
534
552
  if (access) CFRelease(access);
535
553
  if (error) CFRelease(error);
536
- throw std::runtime_error("NitroStorage: Failed to create biometric access control");
554
+ throw taggedStorageError(
555
+ "biometric_unavailable",
556
+ "NitroStorage: Failed to create biometric access control"
557
+ );
537
558
  }
538
559
 
539
560
  NSMutableDictionary* attrs = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
@@ -546,14 +567,16 @@ void IOSStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, c
546
567
  try {
547
568
  setSecure(key, *backup);
548
569
  } catch (const std::exception& restoreEx) {
549
- throw std::runtime_error(
570
+ throw taggedStorageError(
571
+ "biometric_unavailable",
550
572
  std::string("NitroStorage: Biometric set failed with status ") +
551
573
  std::to_string(addStatus) +
552
574
  " and previous value restoration also failed: " + restoreEx.what());
553
575
  }
554
576
  }
555
577
  if (addStatus == errSecInteractionNotAllowed) {
556
- throw std::runtime_error(
578
+ throw taggedStorageError(
579
+ "keychain_locked",
557
580
  "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
558
581
  "The item is not accessible until the device is unlocked."
559
582
  );
@@ -586,13 +609,17 @@ std::optional<std::string> IOSStorageAdapterCpp::getSecureBiometric(const std::s
586
609
  if (str) return std::string([str UTF8String]);
587
610
  }
588
611
  if (status == errSecInteractionNotAllowed) {
589
- throw std::runtime_error(
612
+ throw taggedStorageError(
613
+ "keychain_locked",
590
614
  "NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
591
615
  "The item is not accessible until the device is unlocked."
592
616
  );
593
617
  }
594
618
  if (status == errSecUserCanceled || status == errSecAuthFailed) {
595
- throw std::runtime_error("NitroStorage: Biometric authentication failed");
619
+ throw taggedStorageError(
620
+ "authentication_required",
621
+ "NitroStorage: Biometric authentication failed"
622
+ );
596
623
  }
597
624
  return std::nullopt;
598
625
  }
@@ -640,7 +667,10 @@ void IOSStorageAdapterCpp::clearSecureBiometric() {
640
667
  OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
641
668
  if (status != errSecSuccess && status != errSecItemNotFound) {
642
669
  if (status == errSecInteractionNotAllowed) {
643
- throw std::runtime_error("NitroStorage: Cannot clear biometric storage: keychain is locked (errSecInteractionNotAllowed)");
670
+ throw taggedStorageError(
671
+ "keychain_locked",
672
+ "NitroStorage: Cannot clear biometric storage: keychain is locked (errSecInteractionNotAllowed)"
673
+ );
644
674
  }
645
675
  throw std::runtime_error(
646
676
  std::string("NitroStorage: clearSecureBiometric failed with status ") + std::to_string(status));
@@ -29,7 +29,15 @@ Object.defineProperty(exports, "createIndexedDBBackend", {
29
29
  });
30
30
  exports.createSecureAuthStorage = createSecureAuthStorage;
31
31
  exports.createStorageItem = createStorageItem;
32
+ exports.flushWebStorageBackends = flushWebStorageBackends;
32
33
  exports.getBatch = getBatch;
34
+ Object.defineProperty(exports, "getStorageErrorCode", {
35
+ enumerable: true,
36
+ get: function () {
37
+ return _storageRuntime.getStorageErrorCode;
38
+ }
39
+ });
40
+ exports.getWebDiskStorageBackend = getWebDiskStorageBackend;
33
41
  exports.getWebSecureStorageBackend = getWebSecureStorageBackend;
34
42
  exports.isKeychainLockedError = isKeychainLockedError;
35
43
  Object.defineProperty(exports, "migrateFromMMKV", {
@@ -43,6 +51,7 @@ exports.registerMigration = registerMigration;
43
51
  exports.removeBatch = removeBatch;
44
52
  exports.runTransaction = runTransaction;
45
53
  exports.setBatch = setBatch;
54
+ exports.setWebDiskStorageBackend = setWebDiskStorageBackend;
46
55
  exports.setWebSecureStorageBackend = setWebSecureStorageBackend;
47
56
  exports.storage = void 0;
48
57
  Object.defineProperty(exports, "useSetStorage", {
@@ -66,6 +75,7 @@ Object.defineProperty(exports, "useStorageSelector", {
66
75
  var _reactNativeNitroModules = require("react-native-nitro-modules");
67
76
  var _Storage = require("./Storage.types");
68
77
  var _internal = require("./internal");
78
+ var _storageRuntime = require("./storage-runtime");
69
79
  var _migration = require("./migration");
70
80
  var _storageHooks = require("./storage-hooks");
71
81
  var _indexeddbBackend = require("./indexeddb-backend");
@@ -82,6 +92,7 @@ const registeredMigrations = new Map();
82
92
  const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
83
93
  Promise.resolve().then(task);
84
94
  };
95
+ const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
85
96
  let _storageModule = null;
86
97
  function getStorageModule() {
87
98
  if (!_storageModule) {
@@ -94,11 +105,15 @@ const memoryListeners = new Map();
94
105
  const scopedListeners = new Map([[_Storage.StorageScope.Disk, new Map()], [_Storage.StorageScope.Secure, new Map()]]);
95
106
  const scopedUnsubscribers = new Map();
96
107
  const scopedRawCache = new Map([[_Storage.StorageScope.Disk, new Map()], [_Storage.StorageScope.Secure, new Map()]]);
108
+ const pendingDiskWrites = new Map();
109
+ let diskFlushScheduled = false;
110
+ let diskWritesAsync = false;
97
111
  const pendingSecureWrites = new Map();
98
112
  let secureFlushScheduled = false;
99
113
  let secureDefaultAccessControl = _Storage.AccessControl.WhenUnlocked;
100
114
  let metricsObserver;
101
115
  const metricsCounters = new Map();
116
+ const nativeSecureBackend = "platform-secure-storage";
102
117
  function recordMetric(operation, scope, durationMs, keysCount = 1) {
103
118
  const existing = metricsCounters.get(operation);
104
119
  if (!existing) {
@@ -123,11 +138,11 @@ function measureOperation(operation, scope, fn, keysCount = 1) {
123
138
  if (!metricsObserver) {
124
139
  return fn();
125
140
  }
126
- const start = Date.now();
141
+ const start = now();
127
142
  try {
128
143
  return fn();
129
144
  } finally {
130
- recordMetric(operation, scope, Date.now() - start, keysCount);
145
+ recordMetric(operation, scope, now() - start, keysCount);
131
146
  }
132
147
  }
133
148
  function getScopedListeners(scope) {
@@ -184,12 +199,50 @@ function addKeyListener(registry, key, listener) {
184
199
  function readPendingSecureWrite(key) {
185
200
  return pendingSecureWrites.get(key)?.value;
186
201
  }
202
+ function readPendingDiskWrite(key) {
203
+ return pendingDiskWrites.get(key)?.value;
204
+ }
205
+ function hasPendingDiskWrite(key) {
206
+ return pendingDiskWrites.has(key);
207
+ }
187
208
  function hasPendingSecureWrite(key) {
188
209
  return pendingSecureWrites.has(key);
189
210
  }
211
+ function clearPendingDiskWrite(key) {
212
+ pendingDiskWrites.delete(key);
213
+ }
190
214
  function clearPendingSecureWrite(key) {
191
215
  pendingSecureWrites.delete(key);
192
216
  }
217
+ function flushDiskWrites() {
218
+ diskFlushScheduled = false;
219
+ if (pendingDiskWrites.size === 0) {
220
+ return;
221
+ }
222
+ const writes = Array.from(pendingDiskWrites.values());
223
+ pendingDiskWrites.clear();
224
+ const keysToSet = [];
225
+ const valuesToSet = [];
226
+ const keysToRemove = [];
227
+ writes.forEach(({
228
+ key,
229
+ value
230
+ }) => {
231
+ if (value === undefined) {
232
+ keysToRemove.push(key);
233
+ return;
234
+ }
235
+ keysToSet.push(key);
236
+ valuesToSet.push(value);
237
+ });
238
+ const storageModule = getStorageModule();
239
+ if (keysToSet.length > 0) {
240
+ storageModule.setBatch(keysToSet, valuesToSet, _Storage.StorageScope.Disk);
241
+ }
242
+ if (keysToRemove.length > 0) {
243
+ storageModule.removeBatch(keysToRemove, _Storage.StorageScope.Disk);
244
+ }
245
+ }
193
246
  function flushSecureWrites() {
194
247
  secureFlushScheduled = false;
195
248
  if (pendingSecureWrites.size === 0) {
@@ -229,6 +282,17 @@ function flushSecureWrites() {
229
282
  storageModule.removeBatch(keysToRemove, _Storage.StorageScope.Secure);
230
283
  }
231
284
  }
285
+ function scheduleDiskWrite(key, value) {
286
+ pendingDiskWrites.set(key, {
287
+ key,
288
+ value
289
+ });
290
+ if (diskFlushScheduled) {
291
+ return;
292
+ }
293
+ diskFlushScheduled = true;
294
+ runMicrotask(flushDiskWrites);
295
+ }
232
296
  function scheduleSecureWrite(key, value, accessControl) {
233
297
  const pendingWrite = {
234
298
  key,
@@ -249,6 +313,13 @@ function ensureNativeScopeSubscription(scope) {
249
313
  return;
250
314
  }
251
315
  const unsubscribe = getStorageModule().addOnChange(scope, (key, value) => {
316
+ if (scope === _Storage.StorageScope.Disk) {
317
+ if (key === "") {
318
+ pendingDiskWrites.clear();
319
+ } else {
320
+ clearPendingDiskWrite(key);
321
+ }
322
+ }
252
323
  if (scope === _Storage.StorageScope.Secure) {
253
324
  if (key === "") {
254
325
  pendingSecureWrites.clear();
@@ -284,6 +355,9 @@ function getRawValue(key, scope) {
284
355
  const value = memoryStore.get(key);
285
356
  return typeof value === "string" ? value : undefined;
286
357
  }
358
+ if (scope === _Storage.StorageScope.Disk && hasPendingDiskWrite(key)) {
359
+ return readPendingDiskWrite(key);
360
+ }
287
361
  if (scope === _Storage.StorageScope.Secure && hasPendingSecureWrite(key)) {
288
362
  return readPendingSecureWrite(key);
289
363
  }
@@ -296,6 +370,15 @@ function setRawValue(key, value, scope) {
296
370
  notifyKeyListeners(memoryListeners, key);
297
371
  return;
298
372
  }
373
+ if (scope === _Storage.StorageScope.Disk) {
374
+ cacheRawValue(scope, key, value);
375
+ if (diskWritesAsync) {
376
+ scheduleDiskWrite(key, value);
377
+ return;
378
+ }
379
+ flushDiskWrites();
380
+ clearPendingDiskWrite(key);
381
+ }
299
382
  if (scope === _Storage.StorageScope.Secure) {
300
383
  flushSecureWrites();
301
384
  clearPendingSecureWrite(key);
@@ -311,6 +394,15 @@ function removeRawValue(key, scope) {
311
394
  notifyKeyListeners(memoryListeners, key);
312
395
  return;
313
396
  }
397
+ if (scope === _Storage.StorageScope.Disk) {
398
+ cacheRawValue(scope, key, undefined);
399
+ if (diskWritesAsync) {
400
+ scheduleDiskWrite(key, undefined);
401
+ return;
402
+ }
403
+ flushDiskWrites();
404
+ clearPendingDiskWrite(key);
405
+ }
314
406
  if (scope === _Storage.StorageScope.Secure) {
315
407
  flushSecureWrites();
316
408
  clearPendingSecureWrite(key);
@@ -337,6 +429,10 @@ const storage = exports.storage = {
337
429
  notifyAllListeners(memoryListeners);
338
430
  return;
339
431
  }
432
+ if (scope === _Storage.StorageScope.Disk) {
433
+ flushDiskWrites();
434
+ pendingDiskWrites.clear();
435
+ }
340
436
  if (scope === _Storage.StorageScope.Secure) {
341
437
  flushSecureWrites();
342
438
  pendingSecureWrites.clear();
@@ -365,6 +461,9 @@ const storage = exports.storage = {
365
461
  return;
366
462
  }
367
463
  const keyPrefix = (0, _internal.prefixKey)(namespace, "");
464
+ if (scope === _Storage.StorageScope.Disk) {
465
+ flushDiskWrites();
466
+ }
368
467
  if (scope === _Storage.StorageScope.Secure) {
369
468
  flushSecureWrites();
370
469
  }
@@ -388,6 +487,12 @@ const storage = exports.storage = {
388
487
  if (scope === _Storage.StorageScope.Memory) {
389
488
  return memoryStore.has(key);
390
489
  }
490
+ if (scope === _Storage.StorageScope.Disk) {
491
+ flushDiskWrites();
492
+ }
493
+ if (scope === _Storage.StorageScope.Secure) {
494
+ flushSecureWrites();
495
+ }
391
496
  return getStorageModule().has(key, scope);
392
497
  });
393
498
  },
@@ -397,6 +502,12 @@ const storage = exports.storage = {
397
502
  if (scope === _Storage.StorageScope.Memory) {
398
503
  return Array.from(memoryStore.keys());
399
504
  }
505
+ if (scope === _Storage.StorageScope.Disk) {
506
+ flushDiskWrites();
507
+ }
508
+ if (scope === _Storage.StorageScope.Secure) {
509
+ flushSecureWrites();
510
+ }
400
511
  return getStorageModule().getAllKeys(scope);
401
512
  });
402
513
  },
@@ -406,6 +517,12 @@ const storage = exports.storage = {
406
517
  if (scope === _Storage.StorageScope.Memory) {
407
518
  return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
408
519
  }
520
+ if (scope === _Storage.StorageScope.Disk) {
521
+ flushDiskWrites();
522
+ }
523
+ if (scope === _Storage.StorageScope.Secure) {
524
+ flushSecureWrites();
525
+ }
409
526
  return getStorageModule().getKeysByPrefix(prefix, scope);
410
527
  });
411
528
  },
@@ -425,6 +542,12 @@ const storage = exports.storage = {
425
542
  });
426
543
  return result;
427
544
  }
545
+ if (scope === _Storage.StorageScope.Disk) {
546
+ flushDiskWrites();
547
+ }
548
+ if (scope === _Storage.StorageScope.Secure) {
549
+ flushSecureWrites();
550
+ }
428
551
  const values = getStorageModule().getBatch(keys, scope);
429
552
  keys.forEach((key, idx) => {
430
553
  const value = (0, _internal.decodeNativeBatchValue)(values[idx]);
@@ -445,6 +568,12 @@ const storage = exports.storage = {
445
568
  });
446
569
  return result;
447
570
  }
571
+ if (scope === _Storage.StorageScope.Disk) {
572
+ flushDiskWrites();
573
+ }
574
+ if (scope === _Storage.StorageScope.Secure) {
575
+ flushSecureWrites();
576
+ }
448
577
  const keys = getStorageModule().getAllKeys(scope);
449
578
  if (keys.length === 0) return result;
450
579
  const values = getStorageModule().getBatch(keys, scope);
@@ -461,6 +590,12 @@ const storage = exports.storage = {
461
590
  if (scope === _Storage.StorageScope.Memory) {
462
591
  return memoryStore.size;
463
592
  }
593
+ if (scope === _Storage.StorageScope.Disk) {
594
+ flushDiskWrites();
595
+ }
596
+ if (scope === _Storage.StorageScope.Secure) {
597
+ flushSecureWrites();
598
+ }
464
599
  return getStorageModule().size(scope);
465
600
  });
466
601
  },
@@ -475,6 +610,19 @@ const storage = exports.storage = {
475
610
  getStorageModule().setSecureWritesAsync(enabled);
476
611
  });
477
612
  },
613
+ setDiskWritesAsync: enabled => {
614
+ measureOperation("storage:setDiskWritesAsync", _Storage.StorageScope.Disk, () => {
615
+ diskWritesAsync = enabled;
616
+ if (!enabled) {
617
+ flushDiskWrites();
618
+ }
619
+ });
620
+ },
621
+ flushDiskWrites: () => {
622
+ measureOperation("storage:flushDiskWrites", _Storage.StorageScope.Disk, () => {
623
+ flushDiskWrites();
624
+ });
625
+ },
478
626
  flushSecureWrites: () => {
479
627
  measureOperation("storage:flushSecureWrites", _Storage.StorageScope.Secure, () => {
480
628
  flushSecureWrites();
@@ -503,6 +651,67 @@ const storage = exports.storage = {
503
651
  resetMetrics: () => {
504
652
  metricsCounters.clear();
505
653
  },
654
+ getCapabilities: () => ({
655
+ platform: "native",
656
+ backend: {
657
+ disk: "platform-preferences",
658
+ secure: nativeSecureBackend
659
+ },
660
+ writeBuffering: {
661
+ disk: true,
662
+ secure: true
663
+ },
664
+ errorClassification: true
665
+ }),
666
+ getSecurityCapabilities: () => ({
667
+ platform: "native",
668
+ secureStorage: {
669
+ backend: nativeSecureBackend,
670
+ encrypted: "available",
671
+ accessControl: "unknown",
672
+ keychainAccessGroup: "unknown",
673
+ hardwareBacked: "unknown"
674
+ },
675
+ biometric: {
676
+ storage: "unknown",
677
+ prompt: "unknown",
678
+ biometryOnly: "unknown",
679
+ biometryOrPasscode: "unknown"
680
+ },
681
+ metadata: {
682
+ perKey: true,
683
+ listsWithoutValues: true,
684
+ persistsTimestamps: false
685
+ }
686
+ }),
687
+ getSecureMetadata: key => {
688
+ return measureOperation("storage:getSecureMetadata", _Storage.StorageScope.Secure, () => {
689
+ flushSecureWrites();
690
+ const storageModule = getStorageModule();
691
+ const biometricProtected = storageModule.hasSecureBiometric(key);
692
+ const exists = biometricProtected || storageModule.has(key, _Storage.StorageScope.Secure);
693
+ let kind = "missing";
694
+ if (exists) {
695
+ kind = biometricProtected ? "biometric" : "secure";
696
+ }
697
+ return {
698
+ key,
699
+ exists,
700
+ kind,
701
+ backend: nativeSecureBackend,
702
+ encrypted: "available",
703
+ hardwareBacked: "unknown",
704
+ biometricProtected,
705
+ valueExposed: false
706
+ };
707
+ });
708
+ },
709
+ getAllSecureMetadata: () => {
710
+ return measureOperation("storage:getAllSecureMetadata", _Storage.StorageScope.Secure, () => {
711
+ flushSecureWrites();
712
+ return getStorageModule().getAllKeys(_Storage.StorageScope.Secure).map(key => storage.getSecureMetadata(key));
713
+ });
714
+ },
506
715
  getString: (key, scope) => {
507
716
  return measureOperation("storage:getString", scope, () => {
508
717
  return getRawValue(key, scope);
@@ -546,6 +755,15 @@ function setWebSecureStorageBackend(_backend) {
546
755
  function getWebSecureStorageBackend() {
547
756
  return undefined;
548
757
  }
758
+ function setWebDiskStorageBackend(_backend) {
759
+ // Native platforms do not use web disk backends.
760
+ }
761
+ function getWebDiskStorageBackend() {
762
+ return undefined;
763
+ }
764
+ async function flushWebStorageBackends() {
765
+ // Native platforms do not use web storage backends.
766
+ }
549
767
  function canUseRawBatchPath(item) {
550
768
  return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
551
769
  }
@@ -573,6 +791,7 @@ function createStorageItem(config) {
573
791
  const expirationTtlMs = expiration?.ttlMs;
574
792
  const memoryExpiration = expiration && isMemory ? new Map() : null;
575
793
  const readCache = !isMemory && config.readCache === true;
794
+ const coalesceDiskWrites = config.scope === _Storage.StorageScope.Disk && config.coalesceDiskWrites === true;
576
795
  const coalesceSecureWrites = config.scope === _Storage.StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
577
796
  const defaultValue = config.defaultValue;
578
797
  const nonMemoryScope = config.scope === _Storage.StorageScope.Disk ? _Storage.StorageScope.Disk : config.scope === _Storage.StorageScope.Secure ? _Storage.StorageScope.Secure : null;
@@ -620,6 +839,12 @@ function createStorageItem(config) {
620
839
  }
621
840
  return memoryStore.get(storageKey);
622
841
  }
842
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
843
+ const pending = pendingDiskWrites.get(storageKey);
844
+ if (pending !== undefined) {
845
+ return pending.value;
846
+ }
847
+ }
623
848
  if (nonMemoryScope === _Storage.StorageScope.Secure && !isBiometric) {
624
849
  const pending = pendingSecureWrites.get(storageKey);
625
850
  if (pending !== undefined) {
@@ -646,6 +871,13 @@ function createStorageItem(config) {
646
871
  return;
647
872
  }
648
873
  cacheRawValue(nonMemoryScope, storageKey, rawValue);
874
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
875
+ if (coalesceDiskWrites || diskWritesAsync) {
876
+ scheduleDiskWrite(storageKey, rawValue);
877
+ return;
878
+ }
879
+ clearPendingDiskWrite(storageKey);
880
+ }
649
881
  if (coalesceSecureWrites) {
650
882
  scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
651
883
  return;
@@ -662,6 +894,13 @@ function createStorageItem(config) {
662
894
  return;
663
895
  }
664
896
  cacheRawValue(nonMemoryScope, storageKey, undefined);
897
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
898
+ if (coalesceDiskWrites || diskWritesAsync) {
899
+ scheduleDiskWrite(storageKey, undefined);
900
+ return;
901
+ }
902
+ clearPendingDiskWrite(storageKey);
903
+ }
665
904
  if (coalesceSecureWrites) {
666
905
  scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
667
906
  return;
@@ -822,6 +1061,18 @@ function createStorageItem(config) {
822
1061
  const hasItem = () => measureOperation("item:has", config.scope, () => {
823
1062
  if (isMemory) return memoryStore.has(storageKey);
824
1063
  if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
1064
+ if (nonMemoryScope === _Storage.StorageScope.Disk) {
1065
+ const pending = pendingDiskWrites.get(storageKey);
1066
+ if (pending !== undefined) {
1067
+ return pending.value !== undefined;
1068
+ }
1069
+ }
1070
+ if (nonMemoryScope === _Storage.StorageScope.Secure) {
1071
+ const pending = pendingSecureWrites.get(storageKey);
1072
+ if (pending !== undefined) {
1073
+ return pending.value !== undefined;
1074
+ }
1075
+ }
825
1076
  return getStorageModule().has(storageKey, config.scope);
826
1077
  });
827
1078
  const subscribe = callback => {
@@ -882,6 +1133,13 @@ function getBatch(items, scope) {
882
1133
  const keysToFetch = [];
883
1134
  const keyIndexes = [];
884
1135
  items.forEach((item, index) => {
1136
+ if (scope === _Storage.StorageScope.Disk) {
1137
+ const pending = pendingDiskWrites.get(item.key);
1138
+ if (pending !== undefined) {
1139
+ rawValues[index] = pending.value;
1140
+ return;
1141
+ }
1142
+ }
885
1143
  if (scope === _Storage.StorageScope.Secure) {
886
1144
  const pending = pendingSecureWrites.get(item.key);
887
1145
  if (pending !== undefined) {
@@ -1000,6 +1258,7 @@ function setBatch(items, scope) {
1000
1258
  });
1001
1259
  return;
1002
1260
  }
1261
+ flushDiskWrites();
1003
1262
  const useRawBatchPath = items.every(({
1004
1263
  item
1005
1264
  }) => canUseRawBatchPath(asInternal(item)));
@@ -1024,6 +1283,9 @@ function removeBatch(items, scope) {
1024
1283
  return;
1025
1284
  }
1026
1285
  const keys = items.map(item => item.key);
1286
+ if (scope === _Storage.StorageScope.Disk) {
1287
+ flushDiskWrites();
1288
+ }
1027
1289
  if (scope === _Storage.StorageScope.Secure) {
1028
1290
  flushSecureWrites();
1029
1291
  }
@@ -1069,6 +1331,9 @@ function migrateToLatest(scope = _Storage.StorageScope.Disk) {
1069
1331
  function runTransaction(scope, transaction) {
1070
1332
  return measureOperation("transaction:run", scope, () => {
1071
1333
  (0, _internal.assertValidScope)(scope);
1334
+ if (scope === _Storage.StorageScope.Disk) {
1335
+ flushDiskWrites();
1336
+ }
1072
1337
  if (scope === _Storage.StorageScope.Secure) {
1073
1338
  flushSecureWrites();
1074
1339
  }
@@ -1135,6 +1400,9 @@ function runTransaction(scope, transaction) {
1135
1400
  valuesToSet.push(previousValue);
1136
1401
  }
1137
1402
  });
1403
+ if (scope === _Storage.StorageScope.Disk) {
1404
+ flushDiskWrites();
1405
+ }
1138
1406
  if (scope === _Storage.StorageScope.Secure) {
1139
1407
  flushSecureWrites();
1140
1408
  }
@@ -1152,9 +1420,7 @@ function runTransaction(scope, transaction) {
1152
1420
  });
1153
1421
  }
1154
1422
  function isKeychainLockedError(err) {
1155
- if (!(err instanceof Error)) return false;
1156
- const msg = err.message;
1157
- return msg.includes("errSecInteractionNotAllowed") || msg.includes("UserNotAuthenticatedException") || msg.includes("KeyStoreException") || msg.includes("KeyPermanentlyInvalidatedException") || msg.includes("InvalidKeyException") || msg.includes("android.security.keystore");
1423
+ return (0, _storageRuntime.isLockedStorageErrorCode)((0, _storageRuntime.getStorageErrorCode)(err));
1158
1424
  }
1159
1425
  function createSecureAuthStorage(config, options) {
1160
1426
  const ns = options?.namespace ?? "auth";