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/index.web.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useRef, useSyncExternalStore } from "react";
|
|
2
|
-
import { StorageScope } from "./Storage.types";
|
|
2
|
+
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
3
3
|
import {
|
|
4
4
|
MIGRATION_VERSION_KEY,
|
|
5
5
|
type StoredEnvelope,
|
|
@@ -8,9 +8,11 @@ import {
|
|
|
8
8
|
assertValidScope,
|
|
9
9
|
serializeWithPrimitiveFastPath,
|
|
10
10
|
deserializeWithPrimitiveFastPath,
|
|
11
|
+
prefixKey,
|
|
12
|
+
isNamespaced,
|
|
11
13
|
} from "./internal";
|
|
12
14
|
|
|
13
|
-
export { StorageScope } from "./Storage.types";
|
|
15
|
+
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
14
16
|
export { migrateFromMMKV } from "./migration";
|
|
15
17
|
|
|
16
18
|
export type Validator<T> = (value: unknown) => value is T;
|
|
@@ -33,15 +35,26 @@ export type TransactionContext = {
|
|
|
33
35
|
setRaw: (key: string, value: string) => void;
|
|
34
36
|
removeRaw: (key: string) => void;
|
|
35
37
|
getItem: <T>(item: Pick<StorageItem<T>, "scope" | "key" | "get">) => T;
|
|
36
|
-
setItem: <T>(
|
|
37
|
-
|
|
38
|
+
setItem: <T>(
|
|
39
|
+
item: Pick<StorageItem<T>, "scope" | "key" | "set">,
|
|
40
|
+
value: T,
|
|
41
|
+
) => void;
|
|
42
|
+
removeItem: (
|
|
43
|
+
item: Pick<StorageItem<unknown>, "scope" | "key" | "delete">,
|
|
44
|
+
) => void;
|
|
38
45
|
};
|
|
39
46
|
|
|
40
47
|
type KeyListenerRegistry = Map<string, Set<() => void>>;
|
|
41
48
|
type RawBatchPathItem = {
|
|
42
49
|
_hasValidation?: boolean;
|
|
43
50
|
_hasExpiration?: boolean;
|
|
51
|
+
_isBiometric?: boolean;
|
|
52
|
+
_secureAccessControl?: AccessControl;
|
|
44
53
|
};
|
|
54
|
+
|
|
55
|
+
function asInternal(item: StorageItem<any>): StorageItemInternal<any> {
|
|
56
|
+
return item as unknown as StorageItemInternal<any>;
|
|
57
|
+
}
|
|
45
58
|
type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
|
|
46
59
|
type PendingSecureWrite = { key: string; value: string | undefined };
|
|
47
60
|
type BrowserStorageLike = {
|
|
@@ -49,6 +62,8 @@ type BrowserStorageLike = {
|
|
|
49
62
|
getItem: (key: string) => string | null;
|
|
50
63
|
removeItem: (key: string) => void;
|
|
51
64
|
clear: () => void;
|
|
65
|
+
key: (index: number) => string | null;
|
|
66
|
+
readonly length: number;
|
|
52
67
|
};
|
|
53
68
|
|
|
54
69
|
const registeredMigrations = new Map<number, Migration>();
|
|
@@ -67,13 +82,23 @@ export interface Storage {
|
|
|
67
82
|
get(key: string, scope: number): string | undefined;
|
|
68
83
|
remove(key: string, scope: number): void;
|
|
69
84
|
clear(scope: number): void;
|
|
85
|
+
has(key: string, scope: number): boolean;
|
|
86
|
+
getAllKeys(scope: number): string[];
|
|
87
|
+
size(scope: number): number;
|
|
70
88
|
setBatch(keys: string[], values: string[], scope: number): void;
|
|
71
89
|
getBatch(keys: string[], scope: number): (string | undefined)[];
|
|
72
90
|
removeBatch(keys: string[], scope: number): void;
|
|
73
91
|
addOnChange(
|
|
74
92
|
scope: number,
|
|
75
|
-
callback: (key: string, value: string | undefined) => void
|
|
93
|
+
callback: (key: string, value: string | undefined) => void,
|
|
76
94
|
): () => void;
|
|
95
|
+
setSecureAccessControl(level: number): void;
|
|
96
|
+
setKeychainAccessGroup(group: string): void;
|
|
97
|
+
setSecureBiometric(key: string, value: string): void;
|
|
98
|
+
getSecureBiometric(key: string): string | undefined;
|
|
99
|
+
deleteSecureBiometric(key: string): void;
|
|
100
|
+
hasSecureBiometric(key: string): boolean;
|
|
101
|
+
clearSecureBiometric(): void;
|
|
77
102
|
}
|
|
78
103
|
|
|
79
104
|
const memoryStore = new Map<string, unknown>();
|
|
@@ -82,38 +107,65 @@ const webScopeListeners = new Map<NonMemoryScope, KeyListenerRegistry>([
|
|
|
82
107
|
[StorageScope.Disk, new Map()],
|
|
83
108
|
[StorageScope.Secure, new Map()],
|
|
84
109
|
]);
|
|
85
|
-
const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
86
|
-
[
|
|
87
|
-
|
|
88
|
-
]
|
|
110
|
+
const scopedRawCache = new Map<NonMemoryScope, Map<string, string | undefined>>(
|
|
111
|
+
[
|
|
112
|
+
[StorageScope.Disk, new Map()],
|
|
113
|
+
[StorageScope.Secure, new Map()],
|
|
114
|
+
],
|
|
115
|
+
);
|
|
89
116
|
const pendingSecureWrites = new Map<string, PendingSecureWrite>();
|
|
90
117
|
let secureFlushScheduled = false;
|
|
118
|
+
const SECURE_WEB_PREFIX = "__secure_";
|
|
119
|
+
const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
120
|
+
let hasWarnedAboutWebBiometricFallback = false;
|
|
91
121
|
|
|
92
122
|
function getBrowserStorage(scope: number): BrowserStorageLike | undefined {
|
|
93
123
|
if (scope === StorageScope.Disk) {
|
|
94
124
|
return globalThis.localStorage;
|
|
95
125
|
}
|
|
96
126
|
if (scope === StorageScope.Secure) {
|
|
97
|
-
return globalThis.
|
|
127
|
+
return globalThis.localStorage;
|
|
98
128
|
}
|
|
99
129
|
return undefined;
|
|
100
130
|
}
|
|
101
131
|
|
|
132
|
+
function toSecureStorageKey(key: string): string {
|
|
133
|
+
return `${SECURE_WEB_PREFIX}${key}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function fromSecureStorageKey(key: string): string {
|
|
137
|
+
return key.slice(SECURE_WEB_PREFIX.length);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function toBiometricStorageKey(key: string): string {
|
|
141
|
+
return `${BIOMETRIC_WEB_PREFIX}${key}`;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function fromBiometricStorageKey(key: string): string {
|
|
145
|
+
return key.slice(BIOMETRIC_WEB_PREFIX.length);
|
|
146
|
+
}
|
|
147
|
+
|
|
102
148
|
function getScopedListeners(scope: NonMemoryScope): KeyListenerRegistry {
|
|
103
149
|
return webScopeListeners.get(scope)!;
|
|
104
150
|
}
|
|
105
151
|
|
|
106
|
-
function getScopeRawCache(
|
|
152
|
+
function getScopeRawCache(
|
|
153
|
+
scope: NonMemoryScope,
|
|
154
|
+
): Map<string, string | undefined> {
|
|
107
155
|
return scopedRawCache.get(scope)!;
|
|
108
156
|
}
|
|
109
157
|
|
|
110
|
-
function cacheRawValue(
|
|
158
|
+
function cacheRawValue(
|
|
159
|
+
scope: NonMemoryScope,
|
|
160
|
+
key: string,
|
|
161
|
+
value: string | undefined,
|
|
162
|
+
): void {
|
|
111
163
|
getScopeRawCache(scope).set(key, value);
|
|
112
164
|
}
|
|
113
165
|
|
|
114
166
|
function readCachedRawValue(
|
|
115
167
|
scope: NonMemoryScope,
|
|
116
|
-
key: string
|
|
168
|
+
key: string,
|
|
117
169
|
): string | undefined {
|
|
118
170
|
return getScopeRawCache(scope).get(key);
|
|
119
171
|
}
|
|
@@ -139,7 +191,7 @@ function notifyAllListeners(registry: KeyListenerRegistry): void {
|
|
|
139
191
|
function addKeyListener(
|
|
140
192
|
registry: KeyListenerRegistry,
|
|
141
193
|
key: string,
|
|
142
|
-
listener: () => void
|
|
194
|
+
listener: () => void,
|
|
143
195
|
): () => void {
|
|
144
196
|
let listeners = registry.get(key);
|
|
145
197
|
if (!listeners) {
|
|
@@ -218,25 +270,70 @@ const WebStorage: Storage = {
|
|
|
218
270
|
dispose: () => {},
|
|
219
271
|
set: (key: string, value: string, scope: number) => {
|
|
220
272
|
const storage = getBrowserStorage(scope);
|
|
221
|
-
storage
|
|
273
|
+
if (!storage) {
|
|
274
|
+
return;
|
|
275
|
+
}
|
|
276
|
+
const storageKey =
|
|
277
|
+
scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
278
|
+
storage.setItem(storageKey, value);
|
|
222
279
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
223
280
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
224
281
|
}
|
|
225
282
|
},
|
|
226
283
|
get: (key: string, scope: number) => {
|
|
227
284
|
const storage = getBrowserStorage(scope);
|
|
228
|
-
|
|
285
|
+
const storageKey =
|
|
286
|
+
scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
287
|
+
return storage?.getItem(storageKey) ?? undefined;
|
|
229
288
|
},
|
|
230
289
|
remove: (key: string, scope: number) => {
|
|
231
290
|
const storage = getBrowserStorage(scope);
|
|
232
|
-
storage
|
|
291
|
+
if (!storage) {
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
if (scope === StorageScope.Secure) {
|
|
295
|
+
storage.removeItem(toSecureStorageKey(key));
|
|
296
|
+
storage.removeItem(toBiometricStorageKey(key));
|
|
297
|
+
} else {
|
|
298
|
+
storage.removeItem(key);
|
|
299
|
+
}
|
|
233
300
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
234
301
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
235
302
|
}
|
|
236
303
|
},
|
|
237
304
|
clear: (scope: number) => {
|
|
238
305
|
const storage = getBrowserStorage(scope);
|
|
239
|
-
storage
|
|
306
|
+
if (!storage) {
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
309
|
+
if (scope === StorageScope.Secure) {
|
|
310
|
+
const keysToRemove: string[] = [];
|
|
311
|
+
for (let i = 0; i < storage.length; i++) {
|
|
312
|
+
const key = storage.key(i);
|
|
313
|
+
if (
|
|
314
|
+
key?.startsWith(SECURE_WEB_PREFIX) ||
|
|
315
|
+
key?.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
316
|
+
) {
|
|
317
|
+
keysToRemove.push(key);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
keysToRemove.forEach((key) => storage.removeItem(key));
|
|
321
|
+
} else if (scope === StorageScope.Disk) {
|
|
322
|
+
const keysToRemove: string[] = [];
|
|
323
|
+
for (let i = 0; i < storage.length; i++) {
|
|
324
|
+
const key = storage.key(i);
|
|
325
|
+
if (
|
|
326
|
+
key &&
|
|
327
|
+
!key.startsWith(SECURE_WEB_PREFIX) &&
|
|
328
|
+
!key.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
329
|
+
) {
|
|
330
|
+
keysToRemove.push(key);
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
keysToRemove.forEach((key) => storage.removeItem(key));
|
|
334
|
+
} else {
|
|
335
|
+
storage.clear();
|
|
336
|
+
}
|
|
240
337
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
241
338
|
notifyAllListeners(getScopedListeners(scope));
|
|
242
339
|
}
|
|
@@ -248,7 +345,9 @@ const WebStorage: Storage = {
|
|
|
248
345
|
}
|
|
249
346
|
|
|
250
347
|
keys.forEach((key, index) => {
|
|
251
|
-
|
|
348
|
+
const storageKey =
|
|
349
|
+
scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
350
|
+
storage.setItem(storageKey, values[index]);
|
|
252
351
|
});
|
|
253
352
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
254
353
|
const listeners = getScopedListeners(scope);
|
|
@@ -257,28 +356,109 @@ const WebStorage: Storage = {
|
|
|
257
356
|
},
|
|
258
357
|
getBatch: (keys: string[], scope: number) => {
|
|
259
358
|
const storage = getBrowserStorage(scope);
|
|
260
|
-
return keys.map((key) =>
|
|
359
|
+
return keys.map((key) => {
|
|
360
|
+
const storageKey =
|
|
361
|
+
scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
362
|
+
return storage?.getItem(storageKey) ?? undefined;
|
|
363
|
+
});
|
|
261
364
|
},
|
|
262
365
|
removeBatch: (keys: string[], scope: number) => {
|
|
263
|
-
const storage = getBrowserStorage(scope);
|
|
264
|
-
if (!storage) {
|
|
265
|
-
return;
|
|
266
|
-
}
|
|
267
|
-
|
|
268
366
|
keys.forEach((key) => {
|
|
269
|
-
|
|
367
|
+
WebStorage.remove(key, scope);
|
|
270
368
|
});
|
|
271
|
-
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
272
|
-
const listeners = getScopedListeners(scope);
|
|
273
|
-
keys.forEach((key) => notifyKeyListeners(listeners, key));
|
|
274
|
-
}
|
|
275
369
|
},
|
|
276
370
|
addOnChange: (
|
|
277
371
|
_scope: number,
|
|
278
|
-
_callback: (key: string, value: string | undefined) => void
|
|
372
|
+
_callback: (key: string, value: string | undefined) => void,
|
|
279
373
|
) => {
|
|
280
374
|
return () => {};
|
|
281
375
|
},
|
|
376
|
+
has: (key: string, scope: number) => {
|
|
377
|
+
const storage = getBrowserStorage(scope);
|
|
378
|
+
if (scope === StorageScope.Secure) {
|
|
379
|
+
return (
|
|
380
|
+
storage?.getItem(toSecureStorageKey(key)) !== null ||
|
|
381
|
+
storage?.getItem(toBiometricStorageKey(key)) !== null
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
return storage?.getItem(key) !== null;
|
|
385
|
+
},
|
|
386
|
+
getAllKeys: (scope: number) => {
|
|
387
|
+
const storage = getBrowserStorage(scope);
|
|
388
|
+
if (!storage) return [];
|
|
389
|
+
const keys = new Set<string>();
|
|
390
|
+
for (let i = 0; i < storage.length; i++) {
|
|
391
|
+
const k = storage.key(i);
|
|
392
|
+
if (!k) {
|
|
393
|
+
continue;
|
|
394
|
+
}
|
|
395
|
+
if (scope === StorageScope.Secure) {
|
|
396
|
+
if (k.startsWith(SECURE_WEB_PREFIX)) {
|
|
397
|
+
keys.add(fromSecureStorageKey(k));
|
|
398
|
+
} else if (k.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
399
|
+
keys.add(fromBiometricStorageKey(k));
|
|
400
|
+
}
|
|
401
|
+
continue;
|
|
402
|
+
}
|
|
403
|
+
if (
|
|
404
|
+
k.startsWith(SECURE_WEB_PREFIX) ||
|
|
405
|
+
k.startsWith(BIOMETRIC_WEB_PREFIX)
|
|
406
|
+
) {
|
|
407
|
+
continue;
|
|
408
|
+
}
|
|
409
|
+
keys.add(k);
|
|
410
|
+
}
|
|
411
|
+
return Array.from(keys);
|
|
412
|
+
},
|
|
413
|
+
size: (scope: number) => {
|
|
414
|
+
return WebStorage.getAllKeys(scope).length;
|
|
415
|
+
},
|
|
416
|
+
setSecureAccessControl: () => {},
|
|
417
|
+
setKeychainAccessGroup: () => {},
|
|
418
|
+
setSecureBiometric: (key: string, value: string) => {
|
|
419
|
+
if (
|
|
420
|
+
typeof __DEV__ !== "undefined" &&
|
|
421
|
+
__DEV__ &&
|
|
422
|
+
!hasWarnedAboutWebBiometricFallback
|
|
423
|
+
) {
|
|
424
|
+
hasWarnedAboutWebBiometricFallback = true;
|
|
425
|
+
console.warn(
|
|
426
|
+
"[NitroStorage] Biometric storage is not supported on web. Using localStorage.",
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
globalThis.localStorage?.setItem(toBiometricStorageKey(key), value);
|
|
430
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
431
|
+
},
|
|
432
|
+
getSecureBiometric: (key: string) => {
|
|
433
|
+
return (
|
|
434
|
+
globalThis.localStorage?.getItem(toBiometricStorageKey(key)) ?? undefined
|
|
435
|
+
);
|
|
436
|
+
},
|
|
437
|
+
deleteSecureBiometric: (key: string) => {
|
|
438
|
+
globalThis.localStorage?.removeItem(toBiometricStorageKey(key));
|
|
439
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
440
|
+
},
|
|
441
|
+
hasSecureBiometric: (key: string) => {
|
|
442
|
+
return (
|
|
443
|
+
globalThis.localStorage?.getItem(toBiometricStorageKey(key)) !== null
|
|
444
|
+
);
|
|
445
|
+
},
|
|
446
|
+
clearSecureBiometric: () => {
|
|
447
|
+
const storage = globalThis.localStorage;
|
|
448
|
+
if (!storage) return;
|
|
449
|
+
const keysToNotify: string[] = [];
|
|
450
|
+
const toRemove: string[] = [];
|
|
451
|
+
for (let i = 0; i < storage.length; i++) {
|
|
452
|
+
const k = storage.key(i);
|
|
453
|
+
if (k?.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
454
|
+
toRemove.push(k);
|
|
455
|
+
keysToNotify.push(fromBiometricStorageKey(k));
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
toRemove.forEach((k) => storage.removeItem(k));
|
|
459
|
+
const listeners = getScopedListeners(StorageScope.Secure);
|
|
460
|
+
keysToNotify.forEach((key) => notifyKeyListeners(listeners, key));
|
|
461
|
+
},
|
|
282
462
|
};
|
|
283
463
|
|
|
284
464
|
function getRawValue(key: string, scope: StorageScope): string | undefined {
|
|
@@ -358,12 +538,75 @@ export const storage = {
|
|
|
358
538
|
|
|
359
539
|
clearScopeRawCache(scope);
|
|
360
540
|
WebStorage.clear(scope);
|
|
541
|
+
if (scope === StorageScope.Secure) {
|
|
542
|
+
WebStorage.clearSecureBiometric();
|
|
543
|
+
}
|
|
361
544
|
},
|
|
362
545
|
clearAll: () => {
|
|
363
546
|
storage.clear(StorageScope.Memory);
|
|
364
547
|
storage.clear(StorageScope.Disk);
|
|
365
548
|
storage.clear(StorageScope.Secure);
|
|
366
549
|
},
|
|
550
|
+
clearNamespace: (namespace: string, scope: StorageScope) => {
|
|
551
|
+
assertValidScope(scope);
|
|
552
|
+
if (scope === StorageScope.Memory) {
|
|
553
|
+
for (const key of memoryStore.keys()) {
|
|
554
|
+
if (isNamespaced(key, namespace)) {
|
|
555
|
+
memoryStore.delete(key);
|
|
556
|
+
}
|
|
557
|
+
}
|
|
558
|
+
notifyAllListeners(memoryListeners);
|
|
559
|
+
return;
|
|
560
|
+
}
|
|
561
|
+
if (scope === StorageScope.Secure) {
|
|
562
|
+
flushSecureWrites();
|
|
563
|
+
}
|
|
564
|
+
const keys = WebStorage.getAllKeys(scope);
|
|
565
|
+
const namespacedKeys = keys.filter((k) => isNamespaced(k, namespace));
|
|
566
|
+
if (namespacedKeys.length > 0) {
|
|
567
|
+
WebStorage.removeBatch(namespacedKeys, scope);
|
|
568
|
+
namespacedKeys.forEach((k) => cacheRawValue(scope, k, undefined));
|
|
569
|
+
if (scope === StorageScope.Secure) {
|
|
570
|
+
namespacedKeys.forEach((k) => clearPendingSecureWrite(k));
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
},
|
|
574
|
+
clearBiometric: () => {
|
|
575
|
+
WebStorage.clearSecureBiometric();
|
|
576
|
+
},
|
|
577
|
+
has: (key: string, scope: StorageScope): boolean => {
|
|
578
|
+
assertValidScope(scope);
|
|
579
|
+
if (scope === StorageScope.Memory) return memoryStore.has(key);
|
|
580
|
+
return WebStorage.has(key, scope);
|
|
581
|
+
},
|
|
582
|
+
getAllKeys: (scope: StorageScope): string[] => {
|
|
583
|
+
assertValidScope(scope);
|
|
584
|
+
if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
|
|
585
|
+
return WebStorage.getAllKeys(scope);
|
|
586
|
+
},
|
|
587
|
+
getAll: (scope: StorageScope): Record<string, string> => {
|
|
588
|
+
assertValidScope(scope);
|
|
589
|
+
const result: Record<string, string> = {};
|
|
590
|
+
if (scope === StorageScope.Memory) {
|
|
591
|
+
memoryStore.forEach((value, key) => {
|
|
592
|
+
if (typeof value === "string") result[key] = value;
|
|
593
|
+
});
|
|
594
|
+
return result;
|
|
595
|
+
}
|
|
596
|
+
const keys = WebStorage.getAllKeys(scope);
|
|
597
|
+
keys.forEach((key) => {
|
|
598
|
+
const val = WebStorage.get(key, scope);
|
|
599
|
+
if (val !== undefined) result[key] = val;
|
|
600
|
+
});
|
|
601
|
+
return result;
|
|
602
|
+
},
|
|
603
|
+
size: (scope: StorageScope): number => {
|
|
604
|
+
assertValidScope(scope);
|
|
605
|
+
if (scope === StorageScope.Memory) return memoryStore.size;
|
|
606
|
+
return WebStorage.size(scope);
|
|
607
|
+
},
|
|
608
|
+
setAccessControl: (_level: AccessControl) => {},
|
|
609
|
+
setKeychainAccessGroup: (_group: string) => {},
|
|
367
610
|
};
|
|
368
611
|
|
|
369
612
|
export interface StorageItemConfig<T> {
|
|
@@ -375,27 +618,42 @@ export interface StorageItemConfig<T> {
|
|
|
375
618
|
validate?: Validator<T>;
|
|
376
619
|
onValidationError?: (invalidValue: unknown) => T;
|
|
377
620
|
expiration?: ExpirationConfig;
|
|
621
|
+
onExpired?: (key: string) => void;
|
|
378
622
|
readCache?: boolean;
|
|
379
623
|
coalesceSecureWrites?: boolean;
|
|
624
|
+
namespace?: string;
|
|
625
|
+
biometric?: boolean;
|
|
626
|
+
accessControl?: AccessControl;
|
|
380
627
|
}
|
|
381
628
|
|
|
382
629
|
export interface StorageItem<T> {
|
|
383
630
|
get: () => T;
|
|
384
631
|
set: (value: T | ((prev: T) => T)) => void;
|
|
385
632
|
delete: () => void;
|
|
633
|
+
has: () => boolean;
|
|
386
634
|
subscribe: (callback: () => void) => () => void;
|
|
387
635
|
serialize: (value: T) => string;
|
|
388
636
|
deserialize: (value: string) => T;
|
|
389
|
-
_triggerListeners: () => void;
|
|
390
|
-
_hasValidation?: boolean;
|
|
391
|
-
_hasExpiration?: boolean;
|
|
392
|
-
_readCacheEnabled?: boolean;
|
|
393
637
|
scope: StorageScope;
|
|
394
638
|
key: string;
|
|
395
639
|
}
|
|
396
640
|
|
|
641
|
+
type StorageItemInternal<T> = StorageItem<T> & {
|
|
642
|
+
_triggerListeners: () => void;
|
|
643
|
+
_hasValidation: boolean;
|
|
644
|
+
_hasExpiration: boolean;
|
|
645
|
+
_readCacheEnabled: boolean;
|
|
646
|
+
_isBiometric: boolean;
|
|
647
|
+
_secureAccessControl?: AccessControl;
|
|
648
|
+
};
|
|
649
|
+
|
|
397
650
|
function canUseRawBatchPath(item: RawBatchPathItem): boolean {
|
|
398
|
-
return
|
|
651
|
+
return (
|
|
652
|
+
item._hasExpiration === false &&
|
|
653
|
+
item._hasValidation === false &&
|
|
654
|
+
item._isBiometric !== true &&
|
|
655
|
+
item._secureAccessControl === undefined
|
|
656
|
+
);
|
|
399
657
|
}
|
|
400
658
|
|
|
401
659
|
function defaultSerialize<T>(value: T): string {
|
|
@@ -407,19 +665,28 @@ function defaultDeserialize<T>(value: string): T {
|
|
|
407
665
|
}
|
|
408
666
|
|
|
409
667
|
export function createStorageItem<T = undefined>(
|
|
410
|
-
config: StorageItemConfig<T
|
|
668
|
+
config: StorageItemConfig<T>,
|
|
411
669
|
): StorageItem<T> {
|
|
670
|
+
const storageKey = prefixKey(config.namespace, config.key);
|
|
412
671
|
const serialize = config.serialize ?? defaultSerialize;
|
|
413
672
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
414
673
|
const isMemory = config.scope === StorageScope.Memory;
|
|
674
|
+
const isBiometric =
|
|
675
|
+
config.biometric === true && config.scope === StorageScope.Secure;
|
|
676
|
+
const secureAccessControl = config.accessControl;
|
|
415
677
|
const validate = config.validate;
|
|
416
678
|
const onValidationError = config.onValidationError;
|
|
417
679
|
const expiration = config.expiration;
|
|
680
|
+
const onExpired = config.onExpired;
|
|
418
681
|
const expirationTtlMs = expiration?.ttlMs;
|
|
419
|
-
const memoryExpiration =
|
|
682
|
+
const memoryExpiration =
|
|
683
|
+
expiration && isMemory ? new Map<string, number>() : null;
|
|
420
684
|
const readCache = !isMemory && config.readCache === true;
|
|
421
685
|
const coalesceSecureWrites =
|
|
422
|
-
config.scope === StorageScope.Secure &&
|
|
686
|
+
config.scope === StorageScope.Secure &&
|
|
687
|
+
config.coalesceSecureWrites === true &&
|
|
688
|
+
!isBiometric &&
|
|
689
|
+
secureAccessControl === undefined;
|
|
423
690
|
const nonMemoryScope: NonMemoryScope | null =
|
|
424
691
|
config.scope === StorageScope.Disk
|
|
425
692
|
? StorageScope.Disk
|
|
@@ -454,79 +721,102 @@ export function createStorageItem<T = undefined>(
|
|
|
454
721
|
};
|
|
455
722
|
|
|
456
723
|
if (isMemory) {
|
|
457
|
-
unsubscribe = addKeyListener(memoryListeners,
|
|
724
|
+
unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
|
|
458
725
|
return;
|
|
459
726
|
}
|
|
460
727
|
|
|
461
|
-
unsubscribe = addKeyListener(
|
|
728
|
+
unsubscribe = addKeyListener(
|
|
729
|
+
getScopedListeners(nonMemoryScope!),
|
|
730
|
+
storageKey,
|
|
731
|
+
listener,
|
|
732
|
+
);
|
|
462
733
|
};
|
|
463
734
|
|
|
464
735
|
const readStoredRaw = (): unknown => {
|
|
465
736
|
if (isMemory) {
|
|
466
737
|
if (memoryExpiration) {
|
|
467
|
-
const expiresAt = memoryExpiration.get(
|
|
738
|
+
const expiresAt = memoryExpiration.get(storageKey);
|
|
468
739
|
if (expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
469
|
-
memoryExpiration.delete(
|
|
470
|
-
memoryStore.delete(
|
|
471
|
-
notifyKeyListeners(memoryListeners,
|
|
740
|
+
memoryExpiration.delete(storageKey);
|
|
741
|
+
memoryStore.delete(storageKey);
|
|
742
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
743
|
+
onExpired?.(storageKey);
|
|
472
744
|
return undefined;
|
|
473
745
|
}
|
|
474
746
|
}
|
|
475
|
-
return memoryStore.get(
|
|
747
|
+
return memoryStore.get(storageKey) as T | undefined;
|
|
476
748
|
}
|
|
477
749
|
|
|
478
|
-
if (
|
|
479
|
-
|
|
750
|
+
if (
|
|
751
|
+
nonMemoryScope === StorageScope.Secure &&
|
|
752
|
+
!isBiometric &&
|
|
753
|
+
hasPendingSecureWrite(storageKey)
|
|
754
|
+
) {
|
|
755
|
+
return readPendingSecureWrite(storageKey);
|
|
480
756
|
}
|
|
481
757
|
|
|
482
758
|
if (readCache) {
|
|
483
|
-
if (hasCachedRawValue(nonMemoryScope!,
|
|
484
|
-
return readCachedRawValue(nonMemoryScope!,
|
|
759
|
+
if (hasCachedRawValue(nonMemoryScope!, storageKey)) {
|
|
760
|
+
return readCachedRawValue(nonMemoryScope!, storageKey);
|
|
485
761
|
}
|
|
486
762
|
}
|
|
487
763
|
|
|
488
|
-
|
|
489
|
-
|
|
764
|
+
if (isBiometric) {
|
|
765
|
+
return WebStorage.getSecureBiometric(storageKey);
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
const raw = WebStorage.get(storageKey, config.scope);
|
|
769
|
+
cacheRawValue(nonMemoryScope!, storageKey, raw);
|
|
490
770
|
return raw;
|
|
491
771
|
};
|
|
492
772
|
|
|
493
773
|
const writeStoredRaw = (rawValue: string): void => {
|
|
494
|
-
|
|
774
|
+
if (isBiometric) {
|
|
775
|
+
WebStorage.setSecureBiometric(storageKey, rawValue);
|
|
776
|
+
return;
|
|
777
|
+
}
|
|
778
|
+
|
|
779
|
+
cacheRawValue(nonMemoryScope!, storageKey, rawValue);
|
|
495
780
|
|
|
496
781
|
if (coalesceSecureWrites) {
|
|
497
|
-
scheduleSecureWrite(
|
|
782
|
+
scheduleSecureWrite(storageKey, rawValue);
|
|
498
783
|
return;
|
|
499
784
|
}
|
|
500
785
|
|
|
501
786
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
502
|
-
clearPendingSecureWrite(
|
|
787
|
+
clearPendingSecureWrite(storageKey);
|
|
503
788
|
}
|
|
504
789
|
|
|
505
|
-
WebStorage.set(
|
|
790
|
+
WebStorage.set(storageKey, rawValue, config.scope);
|
|
506
791
|
};
|
|
507
792
|
|
|
508
793
|
const removeStoredRaw = (): void => {
|
|
509
|
-
|
|
794
|
+
if (isBiometric) {
|
|
795
|
+
WebStorage.deleteSecureBiometric(storageKey);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
cacheRawValue(nonMemoryScope!, storageKey, undefined);
|
|
510
800
|
|
|
511
801
|
if (coalesceSecureWrites) {
|
|
512
|
-
scheduleSecureWrite(
|
|
802
|
+
scheduleSecureWrite(storageKey, undefined);
|
|
513
803
|
return;
|
|
514
804
|
}
|
|
515
805
|
|
|
516
806
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
517
|
-
clearPendingSecureWrite(
|
|
807
|
+
clearPendingSecureWrite(storageKey);
|
|
518
808
|
}
|
|
519
809
|
|
|
520
|
-
WebStorage.remove(
|
|
810
|
+
WebStorage.remove(storageKey, config.scope);
|
|
521
811
|
};
|
|
522
812
|
|
|
523
813
|
const writeValueWithoutValidation = (value: T): void => {
|
|
524
814
|
if (isMemory) {
|
|
525
815
|
if (memoryExpiration) {
|
|
526
|
-
memoryExpiration.set(
|
|
816
|
+
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
527
817
|
}
|
|
528
|
-
memoryStore.set(
|
|
529
|
-
notifyKeyListeners(memoryListeners,
|
|
818
|
+
memoryStore.set(storageKey, value);
|
|
819
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
530
820
|
return;
|
|
531
821
|
}
|
|
532
822
|
|
|
@@ -552,7 +842,10 @@ export function createStorageItem<T = undefined>(
|
|
|
552
842
|
return config.defaultValue as T;
|
|
553
843
|
};
|
|
554
844
|
|
|
555
|
-
const ensureValidatedValue = (
|
|
845
|
+
const ensureValidatedValue = (
|
|
846
|
+
candidate: unknown,
|
|
847
|
+
hadStoredValue: boolean,
|
|
848
|
+
): T => {
|
|
556
849
|
if (!validate || validate(candidate)) {
|
|
557
850
|
return candidate as T;
|
|
558
851
|
}
|
|
@@ -598,6 +891,7 @@ export function createStorageItem<T = undefined>(
|
|
|
598
891
|
if (parsed.expiresAt <= Date.now()) {
|
|
599
892
|
removeStoredRaw();
|
|
600
893
|
invalidateParsedCache();
|
|
894
|
+
onExpired?.(storageKey);
|
|
601
895
|
lastValue = ensureValidatedValue(config.defaultValue, false);
|
|
602
896
|
hasLastValue = true;
|
|
603
897
|
return lastValue;
|
|
@@ -626,7 +920,7 @@ export function createStorageItem<T = undefined>(
|
|
|
626
920
|
|
|
627
921
|
if (validate && !validate(newValue)) {
|
|
628
922
|
throw new Error(
|
|
629
|
-
`Validation failed for key "${
|
|
923
|
+
`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`,
|
|
630
924
|
);
|
|
631
925
|
}
|
|
632
926
|
|
|
@@ -638,16 +932,22 @@ export function createStorageItem<T = undefined>(
|
|
|
638
932
|
|
|
639
933
|
if (isMemory) {
|
|
640
934
|
if (memoryExpiration) {
|
|
641
|
-
memoryExpiration.delete(
|
|
935
|
+
memoryExpiration.delete(storageKey);
|
|
642
936
|
}
|
|
643
|
-
memoryStore.delete(
|
|
644
|
-
notifyKeyListeners(memoryListeners,
|
|
937
|
+
memoryStore.delete(storageKey);
|
|
938
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
645
939
|
return;
|
|
646
940
|
}
|
|
647
941
|
|
|
648
942
|
removeStoredRaw();
|
|
649
943
|
};
|
|
650
944
|
|
|
945
|
+
const hasItem = (): boolean => {
|
|
946
|
+
if (isMemory) return memoryStore.has(storageKey);
|
|
947
|
+
if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
|
|
948
|
+
return WebStorage.has(storageKey, config.scope);
|
|
949
|
+
};
|
|
950
|
+
|
|
651
951
|
const subscribe = (callback: () => void): (() => void) => {
|
|
652
952
|
ensureSubscription();
|
|
653
953
|
listeners.add(callback);
|
|
@@ -660,10 +960,11 @@ export function createStorageItem<T = undefined>(
|
|
|
660
960
|
};
|
|
661
961
|
};
|
|
662
962
|
|
|
663
|
-
const storageItem:
|
|
963
|
+
const storageItem: StorageItemInternal<T> = {
|
|
664
964
|
get,
|
|
665
965
|
set,
|
|
666
966
|
delete: deleteItem,
|
|
967
|
+
has: hasItem,
|
|
667
968
|
subscribe,
|
|
668
969
|
serialize,
|
|
669
970
|
deserialize,
|
|
@@ -674,15 +975,17 @@ export function createStorageItem<T = undefined>(
|
|
|
674
975
|
_hasValidation: validate !== undefined,
|
|
675
976
|
_hasExpiration: expiration !== undefined,
|
|
676
977
|
_readCacheEnabled: readCache,
|
|
978
|
+
_isBiometric: isBiometric,
|
|
979
|
+
_secureAccessControl: secureAccessControl,
|
|
677
980
|
scope: config.scope,
|
|
678
|
-
key:
|
|
981
|
+
key: storageKey,
|
|
679
982
|
};
|
|
680
983
|
|
|
681
|
-
return storageItem
|
|
984
|
+
return storageItem as StorageItem<T>;
|
|
682
985
|
}
|
|
683
986
|
|
|
684
987
|
export function useStorage<T>(
|
|
685
|
-
item: StorageItem<T
|
|
988
|
+
item: StorageItem<T>,
|
|
686
989
|
): [T, (value: T | ((prev: T) => T)) => void] {
|
|
687
990
|
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
688
991
|
return [value, item.set];
|
|
@@ -691,9 +994,11 @@ export function useStorage<T>(
|
|
|
691
994
|
export function useStorageSelector<T, TSelected>(
|
|
692
995
|
item: StorageItem<T>,
|
|
693
996
|
selector: (value: T) => TSelected,
|
|
694
|
-
isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is
|
|
997
|
+
isEqual: (prev: TSelected, next: TSelected) => boolean = Object.is,
|
|
695
998
|
): [TSelected, (value: T | ((prev: T) => T)) => void] {
|
|
696
|
-
const selectedRef = useRef<
|
|
999
|
+
const selectedRef = useRef<
|
|
1000
|
+
{ hasValue: false } | { hasValue: true; value: TSelected }
|
|
1001
|
+
>({
|
|
697
1002
|
hasValue: false,
|
|
698
1003
|
});
|
|
699
1004
|
|
|
@@ -711,7 +1016,7 @@ export function useStorageSelector<T, TSelected>(
|
|
|
711
1016
|
const selectedValue = useSyncExternalStore(
|
|
712
1017
|
item.subscribe,
|
|
713
1018
|
getSelectedSnapshot,
|
|
714
|
-
getSelectedSnapshot
|
|
1019
|
+
getSelectedSnapshot,
|
|
715
1020
|
);
|
|
716
1021
|
return [selectedValue, item.set];
|
|
717
1022
|
}
|
|
@@ -722,14 +1027,14 @@ export function useSetStorage<T>(item: StorageItem<T>) {
|
|
|
722
1027
|
|
|
723
1028
|
type BatchReadItem<T> = Pick<
|
|
724
1029
|
StorageItem<T>,
|
|
725
|
-
| "
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
1030
|
+
"key" | "scope" | "get" | "deserialize"
|
|
1031
|
+
> & {
|
|
1032
|
+
_hasValidation?: boolean;
|
|
1033
|
+
_hasExpiration?: boolean;
|
|
1034
|
+
_readCacheEnabled?: boolean;
|
|
1035
|
+
_isBiometric?: boolean;
|
|
1036
|
+
_secureAccessControl?: AccessControl;
|
|
1037
|
+
};
|
|
733
1038
|
type BatchRemoveItem = Pick<StorageItem<unknown>, "key" | "scope" | "delete">;
|
|
734
1039
|
|
|
735
1040
|
export type StorageBatchSetItem<T> = {
|
|
@@ -739,7 +1044,7 @@ export type StorageBatchSetItem<T> = {
|
|
|
739
1044
|
|
|
740
1045
|
export function getBatch(
|
|
741
1046
|
items: readonly BatchReadItem<unknown>[],
|
|
742
|
-
scope: StorageScope
|
|
1047
|
+
scope: StorageScope,
|
|
743
1048
|
): unknown[] {
|
|
744
1049
|
assertBatchScope(items, scope);
|
|
745
1050
|
|
|
@@ -797,11 +1102,11 @@ export function getBatch(
|
|
|
797
1102
|
|
|
798
1103
|
export function setBatch<T>(
|
|
799
1104
|
items: readonly StorageBatchSetItem<T>[],
|
|
800
|
-
scope: StorageScope
|
|
1105
|
+
scope: StorageScope,
|
|
801
1106
|
): void {
|
|
802
1107
|
assertBatchScope(
|
|
803
1108
|
items.map((batchEntry) => batchEntry.item),
|
|
804
|
-
scope
|
|
1109
|
+
scope,
|
|
805
1110
|
);
|
|
806
1111
|
|
|
807
1112
|
if (scope === StorageScope.Memory) {
|
|
@@ -809,7 +1114,9 @@ export function setBatch<T>(
|
|
|
809
1114
|
return;
|
|
810
1115
|
}
|
|
811
1116
|
|
|
812
|
-
const useRawBatchPath = items.every(({ item }) =>
|
|
1117
|
+
const useRawBatchPath = items.every(({ item }) =>
|
|
1118
|
+
canUseRawBatchPath(asInternal(item)),
|
|
1119
|
+
);
|
|
813
1120
|
if (!useRawBatchPath) {
|
|
814
1121
|
items.forEach(({ item, value }) => item.set(value));
|
|
815
1122
|
return;
|
|
@@ -826,7 +1133,7 @@ export function setBatch<T>(
|
|
|
826
1133
|
|
|
827
1134
|
export function removeBatch(
|
|
828
1135
|
items: readonly BatchRemoveItem[],
|
|
829
|
-
scope: StorageScope
|
|
1136
|
+
scope: StorageScope,
|
|
830
1137
|
): void {
|
|
831
1138
|
assertBatchScope(items, scope);
|
|
832
1139
|
|
|
@@ -855,7 +1162,9 @@ export function registerMigration(version: number, migration: Migration): void {
|
|
|
855
1162
|
registeredMigrations.set(version, migration);
|
|
856
1163
|
}
|
|
857
1164
|
|
|
858
|
-
export function migrateToLatest(
|
|
1165
|
+
export function migrateToLatest(
|
|
1166
|
+
scope: StorageScope = StorageScope.Disk,
|
|
1167
|
+
): number {
|
|
859
1168
|
assertValidScope(scope);
|
|
860
1169
|
const currentVersion = readMigrationVersion(scope);
|
|
861
1170
|
const versions = Array.from(registeredMigrations.keys())
|
|
@@ -885,7 +1194,7 @@ export function migrateToLatest(scope: StorageScope = StorageScope.Disk): number
|
|
|
885
1194
|
|
|
886
1195
|
export function runTransaction<T>(
|
|
887
1196
|
scope: StorageScope,
|
|
888
|
-
transaction: (context: TransactionContext) => T
|
|
1197
|
+
transaction: (context: TransactionContext) => T,
|
|
889
1198
|
): T {
|
|
890
1199
|
assertValidScope(scope);
|
|
891
1200
|
if (scope === StorageScope.Secure) {
|
|
@@ -943,3 +1252,35 @@ export function runTransaction<T>(
|
|
|
943
1252
|
throw error;
|
|
944
1253
|
}
|
|
945
1254
|
}
|
|
1255
|
+
|
|
1256
|
+
export type SecureAuthStorageConfig<K extends string = string> = Record<
|
|
1257
|
+
K,
|
|
1258
|
+
{
|
|
1259
|
+
ttlMs?: number;
|
|
1260
|
+
biometric?: boolean;
|
|
1261
|
+
accessControl?: AccessControl;
|
|
1262
|
+
}
|
|
1263
|
+
>;
|
|
1264
|
+
|
|
1265
|
+
export function createSecureAuthStorage<K extends string>(
|
|
1266
|
+
config: SecureAuthStorageConfig<K>,
|
|
1267
|
+
options?: { namespace?: string },
|
|
1268
|
+
): Record<K, StorageItem<string>> {
|
|
1269
|
+
const ns = options?.namespace ?? "auth";
|
|
1270
|
+
const result = {} as Record<K, StorageItem<string>>;
|
|
1271
|
+
|
|
1272
|
+
for (const key of Object.keys(config) as K[]) {
|
|
1273
|
+
const itemConfig = config[key];
|
|
1274
|
+
result[key] = createStorageItem<string>({
|
|
1275
|
+
key,
|
|
1276
|
+
scope: StorageScope.Secure,
|
|
1277
|
+
defaultValue: "",
|
|
1278
|
+
namespace: ns,
|
|
1279
|
+
biometric: itemConfig.biometric,
|
|
1280
|
+
accessControl: itemConfig.accessControl,
|
|
1281
|
+
expiration: itemConfig.ttlMs ? { ttlMs: itemConfig.ttlMs } : undefined,
|
|
1282
|
+
});
|
|
1283
|
+
}
|
|
1284
|
+
|
|
1285
|
+
return result;
|
|
1286
|
+
}
|