react-native-nitro-storage 0.3.0 → 0.3.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.
- package/README.md +414 -256
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +98 -11
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +15 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +130 -33
- package/android/src/main/java/com/nitrostorage/NitroStoragePackage.kt +2 -2
- package/cpp/bindings/HybridStorage.cpp +121 -12
- package/cpp/bindings/HybridStorage.hpp +10 -0
- package/cpp/core/NativeStorageAdapter.hpp +15 -0
- package/ios/IOSStorageAdapterCpp.hpp +19 -0
- package/ios/IOSStorageAdapterCpp.mm +233 -32
- package/lib/commonjs/Storage.types.js +23 -1
- package/lib/commonjs/Storage.types.js.map +1 -1
- package/lib/commonjs/index.js +173 -32
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +289 -49
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +10 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/module/Storage.types.js +22 -0
- package/lib/module/Storage.types.js.map +1 -1
- package/lib/module/index.js +163 -35
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +278 -51
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +8 -0
- package/lib/module/internal.js.map +1 -1
- package/lib/typescript/Storage.nitro.d.ts +10 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/Storage.types.d.ts +20 -0
- package/lib/typescript/Storage.types.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +30 -7
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +40 -7
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +2 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +10 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +10 -0
- package/package.json +4 -1
- package/src/Storage.nitro.ts +11 -2
- package/src/Storage.types.ts +22 -0
- package/src/index.ts +270 -71
- package/src/index.web.ts +431 -90
- package/src/internal.ts +14 -4
- package/src/migration.ts +1 -1
package/src/Storage.types.ts
CHANGED
|
@@ -3,3 +3,25 @@ export enum StorageScope {
|
|
|
3
3
|
Disk = 1,
|
|
4
4
|
Secure = 2,
|
|
5
5
|
}
|
|
6
|
+
|
|
7
|
+
export enum AccessControl {
|
|
8
|
+
/** Accessible when unlocked (default). */
|
|
9
|
+
WhenUnlocked = 0,
|
|
10
|
+
/** Accessible after first unlock until restart. Good for background token refresh. */
|
|
11
|
+
AfterFirstUnlock = 1,
|
|
12
|
+
/** Accessible only when passcode is set, non-migratable. */
|
|
13
|
+
WhenPasscodeSetThisDeviceOnly = 2,
|
|
14
|
+
/** Same as WhenUnlocked but non-migratable between devices. */
|
|
15
|
+
WhenUnlockedThisDeviceOnly = 3,
|
|
16
|
+
/** Same as AfterFirstUnlock but non-migratable. */
|
|
17
|
+
AfterFirstUnlockThisDeviceOnly = 4,
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export enum BiometricLevel {
|
|
21
|
+
/** No biometric requirement (default). */
|
|
22
|
+
None = 0,
|
|
23
|
+
/** Require biometric or passcode for each access. */
|
|
24
|
+
BiometryOrPasscode = 1,
|
|
25
|
+
/** Require biometric only (no passcode fallback). */
|
|
26
|
+
BiometryOnly = 2,
|
|
27
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { useRef, useSyncExternalStore } from "react";
|
|
2
2
|
import { NitroModules } from "react-native-nitro-modules";
|
|
3
3
|
import type { Storage } from "./Storage.nitro";
|
|
4
|
-
import { StorageScope } from "./Storage.types";
|
|
4
|
+
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
5
5
|
import {
|
|
6
6
|
MIGRATION_VERSION_KEY,
|
|
7
7
|
type StoredEnvelope,
|
|
@@ -11,9 +11,11 @@ import {
|
|
|
11
11
|
decodeNativeBatchValue,
|
|
12
12
|
serializeWithPrimitiveFastPath,
|
|
13
13
|
deserializeWithPrimitiveFastPath,
|
|
14
|
+
prefixKey,
|
|
15
|
+
isNamespaced,
|
|
14
16
|
} from "./internal";
|
|
15
17
|
|
|
16
|
-
export { StorageScope } from "./Storage.types";
|
|
18
|
+
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
17
19
|
export type { Storage } from "./Storage.nitro";
|
|
18
20
|
export { migrateFromMMKV } from "./migration";
|
|
19
21
|
|
|
@@ -37,15 +39,26 @@ export type TransactionContext = {
|
|
|
37
39
|
setRaw: (key: string, value: string) => void;
|
|
38
40
|
removeRaw: (key: string) => void;
|
|
39
41
|
getItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "get">) => T;
|
|
40
|
-
setItem: <T>(
|
|
41
|
-
|
|
42
|
+
setItem: <T>(
|
|
43
|
+
item: Pick<StorageItem<T>, "scope" | "key" | "set">,
|
|
44
|
+
value: T,
|
|
45
|
+
) => void;
|
|
46
|
+
removeItem: (
|
|
47
|
+
item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">,
|
|
48
|
+
) => void;
|
|
42
49
|
};
|
|
43
50
|
|
|
44
51
|
type KeyListenerRegistry = Map<string, Set<() => void>>;
|
|
45
52
|
type RawBatchPathItem = {
|
|
46
53
|
_hasValidation?: boolean;
|
|
47
54
|
_hasExpiration?: boolean;
|
|
55
|
+
_isBiometric?: boolean;
|
|
56
|
+
_secureAccessControl?: AccessControl;
|
|
48
57
|
};
|
|
58
|
+
|
|
59
|
+
function asInternal(item: StorageItem<any>): StorageItemInternal<any> {
|
|
60
|
+
return item as unknown as StorageItemInternal<any>;
|
|
61
|
+
}
|
|
49
62
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
50
63
|
type PendingSecureWrite = { key: string; value: string | undefined };
|
|
51
64
|
|
|
@@ -73,28 +86,37 @@ const scopedListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
|
|
|
73
86
|
[StorageScope.Secure, new Map()],
|
|
74
87
|
]);
|
|
75
88
|
const scopedUnsubscribers = new Map<NonMemoryScope, () => void>();
|
|
76
|
-
const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
77
|
-
[
|
|
78
|
-
|
|
79
|
-
]
|
|
89
|
+
const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
90
|
+
[
|
|
91
|
+
[StorageScope.Disk, new Map()],
|
|
92
|
+
[StorageScope.Secure, new Map()],
|
|
93
|
+
],
|
|
94
|
+
);
|
|
80
95
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
81
96
|
let secureFlushScheduled = false;
|
|
97
|
+
let secureDefaultAccessControl: AccessControl = AccessControl.WhenUnlocked;
|
|
82
98
|
|
|
83
99
|
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
84
100
|
return scopedListeners.get(scope)!;
|
|
85
101
|
}
|
|
86
102
|
|
|
87
|
-
function getScopeRawCache(
|
|
103
|
+
function getScopeRawCache(
|
|
104
|
+
scope: NonMemoryScope,
|
|
105
|
+
): Map<string, string | undefined> {
|
|
88
106
|
return scopedRawCache.get(scope)!;
|
|
89
107
|
}
|
|
90
108
|
|
|
91
|
-
function cacheRawValue(
|
|
109
|
+
function cacheRawValue(
|
|
110
|
+
scope: NonMemoryScope,
|
|
111
|
+
key: string,
|
|
112
|
+
value: string | undefined,
|
|
113
|
+
): void {
|
|
92
114
|
getScopeRawCache(scope).set(key, value);
|
|
93
115
|
}
|
|
94
116
|
|
|
95
117
|
function readCachedRawValue(
|
|
96
118
|
scope: NonMemoryScope,
|
|
97
|
-
key: string
|
|
119
|
+
key: string,
|
|
98
120
|
): string | undefined {
|
|
99
121
|
return getScopeRawCache(scope).get(key);
|
|
100
122
|
}
|
|
@@ -120,7 +142,7 @@ function notifyAllListeners(registry: KeyListenerRegistry): void {
|
|
|
120
142
|
function addKeyListener(
|
|
121
143
|
registry: KeyListenerRegistry,
|
|
122
144
|
key: string,
|
|
123
|
-
listener: () => void
|
|
145
|
+
listener: () => void,
|
|
124
146
|
): () => void {
|
|
125
147
|
let listeners = registry.get(key);
|
|
126
148
|
if (!listeners) {
|
|
@@ -177,6 +199,7 @@ function flushSecureWrites(): void {
|
|
|
177
199
|
});
|
|
178
200
|
|
|
179
201
|
const storageModule = getStorageModule();
|
|
202
|
+
storageModule.setSecureAccessControl(secureDefaultAccessControl);
|
|
180
203
|
if (keysToSet.length > 0) {
|
|
181
204
|
storageModule.setBatch(keysToSet, valuesToSet, StorageScope.Secure);
|
|
182
205
|
}
|
|
@@ -260,6 +283,7 @@ function setRawValue(key: string, value: string, scope: StorageScope): void {
|
|
|
260
283
|
if (scope === StorageScope.Secure) {
|
|
261
284
|
flushSecureWrites();
|
|
262
285
|
clearPendingSecureWrite(key);
|
|
286
|
+
getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
|
|
263
287
|
}
|
|
264
288
|
|
|
265
289
|
getStorageModule().set(key, value, scope);
|
|
@@ -312,12 +336,88 @@ export const storage = {
|
|
|
312
336
|
|
|
313
337
|
clearScopeRawCache(scope);
|
|
314
338
|
getStorageModule().clear(scope);
|
|
339
|
+
if (scope === StorageScope.Secure) {
|
|
340
|
+
getStorageModule().clearSecureBiometric();
|
|
341
|
+
}
|
|
315
342
|
},
|
|
316
343
|
clearAll: () => {
|
|
317
344
|
storage.clear(StorageScope.Memory);
|
|
318
345
|
storage.clear(StorageScope.Disk);
|
|
319
346
|
storage.clear(StorageScope.Secure);
|
|
320
347
|
},
|
|
348
|
+
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
349
|
+
assertValidScope(scope);
|
|
350
|
+
if (scope === StorageScope.Memory) {
|
|
351
|
+
for (const key of memoryStore.keys()) {
|
|
352
|
+
if (isNamespaced(key, namespace)) {
|
|
353
|
+
memoryStore.delete(key);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
notifyAllListeners(memoryListeners);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
if (scope === StorageScope.Secure) {
|
|
360
|
+
flushSecureWrites();
|
|
361
|
+
}
|
|
362
|
+
const keys = getStorageModule().getAllKeys(scope);
|
|
363
|
+
const namespacedKeys = keys.filter((k) => isNamespaced(k, namespace));
|
|
364
|
+
if (namespacedKeys.length > 0) {
|
|
365
|
+
getStorageModule().removeBatch(namespacedKeys, scope);
|
|
366
|
+
namespacedKeys.forEach((k) => cacheRawValue(scope, k, undefined));
|
|
367
|
+
if (scope === StorageScope.Secure) {
|
|
368
|
+
namespacedKeys.forEach((k) => clearPendingSecureWrite(k));
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
},
|
|
372
|
+
clearBiometric: () => {
|
|
373
|
+
getStorageModule().clearSecureBiometric();
|
|
374
|
+
},
|
|
375
|
+
has: (key: string, scope: StorageScope): boolean => {
|
|
376
|
+
assertValidScope(scope);
|
|
377
|
+
if (scope === StorageScope.Memory) {
|
|
378
|
+
return memoryStore.has(key);
|
|
379
|
+
}
|
|
380
|
+
return getStorageModule().has(key, scope);
|
|
381
|
+
},
|
|
382
|
+
getAllKeys: (scope: StorageScope): string[] => {
|
|
383
|
+
assertValidScope(scope);
|
|
384
|
+
if (scope === StorageScope.Memory) {
|
|
385
|
+
return Array.from(memoryStore.keys());
|
|
386
|
+
}
|
|
387
|
+
return getStorageModule().getAllKeys(scope);
|
|
388
|
+
},
|
|
389
|
+
getAll: (scope: StorageScope): Record<string, string> => {
|
|
390
|
+
assertValidScope(scope);
|
|
391
|
+
const result: Record<string, string> = {};
|
|
392
|
+
if (scope === StorageScope.Memory) {
|
|
393
|
+
memoryStore.forEach((value, key) => {
|
|
394
|
+
if (typeof value === "string") result[key] = value;
|
|
395
|
+
});
|
|
396
|
+
return result;
|
|
397
|
+
}
|
|
398
|
+
const keys = getStorageModule().getAllKeys(scope);
|
|
399
|
+
if (keys.length === 0) return result;
|
|
400
|
+
const values = getStorageModule().getBatch(keys, scope);
|
|
401
|
+
keys.forEach((key, idx) => {
|
|
402
|
+
const val = decodeNativeBatchValue(values[idx]);
|
|
403
|
+
if (val !== undefined) result[key] = val;
|
|
404
|
+
});
|
|
405
|
+
return result;
|
|
406
|
+
},
|
|
407
|
+
size: (scope: StorageScope): number => {
|
|
408
|
+
assertValidScope(scope);
|
|
409
|
+
if (scope === StorageScope.Memory) {
|
|
410
|
+
return memoryStore.size;
|
|
411
|
+
}
|
|
412
|
+
return getStorageModule().size(scope);
|
|
413
|
+
},
|
|
414
|
+
setAccessControl: (level: AccessControl) => {
|
|
415
|
+
secureDefaultAccessControl = level;
|
|
416
|
+
getStorageModule().setSecureAccessControl(level);
|
|
417
|
+
},
|
|
418
|
+
setKeychainAccessGroup: (group: string) => {
|
|
419
|
+
getStorageModule().setKeychainAccessGroup(group);
|
|
420
|
+
},
|
|
321
421
|
};
|
|
322
422
|
|
|
323
423
|
export interface StorageItemConfig<T> {
|
|
@@ -329,27 +429,42 @@ export interface StorageItemConfig<T> {
|
|
|
329
429
|
validate?: Validator<T>;
|
|
330
430
|
onValidationError?: (invalidValue: unknown) => T;
|
|
331
431
|
expiration?: ExpirationConfig;
|
|
432
|
+
onExpired?: (key: string) => void;
|
|
332
433
|
readCache?: boolean;
|
|
333
434
|
coalesceSecureWrites?: boolean;
|
|
435
|
+
namespace?: string;
|
|
436
|
+
biometric?: boolean;
|
|
437
|
+
accessControl?: AccessControl;
|
|
334
438
|
}
|
|
335
439
|
|
|
336
440
|
export interface StorageItem<T> {
|
|
337
441
|
get: () => T;
|
|
338
442
|
set: (value: T | ((prev: T) => T)) => void;
|
|
339
443
|
delete: () => void;
|
|
444
|
+
has: () => boolean;
|
|
340
445
|
subscribe: (callback: () => void) => () => void;
|
|
341
446
|
serialize: (value: T) => string;
|
|
342
447
|
deserialize: (value: string) => T;
|
|
343
|
-
_triggerListeners: () => void;
|
|
344
|
-
_hasValidation?: boolean;
|
|
345
|
-
_hasExpiration?: boolean;
|
|
346
|
-
_readCacheEnabled?: boolean;
|
|
347
448
|
scope: StorageScope;
|
|
348
449
|
key: string;
|
|
349
450
|
}
|
|
350
451
|
|
|
452
|
+
type StorageItemInternal<T> = StorageItem<T> & {
|
|
453
|
+
_triggerListeners: () => void;
|
|
454
|
+
_hasValidation: boolean;
|
|
455
|
+
_hasExpiration: boolean;
|
|
456
|
+
_readCacheEnabled: boolean;
|
|
457
|
+
_isBiometric: boolean;
|
|
458
|
+
_secureAccessControl?: AccessControl;
|
|
459
|
+
};
|
|
460
|
+
|
|
351
461
|
function canUseRawBatchPath(item: RawBatchPathItem): boolean {
|
|
352
|
-
return
|
|
462
|
+
return (
|
|
463
|
+
item._hasExpiration === false &&
|
|
464
|
+
item._hasValidation === false &&
|
|
465
|
+
item._isBiometric !== true &&
|
|
466
|
+
item._secureAccessControl === undefined
|
|
467
|
+
);
|
|
353
468
|
}
|
|
354
469
|
|
|
355
470
|
function defaultSerialize<T>(value: T): string {
|
|
@@ -361,19 +476,28 @@ function defaultDeserialize<T>(value: string): T {
|
|
|
361
476
|
}
|
|
362
477
|
|
|
363
478
|
export function createStorageItem<T = undefined>(
|
|
364
|
-
config: StorageItemConfig<T
|
|
479
|
+
config: StorageItemConfig<T>,
|
|
365
480
|
): StorageItem<T> {
|
|
481
|
+
const storageKey = prefixKey(config.namespace, config.key);
|
|
366
482
|
const serialize = config.serialize ?? defaultSerialize;
|
|
367
483
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
368
484
|
const isMemory = config.scope === StorageScope.Memory;
|
|
485
|
+
const isBiometric =
|
|
486
|
+
config.biometric === true && config.scope === StorageScope.Secure;
|
|
487
|
+
const secureAccessControl = config.accessControl;
|
|
369
488
|
const validate = config.validate;
|
|
370
489
|
const onValidationError = config.onValidationError;
|
|
371
490
|
const expiration = config.expiration;
|
|
491
|
+
const onExpired = config.onExpired;
|
|
372
492
|
const expirationTtlMs = expiration?.ttlMs;
|
|
373
|
-
const memoryExpiration =
|
|
493
|
+
const memoryExpiration =
|
|
494
|
+
expiration && isMemory ? new Map<string, number>() : null;
|
|
374
495
|
const readCache = !isMemory && config.readCache === true;
|
|
375
496
|
const coalesceSecureWrites =
|
|
376
|
-
config.scope === StorageScope.Secure &&
|
|
497
|
+
config.scope === StorageScope.Secure &&
|
|
498
|
+
config.coalesceSecureWrites === true &&
|
|
499
|
+
!isBiometric &&
|
|
500
|
+
secureAccessControl === undefined;
|
|
377
501
|
const nonMemoryScope: NonMemoryScope | null =
|
|
378
502
|
config.scope === StorageScope.Disk
|
|
379
503
|
? StorageScope.Disk
|
|
@@ -408,80 +532,106 @@ export function createStorageItem<T = undefined>(
|
|
|
408
532
|
};
|
|
409
533
|
|
|
410
534
|
if (isMemory) {
|
|
411
|
-
unsubscribe = addKeyListener(memoryListeners,
|
|
535
|
+
unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
|
|
412
536
|
return;
|
|
413
537
|
}
|
|
414
538
|
|
|
415
539
|
ensureNativeScopeSubscription(nonMemoryScope!);
|
|
416
|
-
unsubscribe = addKeyListener(
|
|
540
|
+
unsubscribe = addKeyListener(
|
|
541
|
+
getScopedListeners(nonMemoryScope!),
|
|
542
|
+
storageKey,
|
|
543
|
+
listener,
|
|
544
|
+
);
|
|
417
545
|
};
|
|
418
546
|
|
|
419
547
|
const readStoredRaw = (): unknown => {
|
|
420
548
|
if (isMemory) {
|
|
421
549
|
if (memoryExpiration) {
|
|
422
|
-
const expiresAt = memoryExpiration.get(
|
|
550
|
+
const expiresAt = memoryExpiration.get(storageKey);
|
|
423
551
|
if (expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
424
|
-
memoryExpiration.delete(
|
|
425
|
-
memoryStore.delete(
|
|
426
|
-
notifyKeyListeners(memoryListeners,
|
|
552
|
+
memoryExpiration.delete(storageKey);
|
|
553
|
+
memoryStore.delete(storageKey);
|
|
554
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
555
|
+
onExpired?.(storageKey);
|
|
427
556
|
return undefined;
|
|
428
557
|
}
|
|
429
558
|
}
|
|
430
|
-
return memoryStore.get(
|
|
559
|
+
return memoryStore.get(storageKey) as T | undefined;
|
|
431
560
|
}
|
|
432
561
|
|
|
433
|
-
if (
|
|
434
|
-
|
|
562
|
+
if (
|
|
563
|
+
nonMemoryScope === StorageScope.Secure &&
|
|
564
|
+
!isBiometric &&
|
|
565
|
+
hasPendingSecureWrite(storageKey)
|
|
566
|
+
) {
|
|
567
|
+
return readPendingSecureWrite(storageKey);
|
|
435
568
|
}
|
|
436
569
|
|
|
437
570
|
if (readCache) {
|
|
438
|
-
if (hasCachedRawValue(nonMemoryScope!,
|
|
439
|
-
return readCachedRawValue(nonMemoryScope!,
|
|
571
|
+
if (hasCachedRawValue(nonMemoryScope!, storageKey)) {
|
|
572
|
+
return readCachedRawValue(nonMemoryScope!, storageKey);
|
|
440
573
|
}
|
|
441
574
|
}
|
|
442
575
|
|
|
443
|
-
|
|
444
|
-
|
|
576
|
+
if (isBiometric) {
|
|
577
|
+
return getStorageModule().getSecureBiometric(storageKey);
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
const raw = getStorageModule().get(storageKey, config.scope);
|
|
581
|
+
cacheRawValue(nonMemoryScope!, storageKey, raw);
|
|
445
582
|
return raw;
|
|
446
583
|
};
|
|
447
584
|
|
|
448
585
|
const writeStoredRaw = (rawValue: string): void => {
|
|
449
|
-
|
|
586
|
+
if (isBiometric) {
|
|
587
|
+
getStorageModule().setSecureBiometric(storageKey, rawValue);
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
|
|
591
|
+
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
450
592
|
|
|
451
593
|
if (coalesceSecureWrites) {
|
|
452
|
-
scheduleSecureWrite(
|
|
594
|
+
scheduleSecureWrite(storageKey, rawValue);
|
|
453
595
|
return;
|
|
454
596
|
}
|
|
455
597
|
|
|
456
598
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
457
|
-
clearPendingSecureWrite(
|
|
599
|
+
clearPendingSecureWrite(storageKey);
|
|
600
|
+
getStorageModule().setSecureAccessControl(
|
|
601
|
+
secureAccessControl ?? secureDefaultAccessControl,
|
|
602
|
+
);
|
|
458
603
|
}
|
|
459
604
|
|
|
460
|
-
getStorageModule().set(
|
|
605
|
+
getStorageModule().set(storageKey, rawValue, config.scope);
|
|
461
606
|
};
|
|
462
607
|
|
|
463
608
|
const removeStoredRaw = (): void => {
|
|
464
|
-
|
|
609
|
+
if (isBiometric) {
|
|
610
|
+
getStorageModule().deleteSecureBiometric(storageKey);
|
|
611
|
+
return;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
465
615
|
|
|
466
616
|
if (coalesceSecureWrites) {
|
|
467
|
-
scheduleSecureWrite(
|
|
617
|
+
scheduleSecureWrite(storageKey, undefined);
|
|
468
618
|
return;
|
|
469
619
|
}
|
|
470
620
|
|
|
471
621
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
472
|
-
clearPendingSecureWrite(
|
|
622
|
+
clearPendingSecureWrite(storageKey);
|
|
473
623
|
}
|
|
474
624
|
|
|
475
|
-
getStorageModule().remove(
|
|
625
|
+
getStorageModule().remove(storageKey, config.scope);
|
|
476
626
|
};
|
|
477
627
|
|
|
478
628
|
const writeValueWithoutValidation = (value: T): void => {
|
|
479
629
|
if (isMemory) {
|
|
480
630
|
if (memoryExpiration) {
|
|
481
|
-
memoryExpiration.set(
|
|
631
|
+
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
482
632
|
}
|
|
483
|
-
memoryStore.set(
|
|
484
|
-
notifyKeyListeners(memoryListeners,
|
|
633
|
+
memoryStore.set(storageKey, value);
|
|
634
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
485
635
|
return;
|
|
486
636
|
}
|
|
487
637
|
|
|
@@ -509,7 +659,7 @@ export function createStorageItem<T = undefined>(
|
|
|
509
659
|
|
|
510
660
|
const ensureValidatedValue = (
|
|
511
661
|
candidate: unknown,
|
|
512
|
-
hadStoredValue: boolean
|
|
662
|
+
hadStoredValue: boolean,
|
|
513
663
|
): T => {
|
|
514
664
|
if (!validate || validate(candidate)) {
|
|
515
665
|
return candidate as T;
|
|
@@ -556,6 +706,7 @@ export function createStorageItem<T = undefined>(
|
|
|
556
706
|
if (parsed.expiresAt <= Date.now()) {
|
|
557
707
|
removeStoredRaw();
|
|
558
708
|
invalidateParsedCache();
|
|
709
|
+
onExpired?.(storageKey);
|
|
559
710
|
lastValue = ensureValidatedValue(config.defaultValue, false);
|
|
560
711
|
hasLastValue = true;
|
|
561
712
|
return lastValue;
|
|
@@ -584,7 +735,7 @@ export function createStorageItem<T = undefined>(
|
|
|
584
735
|
|
|
585
736
|
if (validate && !validate(newValue)) {
|
|
586
737
|
throw new Error(
|
|
587
|
-
`Validation failed for key "${
|
|
738
|
+
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
588
739
|
);
|
|
589
740
|
}
|
|
590
741
|
|
|
@@ -596,16 +747,22 @@ export function createStorageItem<T = undefined>(
|
|
|
596
747
|
|
|
597
748
|
if (isMemory) {
|
|
598
749
|
if (memoryExpiration) {
|
|
599
|
-
memoryExpiration.delete(
|
|
750
|
+
memoryExpiration.delete(storageKey);
|
|
600
751
|
}
|
|
601
|
-
memoryStore.delete(
|
|
602
|
-
notifyKeyListeners(memoryListeners,
|
|
752
|
+
memoryStore.delete(storageKey);
|
|
753
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
603
754
|
return;
|
|
604
755
|
}
|
|
605
756
|
|
|
606
757
|
removeStoredRaw();
|
|
607
758
|
};
|
|
608
759
|
|
|
760
|
+
const hasItem = (): boolean => {
|
|
761
|
+
if (isMemory) return memoryStore.has(storageKey);
|
|
762
|
+
if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
|
|
763
|
+
return getStorageModule().has(storageKey, config.scope);
|
|
764
|
+
};
|
|
765
|
+
|
|
609
766
|
const subscribe = (callback: () => void): (() => void) => {
|
|
610
767
|
ensureSubscription();
|
|
611
768
|
listeners.add(callback);
|
|
@@ -621,10 +778,11 @@ export function createStorageItem<T = undefined>(
|
|
|
621
778
|
};
|
|
622
779
|
};
|
|
623
780
|
|
|
624
|
-
const storageItem:
|
|
781
|
+
const storageItem: StorageItemInternal<T> = {
|
|
625
782
|
get,
|
|
626
783
|
set,
|
|
627
784
|
delete: deleteItem,
|
|
785
|
+
has: hasItem,
|
|
628
786
|
subscribe,
|
|
629
787
|
serialize,
|
|
630
788
|
deserialize,
|
|
@@ -635,15 +793,17 @@ export function createStorageItem<T = undefined>(
|
|
|
635
793
|
_hasValidation: validate !== undefined,
|
|
636
794
|
_hasExpiration: expiration !== undefined,
|
|
637
795
|
_readCacheEnabled: readCache,
|
|
796
|
+
_isBiometric: isBiometric,
|
|
797
|
+
_secureAccessControl: secureAccessControl,
|
|
638
798
|
scope: config.scope,
|
|
639
|
-
key:
|
|
799
|
+
key: storageKey,
|
|
640
800
|
};
|
|
641
801
|
|
|
642
|
-
return storageItem
|
|
802
|
+
return storageItem as StorageItem<T>;
|
|
643
803
|
}
|
|
644
804
|
|
|
645
805
|
export function useStorage<T>(
|
|
646
|
-
item: StorageItem<T
|
|
806
|
+
item: StorageItem<T>,
|
|
647
807
|
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
648
808
|
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
649
809
|
return [value, item.set];
|
|
@@ -652,9 +812,11 @@ export function useStorage<T>(
|
|
|
652
812
|
export function useStorageSelector<T, TSelected>(
|
|
653
813
|
item: StorageItem<T>,
|
|
654
814
|
selector: (value: T) => TSelected,
|
|
655
|
-
isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is
|
|
815
|
+
isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is,
|
|
656
816
|
): [TSelected, (value: T | ((prev: T) => T)) => void] {
|
|
657
|
-
const selectedRef = useRef<
|
|
817
|
+
const selectedRef = useRef<
|
|
818
|
+
{ hasValue: false } | { hasValue: true; value: TSelected }
|
|
819
|
+
>({
|
|
658
820
|
hasValue: false,
|
|
659
821
|
});
|
|
660
822
|
|
|
@@ -672,7 +834,7 @@ export function useStorageSelector<T, TSelected>(
|
|
|
672
834
|
const selectedValue = useSyncExternalStore(
|
|
673
835
|
item.subscribe,
|
|
674
836
|
getSelectedSnapshot,
|
|
675
|
-
getSelectedSnapshot
|
|
837
|
+
getSelectedSnapshot,
|
|
676
838
|
);
|
|
677
839
|
return [selectedValue, item.set];
|
|
678
840
|
}
|
|
@@ -683,14 +845,14 @@ export function useSetStorage<T>(item: StorageItem<T>) {
|
|
|
683
845
|
|
|
684
846
|
type BatchReadItem<T> = Pick<
|
|
685
847
|
StorageItem<T>,
|
|
686
|
-
| "
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
848
|
+
"key" | "scope" | "get" | "deserialize"
|
|
849
|
+
> & {
|
|
850
|
+
_hasValidation?: boolean;
|
|
851
|
+
_hasExpiration?: boolean;
|
|
852
|
+
_readCacheEnabled?: boolean;
|
|
853
|
+
_isBiometric?: boolean;
|
|
854
|
+
_secureAccessControl?: AccessControl;
|
|
855
|
+
};
|
|
694
856
|
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
695
857
|
|
|
696
858
|
export type StorageBatchSetItem<T> = {
|
|
@@ -700,7 +862,7 @@ export type StorageBatchSetItem<T> = {
|
|
|
700
862
|
|
|
701
863
|
export function getBatch(
|
|
702
864
|
items: readonly BatchReadItem<unknown>[],
|
|
703
|
-
scope: StorageScope
|
|
865
|
+
scope: StorageScope,
|
|
704
866
|
): unknown[] {
|
|
705
867
|
assertBatchScope(items, scope);
|
|
706
868
|
|
|
@@ -761,11 +923,11 @@ export function getBatch(
|
|
|
761
923
|
|
|
762
924
|
export function setBatch<T>(
|
|
763
925
|
items: readonly StorageBatchSetItem<T>[],
|
|
764
|
-
scope: StorageScope
|
|
926
|
+
scope: StorageScope,
|
|
765
927
|
): void {
|
|
766
928
|
assertBatchScope(
|
|
767
929
|
items.map((batchEntry) => batchEntry.item),
|
|
768
|
-
scope
|
|
930
|
+
scope,
|
|
769
931
|
);
|
|
770
932
|
|
|
771
933
|
if (scope === StorageScope.Memory) {
|
|
@@ -773,7 +935,9 @@ export function setBatch<T>(
|
|
|
773
935
|
return;
|
|
774
936
|
}
|
|
775
937
|
|
|
776
|
-
const useRawBatchPath = items.every(({ item }) =>
|
|
938
|
+
const useRawBatchPath = items.every(({ item }) =>
|
|
939
|
+
canUseRawBatchPath(asInternal(item)),
|
|
940
|
+
);
|
|
777
941
|
if (!useRawBatchPath) {
|
|
778
942
|
items.forEach(({ item, value }) => item.set(value));
|
|
779
943
|
return;
|
|
@@ -784,6 +948,7 @@ export function setBatch<T>(
|
|
|
784
948
|
|
|
785
949
|
if (scope === StorageScope.Secure) {
|
|
786
950
|
flushSecureWrites();
|
|
951
|
+
getStorageModule().setSecureAccessControl(secureDefaultAccessControl);
|
|
787
952
|
}
|
|
788
953
|
getStorageModule().setBatch(keys, values, scope);
|
|
789
954
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
@@ -791,7 +956,7 @@ export function setBatch<T>(
|
|
|
791
956
|
|
|
792
957
|
export function removeBatch(
|
|
793
958
|
items: readonly BatchRemoveItem[],
|
|
794
|
-
scope: StorageScope
|
|
959
|
+
scope: StorageScope,
|
|
795
960
|
): void {
|
|
796
961
|
assertBatchScope(items, scope);
|
|
797
962
|
|
|
@@ -820,7 +985,9 @@ export function registerMigration(version: number, migration: Migration): void {
|
|
|
820
985
|
registeredMigrations.set(version, migration);
|
|
821
986
|
}
|
|
822
987
|
|
|
823
|
-
export function migrateToLatest(
|
|
988
|
+
export function migrateToLatest(
|
|
989
|
+
scope: StorageScope = StorageScope.Disk,
|
|
990
|
+
): number {
|
|
824
991
|
assertValidScope(scope);
|
|
825
992
|
const currentVersion = readMigrationVersion(scope);
|
|
826
993
|
const versions = Array.from(registeredMigrations.keys())
|
|
@@ -850,7 +1017,7 @@ export function migrateToLatest(scope: StorageScope = StorageScope.Disk): number
|
|
|
850
1017
|
|
|
851
1018
|
export function runTransaction<T>(
|
|
852
1019
|
scope: StorageScope,
|
|
853
|
-
transaction: (context: TransactionContext) => T
|
|
1020
|
+
transaction: (context: TransactionContext) => T,
|
|
854
1021
|
): T {
|
|
855
1022
|
assertValidScope(scope);
|
|
856
1023
|
if (scope === StorageScope.Secure) {
|
|
@@ -908,3 +1075,35 @@ export function runTransaction<T>(
|
|
|
908
1075
|
throw error;
|
|
909
1076
|
}
|
|
910
1077
|
}
|
|
1078
|
+
|
|
1079
|
+
export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
1080
|
+
K,
|
|
1081
|
+
{
|
|
1082
|
+
ttlMs?: number;
|
|
1083
|
+
biometric?: boolean;
|
|
1084
|
+
accessControl?: AccessControl;
|
|
1085
|
+
}
|
|
1086
|
+
>;
|
|
1087
|
+
|
|
1088
|
+
export function createSecureAuthStorage<K extends string>(
|
|
1089
|
+
config: SecureAuthStorageConfig<K>,
|
|
1090
|
+
options?: { namespace?: string },
|
|
1091
|
+
): Record<K, StorageItem<string>> {
|
|
1092
|
+
const ns = options?.namespace ?? "auth";
|
|
1093
|
+
const result = {} as Record<K, StorageItem<string>>;
|
|
1094
|
+
|
|
1095
|
+
for (const key of Object.keys(config) as K[]) {
|
|
1096
|
+
const itemConfig = config[key];
|
|
1097
|
+
result[key] = createStorageItem<string>({
|
|
1098
|
+
key,
|
|
1099
|
+
scope: StorageScope.Secure,
|
|
1100
|
+
defaultValue: "",
|
|
1101
|
+
namespace: ns,
|
|
1102
|
+
biometric: itemConfig.biometric,
|
|
1103
|
+
accessControl: itemConfig.accessControl,
|
|
1104
|
+
expiration: itemConfig.ttlMs ? { ttlMs: itemConfig.ttlMs } : undefined,
|
|
1105
|
+
});
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
return result;
|
|
1109
|
+
}
|