react-native-nitro-storage 0.9.0 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (77) hide show
  1. package/CHANGELOG.md +101 -29
  2. package/README.md +83 -26
  3. package/android/build.gradle +3 -1
  4. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
  6. package/cpp/bindings/HybridStorage.cpp +106 -283
  7. package/cpp/bindings/HybridStorage.hpp +16 -9
  8. package/docs/api-reference.md +75 -19
  9. package/docs/benchmarks.md +6 -1
  10. package/docs/keychain-lifecycle-testing.md +36 -0
  11. package/docs/recipes.md +1 -2
  12. package/docs/secure-storage.md +45 -9
  13. package/ios/IOSStorageAdapterCpp.mm +391 -175
  14. package/lib/commonjs/capabilities.js +3 -1
  15. package/lib/commonjs/capabilities.js.map +1 -1
  16. package/lib/commonjs/core/durability.js +111 -45
  17. package/lib/commonjs/core/durability.js.map +1 -1
  18. package/lib/commonjs/index.js +15 -5
  19. package/lib/commonjs/index.js.map +1 -1
  20. package/lib/commonjs/index.web.js +219 -97
  21. package/lib/commonjs/index.web.js.map +1 -1
  22. package/lib/commonjs/internal.js +3 -11
  23. package/lib/commonjs/internal.js.map +1 -1
  24. package/lib/commonjs/shared.js +62 -2
  25. package/lib/commonjs/shared.js.map +1 -1
  26. package/lib/commonjs/storage-core.js +883 -163
  27. package/lib/commonjs/storage-core.js.map +1 -1
  28. package/lib/commonjs/storage-runtime.js +5 -1
  29. package/lib/commonjs/storage-runtime.js.map +1 -1
  30. package/lib/commonjs/testing.js +13 -0
  31. package/lib/commonjs/testing.js.map +1 -1
  32. package/lib/module/capabilities.js +2 -1
  33. package/lib/module/capabilities.js.map +1 -1
  34. package/lib/module/core/durability.js +111 -45
  35. package/lib/module/core/durability.js.map +1 -1
  36. package/lib/module/index.js +12 -8
  37. package/lib/module/index.js.map +1 -1
  38. package/lib/module/index.web.js +215 -99
  39. package/lib/module/index.web.js.map +1 -1
  40. package/lib/module/internal.js +2 -9
  41. package/lib/module/internal.js.map +1 -1
  42. package/lib/module/shared.js +60 -2
  43. package/lib/module/shared.js.map +1 -1
  44. package/lib/module/storage-core.js +884 -164
  45. package/lib/module/storage-core.js.map +1 -1
  46. package/lib/module/storage-runtime.js +4 -1
  47. package/lib/module/storage-runtime.js.map +1 -1
  48. package/lib/module/testing.js +8 -1
  49. package/lib/module/testing.js.map +1 -1
  50. package/lib/typescript/capabilities.d.ts +2 -1
  51. package/lib/typescript/capabilities.d.ts.map +1 -1
  52. package/lib/typescript/core/durability.d.ts +8 -4
  53. package/lib/typescript/core/durability.d.ts.map +1 -1
  54. package/lib/typescript/index.d.ts +2 -2
  55. package/lib/typescript/index.d.ts.map +1 -1
  56. package/lib/typescript/index.web.d.ts +3 -29
  57. package/lib/typescript/index.web.d.ts.map +1 -1
  58. package/lib/typescript/internal.d.ts +0 -2
  59. package/lib/typescript/internal.d.ts.map +1 -1
  60. package/lib/typescript/shared.d.ts +19 -0
  61. package/lib/typescript/shared.d.ts.map +1 -1
  62. package/lib/typescript/storage-core.d.ts +15 -6
  63. package/lib/typescript/storage-core.d.ts.map +1 -1
  64. package/lib/typescript/storage-runtime.d.ts +2 -1
  65. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  66. package/lib/typescript/testing.d.ts +1 -1
  67. package/lib/typescript/testing.d.ts.map +1 -1
  68. package/package.json +2 -2
  69. package/src/capabilities.ts +3 -1
  70. package/src/core/durability.ts +140 -43
  71. package/src/index.ts +20 -10
  72. package/src/index.web.ts +328 -166
  73. package/src/internal.ts +0 -14
  74. package/src/shared.ts +85 -0
  75. package/src/storage-core.ts +1198 -232
  76. package/src/storage-runtime.ts +6 -0
  77. package/src/testing.ts +8 -1
