react-native-nitro-storage 0.8.0 → 0.10.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.
- package/CHANGELOG.md +107 -25
- package/README.md +140 -27
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +5 -2
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +240 -34
- package/cpp/bindings/HybridStorage.cpp +109 -293
- package/cpp/bindings/HybridStorage.hpp +17 -10
- package/docs/api-reference.md +75 -19
- package/docs/benchmarks.md +6 -1
- package/docs/keychain-lifecycle-testing.md +36 -0
- package/docs/recipes.md +1 -2
- package/docs/secure-storage.md +45 -9
- package/ios/IOSStorageAdapterCpp.mm +391 -175
- package/lib/commonjs/capabilities.js +3 -1
- package/lib/commonjs/capabilities.js.map +1 -1
- package/lib/commonjs/core/durability.js +110 -43
- package/lib/commonjs/core/durability.js.map +1 -1
- package/lib/commonjs/index.js +15 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +219 -97
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +3 -11
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/shared.js +62 -2
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +859 -145
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +5 -1
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/commonjs/testing.js +13 -0
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/module/capabilities.js +2 -1
- package/lib/module/capabilities.js.map +1 -1
- package/lib/module/core/durability.js +110 -43
- package/lib/module/core/durability.js.map +1 -1
- package/lib/module/index.js +12 -8
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +215 -99
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +2 -9
- package/lib/module/internal.js.map +1 -1
- package/lib/module/shared.js +60 -2
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +860 -146
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-runtime.js +4 -1
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/module/testing.js +8 -1
- package/lib/module/testing.js.map +1 -1
- package/lib/typescript/capabilities.d.ts +2 -1
- package/lib/typescript/capabilities.d.ts.map +1 -1
- package/lib/typescript/core/durability.d.ts +7 -2
- package/lib/typescript/core/durability.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +2 -2
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +3 -29
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +0 -2
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +19 -0
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +12 -3
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +2 -1
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +1 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +1 -1
- package/package.json +3 -3
- package/react-native-nitro-storage.podspec +3 -1
- package/src/capabilities.ts +3 -1
- package/src/core/durability.ts +139 -39
- package/src/index.ts +20 -10
- package/src/index.web.ts +328 -166
- package/src/internal.ts +0 -14
- package/src/shared.ts +85 -0
- package/src/storage-core.ts +1165 -199
- package/src/storage-runtime.ts +6 -0
- 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
|
-
|
|
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
|
|
240
|
-
|
|
241
|
-
|
|
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
|
-
|
|
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
|
-
|
|
290
|
-
|
|
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(
|
|
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) =>
|
|
322
|
-
|
|
379
|
+
(backend) =>
|
|
380
|
+
backend.getAllKeys().includes(toSecureStorageKey(plainKey)),
|
|
381
|
+
)
|
|
323
382
|
) {
|
|
324
383
|
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
325
384
|
}
|
|
326
|
-
getInternals().cacheRawValue(
|
|
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(
|
|
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
|
-
|
|
460
|
-
|
|
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
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
backend.removeMany
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
backend
|
|
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
|
-
|
|
505
|
-
|
|
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
|
-
|
|
532
|
-
|
|
533
|
-
backend.setMany
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
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
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
backend.removeMany
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
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
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
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
|
-
|
|
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.
|
|
692
|
-
backend.
|
|
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
|
-
|
|
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
|
-
)
|
|
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
|
-
|
|
733
|
-
withWebBackendOperation(
|
|
883
|
+
try {
|
|
884
|
+
return withWebBackendOperation(
|
|
734
885
|
StorageScope.Secure,
|
|
735
886
|
"hasSecureBiometric",
|
|
736
|
-
(backend) => backend.
|
|
737
|
-
)
|
|
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
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
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
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
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);
|