package/src/index.web.ts CHANGED
@@ -2,6 +2,7 @@ import { resolveWebWriteBuffering } from "./capabilities";
2
2
  import { unescapeCollidingRawValue } from "./internal";
3
3
  import {
4
4
  assertAccessControlLevel,
5
+ createStorageCompositeError,
5
6
  assertBiometricLevel,
6
7
  notifyAllListeners,
7
8
  notifyKeyListeners,
@@ -16,6 +17,7 @@ import type {
16
17
  SecurityCapabilities,
17
18
  StorageCapabilities,
18
19
  } from "./storage-runtime";
20
+ import type { Storage } from "./Storage.nitro";
19
21
  import type { AccessControl } from "./Storage.types";
20
22
  import { StorageScope, BiometricLevel } from "./Storage.types";
21
23
  import {
@@ -37,6 +39,8 @@ export type {
37
39
  StorageMetricsObserver,
38
40
  StorageSelectorListener,
39
41
  StorageSelectorSubscribeOptions,
42
+ StorageCompositeError,
43
+ StorageCompensationError,
40
44
  StorageVersion,
41
45
  Validator,
42
46
  VersionedValue,
@@ -44,9 +48,11 @@ export type {
44
48
  export { isKeychainLockedError } from "./shared";
45
49
 
46
50
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
51
+ export type { Storage } from "./Storage.nitro";
47
52
  export { migrateFromMMKV } from "./migration";
48
53
  export {
49
54
  getStorageErrorCode,
55
+ isStorageError,
50
56
  type SecureStorageMetadata,
51
57
  type SecurityCapabilities,
52
58
  type StorageCapabilities,
@@ -85,37 +91,6 @@ export type {
85
91
  } from "./storage-core";
86
92
  export type { PlatformScope, PlatformStorage } from "./storage-platform";
87
93
 
88
- export type Storage = {
89
- name: string;
90
- equals: (other: unknown) => boolean;
91
- dispose: () => void;
92
- set(key: string, value: string, scope: number): void;
93
- get(key: string, scope: number): string | undefined;
94
- remove(key: string, scope: number): void;
95
- clear(scope: number): void;
96
- has(key: string, scope: number): boolean;
97
- getAllKeys(scope: number): string[];
98
- getKeysByPrefix(prefix: string, scope: number): string[];
99
- size(scope: number): number;
100
- setBatch(keys: string[], values: string[], scope: number): void;
101
- getBatch(keys: string[], scope: number): (string | undefined)[];
102
- removeBatch(keys: string[], scope: number): void;
103
- removeByPrefix(prefix: string, scope: number): void;
104
- addOnChange(
105
- scope: number,
106
- callback: (key: string, value: string | undefined) => void,
107
- ): () => void;
108
- setSecureAccessControl(level: number): void;
109
- setSecureWritesAsync(enabled: boolean): void;
110
- setKeychainAccessGroup(group: string): void;
111
- setSecureBiometric(key: string, value: string): void;
112
- setSecureBiometricWithLevel(key: string, value: string, level: number): void;
113
- getSecureBiometric(key: string): string | undefined;
114
- deleteSecureBiometric(key: string): void;
115
- hasSecureBiometric(key: string): boolean;
116
- clearSecureBiometric(): void;
117
- };
118
-
119
94
  const webScopeKeyIndex: Record<NonMemoryScope, Set<string>> = {
120
95
  [StorageScope.Disk]: new Set(),
121
96
  [StorageScope.Secure]: new Set(),
@@ -182,9 +157,16 @@ function createWebStorageError(
182
157
  const backendName = getBackendName(scope, backend);
183
158
  const message =
184
159
  error instanceof Error ? error.message : String(error ?? "Unknown error");
185
- return new Error(
160
+ const wrapped = new Error(
186
161
  `NitroStorage(web): ${operation} failed for ${backendName}: ${message}`,
187
162
  );
163
+ Object.defineProperty(wrapped, "cause", {
164
+ configurable: true,
165
+ enumerable: false,
166
+ value: error,
167
+ writable: false,
168
+ });
169
+ return wrapped;
188
170
  }
189
171
 
190
172
  function withWebBackendOperation<T>(
@@ -236,15 +218,12 @@ function getWebScopeKeyIndex(scope: NonMemoryScope): Set<string> {
236
218
  return webScopeKeyIndex[scope];
237
219
  }
238
220
 
239
- function hydrateWebScopeKeyIndex(scope: NonMemoryScope): void {
240
- if (hydratedWebScopeKeyIndex.has(scope)) {
241
- return;
242
- }
243
-
244
- const backend = getWebBackend(scope);
221
+ function replaceWebScopeKeyIndex(
222
+ scope: NonMemoryScope,
223
+ keys: readonly string[],
224
+ ): void {
245
225
  const keyIndex = getWebScopeKeyIndex(scope);
246
226
  keyIndex.clear();
247
- const keys = backend?.getAllKeys() ?? [];
248
227
  for (const key of keys) {
249
228
  if (scope === StorageScope.Disk) {
250
229
  keyIndex.add(key);
@@ -259,7 +238,63 @@ function hydrateWebScopeKeyIndex(scope: NonMemoryScope): void {
259
238
  keyIndex.add(fromBiometricStorageKey(key));
260
239
  }
261
240
  }
262
- hydratedWebScopeKeyIndex.add(scope);
241
+ }
242
+
243
+ function invalidateWebScopeKeyIndex(scope: NonMemoryScope): void {
244
+ getWebScopeKeyIndex(scope).clear();
245
+ hydratedWebScopeKeyIndex.delete(scope);
246
+ getInternals().clearScopeRawCache(scope);
247
+ }
248
+
249
+ function hydrateWebScopeKeyIndex(scope: NonMemoryScope): void {
250
+ if (hydratedWebScopeKeyIndex.has(scope)) {
251
+ return;
252
+ }
253
+
254
+ try {
255
+ const backend = getWebBackend(scope);
256
+ const keys = backend?.getAllKeys() ?? [];
257
+ replaceWebScopeKeyIndex(scope, keys);
258
+ hydratedWebScopeKeyIndex.add(scope);
259
+ } catch (error) {
260
+ invalidateWebScopeKeyIndex(scope);
261
+ throw error;
262
+ }
263
+ }
264
+
265
+ function reconcileWebScopeKeyIndex(scope: NonMemoryScope): void {
266
+ try {
267
+ const keys = withWebBackendOperation(
268
+ scope,
269
+ "reconcile-key-index",
270
+ (backend) => backend.getAllKeys(),
271
+ );
272
+ replaceWebScopeKeyIndex(scope, keys);
273
+ hydratedWebScopeKeyIndex.add(scope);
274
+ } catch (error) {
275
+ invalidateWebScopeKeyIndex(scope);
276
+ throw error;
277
+ }
278
+ }
279
+
280
+ function throwAfterWebMutationFailure(
281
+ scope: NonMemoryScope,
282
+ operation: string,
283
+ primary: unknown,
284
+ contexts: readonly { label: string; error: unknown }[] = [],
285
+ ): never {
286
+ try {
287
+ reconcileWebScopeKeyIndex(scope);
288
+ } catch (reconciliationError) {
289
+ throw createStorageCompositeError(operation, primary, [
290
+ ...contexts,
291
+ { label: "index reconciliation", error: reconciliationError },
292
+ ]);
293
+ }
294
+ if (contexts.length > 0) {
295
+ throw createStorageCompositeError(operation, primary, contexts);
296
+ }
297
+ throw primary;
263
298
  }
264
299
 
265
300
  function ensureWebScopeKeyIndex(scope: NonMemoryScope): Set<string> {
@@ -284,13 +319,34 @@ function applyExternalChangeEvent(
284
319
  const oldValue = getInternals().readCachedRawValue(
285
320
  StorageScope.Secure,
286
321
  plainKey,
322
+ "plain",
287
323
  );
324
+ getInternals().invalidateRawCache(StorageScope.Secure, plainKey);
288
325
  if (newValue === null) {
289
- ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
290
- getInternals().cacheRawValue(StorageScope.Secure, plainKey, undefined);
326
+ if (
327
+ !withWebBackendOperation(
328
+ StorageScope.Secure,
329
+ "external-sync:getBiometricItem",
330
+ (backend) =>
331
+ backend.getAllKeys().includes(toBiometricStorageKey(plainKey)),
332
+ )
333
+ ) {
334
+ ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
335
+ }
336
+ getInternals().cacheRawValue(
337
+ StorageScope.Secure,
338
+ plainKey,
339
+ undefined,
340
+ "plain",
341
+ );
291
342
  } else {
292
343
  ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
293
- getInternals().cacheRawValue(StorageScope.Secure, plainKey, newValue);
344
+ getInternals().cacheRawValue(
345
+ StorageScope.Secure,
346
+ plainKey,
347
+ newValue,
348
+ "plain",
349
+ );
294
350
  }
295
351
  notifyKeyListeners(
296
352
  getInternals().getScopedListeners(StorageScope.Secure),
@@ -312,21 +368,34 @@ function applyExternalChangeEvent(
312
368
  const oldValue = getInternals().readCachedRawValue(
313
369
  StorageScope.Secure,
314
370
  plainKey,
371
+ "biometric",
315
372
  );
373
+ getInternals().invalidateRawCache(StorageScope.Secure, plainKey);
316
374
  if (newValue === null) {
317
375
  if (
318
- withWebBackendOperation(
376
+ !withWebBackendOperation(
319
377
  StorageScope.Secure,
320
378
  "external-sync:getItem",
321
- (backend) => backend.getItem(toSecureStorageKey(plainKey)),
322
- ) === null
379
+ (backend) =>
380
+ backend.getAllKeys().includes(toSecureStorageKey(plainKey)),
381
+ )
323
382
  ) {
324
383
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
325
384
  }
326
- getInternals().cacheRawValue(StorageScope.Secure, plainKey, undefined);
385
+ getInternals().cacheRawValue(
386
+ StorageScope.Secure,
387
+ plainKey,
388
+ undefined,
389
+ "biometric",
390
+ );
327
391
  } else {
328
392
  ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
329
- getInternals().cacheRawValue(StorageScope.Secure, plainKey, newValue);
393
+ getInternals().cacheRawValue(
394
+ StorageScope.Secure,
395
+ plainKey,
396
+ newValue,
397
+ "biometric",
398
+ );
330
399
  }
331
400
  notifyKeyListeners(
332
401
  getInternals().getScopedListeners(StorageScope.Secure),
@@ -456,9 +525,13 @@ const WebStorage: Storage = {
456
525
  }
457
526
  const storageKey =
458
527
  scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
459
- withWebBackendOperation(scope, "set", (backend) => {
460
- backend.setItem(storageKey, value);
461
- });
528
+ try {
529
+ withWebBackendOperation(scope, "set", (backend) => {
530
+ backend.setItem(storageKey, value);
531
+ });
532
+ } catch (error) {
533
+ throwAfterWebMutationFailure(scope, "set", error);
534
+ }
462
535
  ensureWebScopeKeyIndex(scope).add(key);
463
536
  notifyKeyListeners(getInternals().getScopedListeners(scope), key);
464
537
  },
@@ -477,22 +550,26 @@ const WebStorage: Storage = {
477
550
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
478
551
  return;
479
552
  }
480
- if (scope === StorageScope.Secure) {
481
- withWebBackendOperation(scope, "remove", (backend) => {
482
- if (backend.removeMany) {
483
- backend.removeMany([
484
- toSecureStorageKey(key),
485
- toBiometricStorageKey(key),
486
- ]);
487
- return;
488
- }
489
- backend.removeItem(toSecureStorageKey(key));
490
- backend.removeItem(toBiometricStorageKey(key));
491
- });
492
- } else {
493
- withWebBackendOperation(scope, "remove", (backend) => {
494
- backend.removeItem(key);
495
- });
553
+ try {
554
+ if (scope === StorageScope.Secure) {
555
+ withWebBackendOperation(scope, "remove", (backend) => {
556
+ if (backend.removeMany) {
557
+ backend.removeMany([
558
+ toSecureStorageKey(key),
559
+ toBiometricStorageKey(key),
560
+ ]);
561
+ return;
562
+ }
563
+ backend.removeItem(toSecureStorageKey(key));
564
+ backend.removeItem(toBiometricStorageKey(key));
565
+ });
566
+ } else {
567
+ withWebBackendOperation(scope, "remove", (backend) => {
568
+ backend.removeItem(key);
569
+ });
570
+ }
571
+ } catch (error) {
572
+ throwAfterWebMutationFailure(scope, "remove", error);
496
573
  }
497
574
  ensureWebScopeKeyIndex(scope).delete(key);
498
575
  notifyKeyListeners(getInternals().getScopedListeners(scope), key);
@@ -501,9 +578,13 @@ const WebStorage: Storage = {
501
578
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
502
579
  return;
503
580
  }
504
- withWebBackendOperation(scope, "clear", (backend) => {
505
- backend.clear();
506
- });
581
+ try {
582
+ withWebBackendOperation(scope, "clear", (backend) => {
583
+ backend.clear();
584
+ });
585
+ } catch (error) {
586
+ throwAfterWebMutationFailure(scope, "clear", error);
587
+ }
507
588
  ensureWebScopeKeyIndex(scope).clear();
508
589
  notifyAllListeners(getInternals().getScopedListeners(scope));
509
590
  },
@@ -528,15 +609,19 @@ const WebStorage: Storage = {
528
609
  value,
529
610
  ]);
530
611
  });
531
- withWebBackendOperation(scope, "setBatch", (backend) => {
532
- if (backend.setMany) {
533
- backend.setMany(entries);
534
- return;
535
- }
536
- entries.forEach(([storageKey, value]) => {
537
- backend.setItem(storageKey, value);
612
+ try {
613
+ withWebBackendOperation(scope, "setBatch", (backend) => {
614
+ if (backend.setMany) {
615
+ backend.setMany(entries);
616
+ return;
617
+ }
618
+ entries.forEach(([storageKey, value]) => {
619
+ backend.setItem(storageKey, value);
620
+ });
538
621
  });
539
- });
622
+ } catch (error) {
623
+ throwAfterWebMutationFailure(scope, "setBatch", error);
624
+ }
540
625
  const keyIndex = ensureWebScopeKeyIndex(scope);
541
626
  entries.forEach(([storageKey]) =>
542
627
  keyIndex.add(
@@ -570,30 +655,34 @@ const WebStorage: Storage = {
570
655
  return;
571
656
  }
572
657
 
573
- if (scope === StorageScope.Secure) {
574
- const storageKeys = keys.flatMap((key) => [
575
- toSecureStorageKey(key),
576
- toBiometricStorageKey(key),
577
- ]);
578
- withWebBackendOperation(scope, "removeBatch", (backend) => {
579
- if (backend.removeMany) {
580
- backend.removeMany(storageKeys);
581
- return;
582
- }
583
- storageKeys.forEach((storageKey) => {
584
- backend.removeItem(storageKey);
658
+ try {
659
+ if (scope === StorageScope.Secure) {
660
+ const storageKeys = keys.flatMap((key) => [
661
+ toSecureStorageKey(key),
662
+ toBiometricStorageKey(key),
663
+ ]);
664
+ withWebBackendOperation(scope, "removeBatch", (backend) => {
665
+ if (backend.removeMany) {
666
+ backend.removeMany(storageKeys);
667
+ return;
668
+ }
669
+ storageKeys.forEach((storageKey) => {
670
+ backend.removeItem(storageKey);
671
+ });
585
672
  });
586
- });
587
- } else {
588
- withWebBackendOperation(scope, "removeBatch", (backend) => {
589
- if (backend.removeMany) {
590
- backend.removeMany(keys);
591
- return;
592
- }
593
- keys.forEach((key) => {
594
- backend.removeItem(key);
673
+ } else {
674
+ withWebBackendOperation(scope, "removeBatch", (backend) => {
675
+ if (backend.removeMany) {
676
+ backend.removeMany(keys);
677
+ return;
678
+ }
679
+ keys.forEach((key) => {
680
+ backend.removeItem(key);
681
+ });
595
682
  });
596
- });
683
+ }
684
+ } catch (error) {
685
+ throwAfterWebMutationFailure(scope, "removeBatch", error);
597
686
  }
598
687
 
599
688
  const keyIndex = ensureWebScopeKeyIndex(scope);
@@ -662,19 +751,8 @@ const WebStorage: Storage = {
662
751
  },
663
752
  setSecureBiometricWithLevel: (key: string, value: string, level: number) => {
664
753
  assertBiometricLevel(level);
665
- if (level === BiometricLevel.None) {
666
- withWebBackendOperation(StorageScope.Secure, "setSecure", (backend) => {
667
- backend.removeItem(toBiometricStorageKey(key));
668
- backend.setItem(toSecureStorageKey(key), value);
669
- });
670
- ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
671
- notifyKeyListeners(
672
- getInternals().getScopedListeners(StorageScope.Secure),
673
- key,
674
- );
675
- return;
676
- }
677
754
  if (
755
+ level !== BiometricLevel.None &&
678
756
  typeof __DEV__ !== "undefined" &&
679
757
  __DEV__ &&
680
758
  !hasWarnedAboutWebBiometricFallback
@@ -684,14 +762,71 @@ const WebStorage: Storage = {
684
762
  "[NitroStorage] Biometric storage is not supported on web. Using localStorage.",
685
763
  );
686
764
  }
687
- withWebBackendOperation(
765
+
766
+ const biometricStorageKey = toBiometricStorageKey(key);
767
+ const plainStorageKey = toSecureStorageKey(key);
768
+ const previous = withWebBackendOperation(
688
769
  StorageScope.Secure,
689
- "setSecureBiometric",
690
- (backend) => {
691
- backend.setItem(toBiometricStorageKey(key), value);
692
- backend.removeItem(toSecureStorageKey(key));
693
- },
770
+ "setSecureBiometric:readPrevious",
771
+ (backend) => ({
772
+ biometric: backend.getItem(biometricStorageKey),
773
+ plain: backend.getItem(plainStorageKey),
774
+ }),
694
775
  );
776
+ getInternals().invalidateRawCache(StorageScope.Secure, key);
777
+
778
+ const restoreErrors: { label: string; error: unknown }[] = [];
779
+ const restore = (
780
+ storageKey: string,
781
+ previousValue: string | null,
782
+ label: string,
783
+ ): void => {
784
+ try {
785
+ withWebBackendOperation(
786
+ StorageScope.Secure,
787
+ `setSecureBiometric:rollback:${label}`,
788
+ (backend) => {
789
+ if (previousValue === null) {
790
+ backend.removeItem(storageKey);
791
+ } else {
792
+ backend.setItem(storageKey, previousValue);
793
+ }
794
+ },
795
+ );
796
+ } catch (error) {
797
+ restoreErrors.push({ label: `rollback ${label}`, error });
798
+ }
799
+ };
800
+
801
+ try {
802
+ withWebBackendOperation(
803
+ StorageScope.Secure,
804
+ level === BiometricLevel.None
805
+ ? "setSecureBiometric:demote"
806
+ : "setSecureBiometric:promote",
807
+ (backend) => {
808
+ if (level === BiometricLevel.None) {
809
+ backend.removeItem(biometricStorageKey);
810
+ backend.setItem(plainStorageKey, value);
811
+ return;
812
+ }
813
+ backend.setItem(biometricStorageKey, value);
814
+ backend.removeItem(plainStorageKey);
815
+ },
816
+ );
817
+ } catch (error) {
818
+ restore(biometricStorageKey, previous.biometric, "biometric");
819
+ restore(plainStorageKey, previous.plain, "plain");
820
+ throwAfterWebMutationFailure(
821
+ StorageScope.Secure,
822
+ level === BiometricLevel.None
823
+ ? "secure demotion"
824
+ : "biometric promotion",
825
+ error,
826
+ restoreErrors,
827
+ );
828
+ }
829
+
695
830
  ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
696
831
  notifyKeyListeners(
697
832
  getInternals().getScopedListeners(StorageScope.Secure),
@@ -707,20 +842,36 @@ const WebStorage: Storage = {
707
842
  return value ?? undefined;
708
843
  },
709
844
  deleteSecureBiometric: (key: string) => {
710
- withWebBackendOperation(
711
- StorageScope.Secure,
712
- "deleteSecureBiometric",
713
- (backend) => {
714
- backend.removeItem(toBiometricStorageKey(key));
715
- },
716
- );
717
- if (
845
+ try {
718
846
  withWebBackendOperation(
847
+ StorageScope.Secure,
848
+ "deleteSecureBiometric",
849
+ (backend) => {
850
+ backend.removeItem(toBiometricStorageKey(key));
851
+ },
852
+ );
853
+ } catch (error) {
854
+ throwAfterWebMutationFailure(
855
+ StorageScope.Secure,
856
+ "delete biometric",
857
+ error,
858
+ );
859
+ }
860
+ let hasPlainValue: string | null;
861
+ try {
862
+ hasPlainValue = withWebBackendOperation(
719
863
  StorageScope.Secure,
720
864
  "deleteSecureBiometric:getItem",
721
865
  (backend) => backend.getItem(toSecureStorageKey(key)),
722
- ) === null
723
- ) {
866
+ );
867
+ } catch (error) {
868
+ throwAfterWebMutationFailure(
869
+ StorageScope.Secure,
870
+ "delete biometric",
871
+ error,
872
+ );
873
+ }
874
+ if (hasPlainValue === null) {
724
875
  ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
725
876
  }
726
877
  notifyKeyListeners(
@@ -729,54 +880,65 @@ const WebStorage: Storage = {
729
880
  );
730
881
  },
731
882
  hasSecureBiometric: (key: string) => {
732
- return (
733
- withWebBackendOperation(
883
+ try {
884
+ return withWebBackendOperation(
734
885
  StorageScope.Secure,
735
886
  "hasSecureBiometric",
736
- (backend) => backend.getItem(toBiometricStorageKey(key)),
737
- ) !== null
738
- );
887
+ (backend) => backend.getAllKeys().includes(toBiometricStorageKey(key)),
888
+ );
889
+ } catch (error) {
890
+ invalidateWebScopeKeyIndex(StorageScope.Secure);
891
+ throw error;
892
+ }
739
893
  },
740
894
  clearSecureBiometric: () => {
741
- const storageKeys = withWebBackendOperation(
742
- StorageScope.Secure,
743
- "clearSecureBiometric:getAllKeys",
744
- (backend) => backend.getAllKeys(),
745
- );
895
+ let storageKeys: string[];
896
+ try {
897
+ storageKeys = withWebBackendOperation(
898
+ StorageScope.Secure,
899
+ "clearSecureBiometric:getAllKeys",
900
+ (backend) => backend.getAllKeys(),
901
+ );
902
+ } catch (error) {
903
+ invalidateWebScopeKeyIndex(StorageScope.Secure);
904
+ throw error;
905
+ }
746
906
  const keysToNotify = storageKeys
747
907
  .filter((key) => key.startsWith(BIOMETRIC_WEB_PREFIX))
748
908
  .map((key) => fromBiometricStorageKey(key));
749
909
  if (keysToNotify.length === 0) {
910
+ replaceWebScopeKeyIndex(StorageScope.Secure, storageKeys);
911
+ hydratedWebScopeKeyIndex.add(StorageScope.Secure);
750
912
  return;
751
913
  }
752
- withWebBackendOperation(
753
- StorageScope.Secure,
754
- "clearSecureBiometric",
755
- (backend) => {
756
- const biometricKeys = keysToNotify.map((key) =>
757
- toBiometricStorageKey(key),
758
- );
759
- if (backend.removeMany) {
760
- backend.removeMany(biometricKeys);
761
- return;
762
- }
763
- biometricKeys.forEach((storageKey) => {
764
- backend.removeItem(storageKey);
765
- });
766
- },
767
- );
768
- const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
769
- keysToNotify.forEach((key) => {
770
- if (
771
- withWebBackendOperation(
772
- StorageScope.Secure,
773
- "clearSecureBiometric:getItem",
774
- (backend) => backend.getItem(toSecureStorageKey(key)),
775
- ) === null
776
- ) {
777
- keyIndex.delete(key);
778
- }
779
- });
914
+
915
+ try {
916
+ withWebBackendOperation(
917
+ StorageScope.Secure,
918
+ "clearSecureBiometric",
919
+ (backend) => {
920
+ const biometricKeys = keysToNotify.map((key) =>
921
+ toBiometricStorageKey(key),
922
+ );
923
+ if (backend.removeMany) {
924
+ backend.removeMany(biometricKeys);
925
+ return;
926
+ }
927
+ biometricKeys.forEach((storageKey) => {
928
+ backend.removeItem(storageKey);
929
+ });
930
+ },
931
+ );
932
+ } catch (error) {
933
+ throwAfterWebMutationFailure(
934
+ StorageScope.Secure,
935
+ "clear biometric",
936
+ error,
937
+ );
938
+ }
939
+
940
+ reconcileWebScopeKeyIndex(StorageScope.Secure);
941
+
780
942
  const listeners = getInternals().getScopedListeners(StorageScope.Secure);
781
943
  keysToNotify.forEach((key) => {
782
944
  notifyKeyListeners(listeners, key);