react-native-nitro-storage 0.3.1 → 0.4.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/README.md +334 -34
- package/android/CMakeLists.txt +2 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +26 -2
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +4 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +90 -18
- package/cpp/bindings/HybridStorage.cpp +214 -23
- package/cpp/bindings/HybridStorage.hpp +31 -3
- package/cpp/core/NativeStorageAdapter.hpp +4 -0
- package/ios/IOSStorageAdapterCpp.hpp +17 -0
- package/ios/IOSStorageAdapterCpp.mm +140 -10
- package/lib/commonjs/index.js +555 -288
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +750 -309
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/internal.js +25 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/storage-hooks.js +36 -0
- package/lib/commonjs/storage-hooks.js.map +1 -0
- package/lib/module/index.js +537 -287
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +732 -308
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/internal.js +24 -0
- package/lib/module/internal.js.map +1 -1
- package/lib/module/storage-hooks.js +30 -0
- package/lib/module/storage-hooks.js.map +1 -0
- package/lib/typescript/Storage.nitro.d.ts +4 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +41 -4
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +45 -4
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +1 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/storage-hooks.d.ts +10 -0
- package/lib/typescript/storage-hooks.d.ts.map +1 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +4 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +4 -0
- package/package.json +5 -3
- package/src/Storage.nitro.ts +4 -0
- package/src/index.ts +704 -324
- package/src/index.web.ts +929 -346
- package/src/internal.ts +28 -0
- package/src/storage-hooks.ts +48 -0
package/lib/module/index.web.js
CHANGED
|
@@ -1,13 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, prefixKey, isNamespaced } from "./internal";
|
|
3
|
+
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
4
|
+
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, toVersionToken, prefixKey, isNamespaced } from "./internal";
|
|
6
5
|
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
7
6
|
export { migrateFromMMKV } from "./migration";
|
|
8
7
|
function asInternal(item) {
|
|
9
8
|
return item;
|
|
10
9
|
}
|
|
10
|
+
function isUpdater(valueOrFn) {
|
|
11
|
+
return typeof valueOrFn === "function";
|
|
12
|
+
}
|
|
13
|
+
function typedKeys(record) {
|
|
14
|
+
return Object.keys(record);
|
|
15
|
+
}
|
|
11
16
|
const registeredMigrations = new Map();
|
|
12
17
|
const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
|
|
13
18
|
Promise.resolve().then(task);
|
|
@@ -16,17 +21,83 @@ const memoryStore = new Map();
|
|
|
16
21
|
const memoryListeners = new Map();
|
|
17
22
|
const webScopeListeners = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
|
|
18
23
|
const scopedRawCache = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
|
|
24
|
+
const webScopeKeyIndex = new Map([[StorageScope.Disk, new Set()], [StorageScope.Secure, new Set()]]);
|
|
25
|
+
const hydratedWebScopeKeyIndex = new Set();
|
|
19
26
|
const pendingSecureWrites = new Map();
|
|
20
27
|
let secureFlushScheduled = false;
|
|
21
28
|
const SECURE_WEB_PREFIX = "__secure_";
|
|
22
29
|
const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
23
30
|
let hasWarnedAboutWebBiometricFallback = false;
|
|
31
|
+
let hasWebStorageEventSubscription = false;
|
|
32
|
+
let metricsObserver;
|
|
33
|
+
const metricsCounters = new Map();
|
|
34
|
+
function recordMetric(operation, scope, durationMs, keysCount = 1) {
|
|
35
|
+
const existing = metricsCounters.get(operation);
|
|
36
|
+
if (!existing) {
|
|
37
|
+
metricsCounters.set(operation, {
|
|
38
|
+
count: 1,
|
|
39
|
+
totalDurationMs: durationMs,
|
|
40
|
+
maxDurationMs: durationMs
|
|
41
|
+
});
|
|
42
|
+
} else {
|
|
43
|
+
existing.count += 1;
|
|
44
|
+
existing.totalDurationMs += durationMs;
|
|
45
|
+
existing.maxDurationMs = Math.max(existing.maxDurationMs, durationMs);
|
|
46
|
+
}
|
|
47
|
+
metricsObserver?.({
|
|
48
|
+
operation,
|
|
49
|
+
scope,
|
|
50
|
+
durationMs,
|
|
51
|
+
keysCount
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
function measureOperation(operation, scope, fn, keysCount = 1) {
|
|
55
|
+
const start = Date.now();
|
|
56
|
+
try {
|
|
57
|
+
return fn();
|
|
58
|
+
} finally {
|
|
59
|
+
recordMetric(operation, scope, Date.now() - start, keysCount);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
function createLocalStorageWebSecureBackend() {
|
|
63
|
+
return {
|
|
64
|
+
getItem: key => globalThis.localStorage?.getItem(key) ?? null,
|
|
65
|
+
setItem: (key, value) => globalThis.localStorage?.setItem(key, value),
|
|
66
|
+
removeItem: key => globalThis.localStorage?.removeItem(key),
|
|
67
|
+
clear: () => globalThis.localStorage?.clear(),
|
|
68
|
+
getAllKeys: () => {
|
|
69
|
+
const storage = globalThis.localStorage;
|
|
70
|
+
if (!storage) return [];
|
|
71
|
+
const keys = [];
|
|
72
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
73
|
+
const key = storage.key(index);
|
|
74
|
+
if (key) {
|
|
75
|
+
keys.push(key);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return keys;
|
|
79
|
+
}
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
let webSecureStorageBackend = createLocalStorageWebSecureBackend();
|
|
24
83
|
function getBrowserStorage(scope) {
|
|
25
84
|
if (scope === StorageScope.Disk) {
|
|
26
85
|
return globalThis.localStorage;
|
|
27
86
|
}
|
|
28
87
|
if (scope === StorageScope.Secure) {
|
|
29
|
-
|
|
88
|
+
if (!webSecureStorageBackend) {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
return {
|
|
92
|
+
setItem: (key, value) => webSecureStorageBackend?.setItem(key, value),
|
|
93
|
+
getItem: key => webSecureStorageBackend?.getItem(key) ?? null,
|
|
94
|
+
removeItem: key => webSecureStorageBackend?.removeItem(key),
|
|
95
|
+
clear: () => webSecureStorageBackend?.clear(),
|
|
96
|
+
key: index => webSecureStorageBackend?.getAllKeys()[index] ?? null,
|
|
97
|
+
get length() {
|
|
98
|
+
return webSecureStorageBackend?.getAllKeys().length ?? 0;
|
|
99
|
+
}
|
|
100
|
+
};
|
|
30
101
|
}
|
|
31
102
|
return undefined;
|
|
32
103
|
}
|
|
@@ -42,6 +113,99 @@ function toBiometricStorageKey(key) {
|
|
|
42
113
|
function fromBiometricStorageKey(key) {
|
|
43
114
|
return key.slice(BIOMETRIC_WEB_PREFIX.length);
|
|
44
115
|
}
|
|
116
|
+
function getWebScopeKeyIndex(scope) {
|
|
117
|
+
return webScopeKeyIndex.get(scope);
|
|
118
|
+
}
|
|
119
|
+
function hydrateWebScopeKeyIndex(scope) {
|
|
120
|
+
if (hydratedWebScopeKeyIndex.has(scope)) {
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
const storage = getBrowserStorage(scope);
|
|
124
|
+
const keyIndex = getWebScopeKeyIndex(scope);
|
|
125
|
+
keyIndex.clear();
|
|
126
|
+
if (storage) {
|
|
127
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
128
|
+
const key = storage.key(index);
|
|
129
|
+
if (!key) {
|
|
130
|
+
continue;
|
|
131
|
+
}
|
|
132
|
+
if (scope === StorageScope.Disk) {
|
|
133
|
+
if (!key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
134
|
+
keyIndex.add(key);
|
|
135
|
+
}
|
|
136
|
+
continue;
|
|
137
|
+
}
|
|
138
|
+
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
139
|
+
keyIndex.add(fromSecureStorageKey(key));
|
|
140
|
+
continue;
|
|
141
|
+
}
|
|
142
|
+
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
143
|
+
keyIndex.add(fromBiometricStorageKey(key));
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
hydratedWebScopeKeyIndex.add(scope);
|
|
148
|
+
}
|
|
149
|
+
function ensureWebScopeKeyIndex(scope) {
|
|
150
|
+
hydrateWebScopeKeyIndex(scope);
|
|
151
|
+
return getWebScopeKeyIndex(scope);
|
|
152
|
+
}
|
|
153
|
+
function handleWebStorageEvent(event) {
|
|
154
|
+
const key = event.key;
|
|
155
|
+
if (key === null) {
|
|
156
|
+
clearScopeRawCache(StorageScope.Disk);
|
|
157
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
158
|
+
ensureWebScopeKeyIndex(StorageScope.Disk).clear();
|
|
159
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).clear();
|
|
160
|
+
notifyAllListeners(getScopedListeners(StorageScope.Disk));
|
|
161
|
+
notifyAllListeners(getScopedListeners(StorageScope.Secure));
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
165
|
+
const plainKey = fromSecureStorageKey(key);
|
|
166
|
+
if (event.newValue === null) {
|
|
167
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
168
|
+
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
169
|
+
} else {
|
|
170
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
171
|
+
cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
|
|
172
|
+
}
|
|
173
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
177
|
+
const plainKey = fromBiometricStorageKey(key);
|
|
178
|
+
if (event.newValue === null) {
|
|
179
|
+
if (getBrowserStorage(StorageScope.Secure)?.getItem(toSecureStorageKey(plainKey)) === null) {
|
|
180
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).delete(plainKey);
|
|
181
|
+
}
|
|
182
|
+
cacheRawValue(StorageScope.Secure, plainKey, undefined);
|
|
183
|
+
} else {
|
|
184
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(plainKey);
|
|
185
|
+
cacheRawValue(StorageScope.Secure, plainKey, event.newValue);
|
|
186
|
+
}
|
|
187
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), plainKey);
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
if (event.newValue === null) {
|
|
191
|
+
ensureWebScopeKeyIndex(StorageScope.Disk).delete(key);
|
|
192
|
+
cacheRawValue(StorageScope.Disk, key, undefined);
|
|
193
|
+
} else {
|
|
194
|
+
ensureWebScopeKeyIndex(StorageScope.Disk).add(key);
|
|
195
|
+
cacheRawValue(StorageScope.Disk, key, event.newValue);
|
|
196
|
+
}
|
|
197
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Disk), key);
|
|
198
|
+
}
|
|
199
|
+
function ensureWebStorageEventSubscription() {
|
|
200
|
+
if (hasWebStorageEventSubscription) {
|
|
201
|
+
return;
|
|
202
|
+
}
|
|
203
|
+
if (typeof window === "undefined" || typeof window.addEventListener !== "function") {
|
|
204
|
+
return;
|
|
205
|
+
}
|
|
206
|
+
window.addEventListener("storage", handleWebStorageEvent);
|
|
207
|
+
hasWebStorageEventSubscription = true;
|
|
208
|
+
}
|
|
45
209
|
function getScopedListeners(scope) {
|
|
46
210
|
return webScopeListeners.get(scope);
|
|
47
211
|
}
|
|
@@ -102,32 +266,46 @@ function flushSecureWrites() {
|
|
|
102
266
|
}
|
|
103
267
|
const writes = Array.from(pendingSecureWrites.values());
|
|
104
268
|
pendingSecureWrites.clear();
|
|
105
|
-
const
|
|
106
|
-
const valuesToSet = [];
|
|
269
|
+
const groupedSetWrites = new Map();
|
|
107
270
|
const keysToRemove = [];
|
|
108
271
|
writes.forEach(({
|
|
109
272
|
key,
|
|
110
|
-
value
|
|
273
|
+
value,
|
|
274
|
+
accessControl
|
|
111
275
|
}) => {
|
|
112
276
|
if (value === undefined) {
|
|
113
277
|
keysToRemove.push(key);
|
|
114
278
|
} else {
|
|
115
|
-
|
|
116
|
-
|
|
279
|
+
const resolvedAccessControl = accessControl ?? AccessControl.WhenUnlocked;
|
|
280
|
+
const existingGroup = groupedSetWrites.get(resolvedAccessControl);
|
|
281
|
+
const group = existingGroup ?? {
|
|
282
|
+
keys: [],
|
|
283
|
+
values: []
|
|
284
|
+
};
|
|
285
|
+
group.keys.push(key);
|
|
286
|
+
group.values.push(value);
|
|
287
|
+
if (!existingGroup) {
|
|
288
|
+
groupedSetWrites.set(resolvedAccessControl, group);
|
|
289
|
+
}
|
|
117
290
|
}
|
|
118
291
|
});
|
|
119
|
-
|
|
120
|
-
WebStorage.
|
|
121
|
-
|
|
292
|
+
groupedSetWrites.forEach((group, accessControl) => {
|
|
293
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
294
|
+
WebStorage.setBatch(group.keys, group.values, StorageScope.Secure);
|
|
295
|
+
});
|
|
122
296
|
if (keysToRemove.length > 0) {
|
|
123
297
|
WebStorage.removeBatch(keysToRemove, StorageScope.Secure);
|
|
124
298
|
}
|
|
125
299
|
}
|
|
126
|
-
function scheduleSecureWrite(key, value) {
|
|
127
|
-
|
|
300
|
+
function scheduleSecureWrite(key, value, accessControl) {
|
|
301
|
+
const pendingWrite = {
|
|
128
302
|
key,
|
|
129
303
|
value
|
|
130
|
-
}
|
|
304
|
+
};
|
|
305
|
+
if (accessControl !== undefined) {
|
|
306
|
+
pendingWrite.accessControl = accessControl;
|
|
307
|
+
}
|
|
308
|
+
pendingSecureWrites.set(key, pendingWrite);
|
|
131
309
|
if (secureFlushScheduled) {
|
|
132
310
|
return;
|
|
133
311
|
}
|
|
@@ -146,6 +324,7 @@ const WebStorage = {
|
|
|
146
324
|
const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
147
325
|
storage.setItem(storageKey, value);
|
|
148
326
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
327
|
+
ensureWebScopeKeyIndex(scope).add(key);
|
|
149
328
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
150
329
|
}
|
|
151
330
|
},
|
|
@@ -166,6 +345,7 @@ const WebStorage = {
|
|
|
166
345
|
storage.removeItem(key);
|
|
167
346
|
}
|
|
168
347
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
348
|
+
ensureWebScopeKeyIndex(scope).delete(key);
|
|
169
349
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
170
350
|
}
|
|
171
351
|
},
|
|
@@ -196,6 +376,7 @@ const WebStorage = {
|
|
|
196
376
|
storage.clear();
|
|
197
377
|
}
|
|
198
378
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
379
|
+
ensureWebScopeKeyIndex(scope).clear();
|
|
199
380
|
notifyAllListeners(getScopedListeners(scope));
|
|
200
381
|
}
|
|
201
382
|
},
|
|
@@ -205,10 +386,16 @@ const WebStorage = {
|
|
|
205
386
|
return;
|
|
206
387
|
}
|
|
207
388
|
keys.forEach((key, index) => {
|
|
389
|
+
const value = values[index];
|
|
390
|
+
if (value === undefined) {
|
|
391
|
+
return;
|
|
392
|
+
}
|
|
208
393
|
const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
209
|
-
storage.setItem(storageKey,
|
|
394
|
+
storage.setItem(storageKey, value);
|
|
210
395
|
});
|
|
211
396
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
397
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
398
|
+
keys.forEach(key => keyIndex.add(key));
|
|
212
399
|
const listeners = getScopedListeners(scope);
|
|
213
400
|
keys.forEach(key => notifyKeyListeners(listeners, key));
|
|
214
401
|
}
|
|
@@ -221,9 +408,37 @@ const WebStorage = {
|
|
|
221
408
|
});
|
|
222
409
|
},
|
|
223
410
|
removeBatch: (keys, scope) => {
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
411
|
+
const storage = getBrowserStorage(scope);
|
|
412
|
+
if (!storage) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
415
|
+
if (scope === StorageScope.Secure) {
|
|
416
|
+
keys.forEach(key => {
|
|
417
|
+
storage.removeItem(toSecureStorageKey(key));
|
|
418
|
+
storage.removeItem(toBiometricStorageKey(key));
|
|
419
|
+
});
|
|
420
|
+
} else {
|
|
421
|
+
keys.forEach(key => {
|
|
422
|
+
storage.removeItem(key);
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
426
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
427
|
+
keys.forEach(key => keyIndex.delete(key));
|
|
428
|
+
const listeners = getScopedListeners(scope);
|
|
429
|
+
keys.forEach(key => notifyKeyListeners(listeners, key));
|
|
430
|
+
}
|
|
431
|
+
},
|
|
432
|
+
removeByPrefix: (prefix, scope) => {
|
|
433
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
434
|
+
return;
|
|
435
|
+
}
|
|
436
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
437
|
+
const keys = Array.from(keyIndex).filter(key => key.startsWith(prefix));
|
|
438
|
+
if (keys.length === 0) {
|
|
439
|
+
return;
|
|
440
|
+
}
|
|
441
|
+
WebStorage.removeBatch(keys, scope);
|
|
227
442
|
},
|
|
228
443
|
addOnChange: (_scope, _callback) => {
|
|
229
444
|
return () => {};
|
|
@@ -236,54 +451,54 @@ const WebStorage = {
|
|
|
236
451
|
return storage?.getItem(key) !== null;
|
|
237
452
|
},
|
|
238
453
|
getAllKeys: scope => {
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
const keys = new Set();
|
|
242
|
-
for (let i = 0; i < storage.length; i++) {
|
|
243
|
-
const k = storage.key(i);
|
|
244
|
-
if (!k) {
|
|
245
|
-
continue;
|
|
246
|
-
}
|
|
247
|
-
if (scope === StorageScope.Secure) {
|
|
248
|
-
if (k.startsWith(SECURE_WEB_PREFIX)) {
|
|
249
|
-
keys.add(fromSecureStorageKey(k));
|
|
250
|
-
} else if (k.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
251
|
-
keys.add(fromBiometricStorageKey(k));
|
|
252
|
-
}
|
|
253
|
-
continue;
|
|
254
|
-
}
|
|
255
|
-
if (k.startsWith(SECURE_WEB_PREFIX) || k.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
256
|
-
continue;
|
|
257
|
-
}
|
|
258
|
-
keys.add(k);
|
|
454
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
455
|
+
return [];
|
|
259
456
|
}
|
|
260
|
-
return Array.from(
|
|
457
|
+
return Array.from(ensureWebScopeKeyIndex(scope));
|
|
458
|
+
},
|
|
459
|
+
getKeysByPrefix: (prefix, scope) => {
|
|
460
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
461
|
+
return [];
|
|
462
|
+
}
|
|
463
|
+
return Array.from(ensureWebScopeKeyIndex(scope)).filter(key => key.startsWith(prefix));
|
|
261
464
|
},
|
|
262
465
|
size: scope => {
|
|
263
|
-
|
|
466
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
467
|
+
return ensureWebScopeKeyIndex(scope).size;
|
|
468
|
+
}
|
|
469
|
+
return 0;
|
|
264
470
|
},
|
|
265
471
|
setSecureAccessControl: () => {},
|
|
472
|
+
setSecureWritesAsync: _enabled => {},
|
|
266
473
|
setKeychainAccessGroup: () => {},
|
|
267
474
|
setSecureBiometric: (key, value) => {
|
|
475
|
+
WebStorage.setSecureBiometricWithLevel(key, value, BiometricLevel.BiometryOnly);
|
|
476
|
+
},
|
|
477
|
+
setSecureBiometricWithLevel: (key, value, _level) => {
|
|
268
478
|
if (typeof __DEV__ !== "undefined" && __DEV__ && !hasWarnedAboutWebBiometricFallback) {
|
|
269
479
|
hasWarnedAboutWebBiometricFallback = true;
|
|
270
480
|
console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
|
|
271
481
|
}
|
|
272
|
-
|
|
482
|
+
getBrowserStorage(StorageScope.Secure)?.setItem(toBiometricStorageKey(key), value);
|
|
483
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
273
484
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
274
485
|
},
|
|
275
486
|
getSecureBiometric: key => {
|
|
276
|
-
return
|
|
487
|
+
return getBrowserStorage(StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) ?? undefined;
|
|
277
488
|
},
|
|
278
489
|
deleteSecureBiometric: key => {
|
|
279
|
-
|
|
490
|
+
const storage = getBrowserStorage(StorageScope.Secure);
|
|
491
|
+
storage?.removeItem(toBiometricStorageKey(key));
|
|
492
|
+
if (storage?.getItem(toSecureStorageKey(key)) === null) {
|
|
493
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
|
|
494
|
+
}
|
|
280
495
|
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
281
496
|
},
|
|
282
497
|
hasSecureBiometric: key => {
|
|
283
|
-
return
|
|
498
|
+
return getBrowserStorage(StorageScope.Secure)?.getItem(toBiometricStorageKey(key)) !== null;
|
|
284
499
|
},
|
|
285
500
|
clearSecureBiometric: () => {
|
|
286
|
-
const storage =
|
|
501
|
+
const storage = getBrowserStorage(StorageScope.Secure);
|
|
287
502
|
if (!storage) return;
|
|
288
503
|
const keysToNotify = [];
|
|
289
504
|
const toRemove = [];
|
|
@@ -295,6 +510,12 @@ const WebStorage = {
|
|
|
295
510
|
}
|
|
296
511
|
}
|
|
297
512
|
toRemove.forEach(k => storage.removeItem(k));
|
|
513
|
+
const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
|
|
514
|
+
keysToNotify.forEach(key => {
|
|
515
|
+
if (storage.getItem(toSecureStorageKey(key)) === null) {
|
|
516
|
+
keyIndex.delete(key);
|
|
517
|
+
}
|
|
518
|
+
});
|
|
298
519
|
const listeners = getScopedListeners(StorageScope.Secure);
|
|
299
520
|
keysToNotify.forEach(key => notifyKeyListeners(listeners, key));
|
|
300
521
|
}
|
|
@@ -351,90 +572,173 @@ function writeMigrationVersion(scope, version) {
|
|
|
351
572
|
}
|
|
352
573
|
export const storage = {
|
|
353
574
|
clear: scope => {
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
}
|
|
575
|
+
measureOperation("storage:clear", scope, () => {
|
|
576
|
+
if (scope === StorageScope.Memory) {
|
|
577
|
+
memoryStore.clear();
|
|
578
|
+
notifyAllListeners(memoryListeners);
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (scope === StorageScope.Secure) {
|
|
582
|
+
flushSecureWrites();
|
|
583
|
+
pendingSecureWrites.clear();
|
|
584
|
+
}
|
|
585
|
+
clearScopeRawCache(scope);
|
|
586
|
+
WebStorage.clear(scope);
|
|
587
|
+
});
|
|
368
588
|
},
|
|
369
589
|
clearAll: () => {
|
|
370
|
-
storage
|
|
371
|
-
|
|
372
|
-
|
|
590
|
+
measureOperation("storage:clearAll", StorageScope.Memory, () => {
|
|
591
|
+
storage.clear(StorageScope.Memory);
|
|
592
|
+
storage.clear(StorageScope.Disk);
|
|
593
|
+
storage.clear(StorageScope.Secure);
|
|
594
|
+
}, 3);
|
|
373
595
|
},
|
|
374
596
|
clearNamespace: (namespace, scope) => {
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
597
|
+
measureOperation("storage:clearNamespace", scope, () => {
|
|
598
|
+
assertValidScope(scope);
|
|
599
|
+
if (scope === StorageScope.Memory) {
|
|
600
|
+
for (const key of memoryStore.keys()) {
|
|
601
|
+
if (isNamespaced(key, namespace)) {
|
|
602
|
+
memoryStore.delete(key);
|
|
603
|
+
}
|
|
380
604
|
}
|
|
605
|
+
notifyAllListeners(memoryListeners);
|
|
606
|
+
return;
|
|
381
607
|
}
|
|
382
|
-
|
|
383
|
-
return;
|
|
384
|
-
}
|
|
385
|
-
if (scope === StorageScope.Secure) {
|
|
386
|
-
flushSecureWrites();
|
|
387
|
-
}
|
|
388
|
-
const keys = WebStorage.getAllKeys(scope);
|
|
389
|
-
const namespacedKeys = keys.filter(k => isNamespaced(k, namespace));
|
|
390
|
-
if (namespacedKeys.length > 0) {
|
|
391
|
-
WebStorage.removeBatch(namespacedKeys, scope);
|
|
392
|
-
namespacedKeys.forEach(k => cacheRawValue(scope, k, undefined));
|
|
608
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
393
609
|
if (scope === StorageScope.Secure) {
|
|
394
|
-
|
|
610
|
+
flushSecureWrites();
|
|
395
611
|
}
|
|
396
|
-
|
|
612
|
+
clearScopeRawCache(scope);
|
|
613
|
+
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
614
|
+
});
|
|
397
615
|
},
|
|
398
616
|
clearBiometric: () => {
|
|
399
|
-
|
|
617
|
+
measureOperation("storage:clearBiometric", StorageScope.Secure, () => {
|
|
618
|
+
WebStorage.clearSecureBiometric();
|
|
619
|
+
});
|
|
400
620
|
},
|
|
401
621
|
has: (key, scope) => {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
622
|
+
return measureOperation("storage:has", scope, () => {
|
|
623
|
+
assertValidScope(scope);
|
|
624
|
+
if (scope === StorageScope.Memory) return memoryStore.has(key);
|
|
625
|
+
return WebStorage.has(key, scope);
|
|
626
|
+
});
|
|
405
627
|
},
|
|
406
628
|
getAllKeys: scope => {
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
629
|
+
return measureOperation("storage:getAllKeys", scope, () => {
|
|
630
|
+
assertValidScope(scope);
|
|
631
|
+
if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
|
|
632
|
+
return WebStorage.getAllKeys(scope);
|
|
633
|
+
});
|
|
634
|
+
},
|
|
635
|
+
getKeysByPrefix: (prefix, scope) => {
|
|
636
|
+
return measureOperation("storage:getKeysByPrefix", scope, () => {
|
|
637
|
+
assertValidScope(scope);
|
|
638
|
+
if (scope === StorageScope.Memory) {
|
|
639
|
+
return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
|
|
640
|
+
}
|
|
641
|
+
return WebStorage.getKeysByPrefix(prefix, scope);
|
|
642
|
+
});
|
|
643
|
+
},
|
|
644
|
+
getByPrefix: (prefix, scope) => {
|
|
645
|
+
return measureOperation("storage:getByPrefix", scope, () => {
|
|
646
|
+
const result = {};
|
|
647
|
+
const keys = storage.getKeysByPrefix(prefix, scope);
|
|
648
|
+
if (keys.length === 0) {
|
|
649
|
+
return result;
|
|
650
|
+
}
|
|
651
|
+
if (scope === StorageScope.Memory) {
|
|
652
|
+
keys.forEach(key => {
|
|
653
|
+
const value = memoryStore.get(key);
|
|
654
|
+
if (typeof value === "string") {
|
|
655
|
+
result[key] = value;
|
|
656
|
+
}
|
|
657
|
+
});
|
|
658
|
+
return result;
|
|
659
|
+
}
|
|
660
|
+
const values = WebStorage.getBatch(keys, scope);
|
|
661
|
+
keys.forEach((key, index) => {
|
|
662
|
+
const value = values[index];
|
|
663
|
+
if (value !== undefined) {
|
|
664
|
+
result[key] = value;
|
|
665
|
+
}
|
|
666
|
+
});
|
|
667
|
+
return result;
|
|
668
|
+
});
|
|
410
669
|
},
|
|
411
670
|
getAll: scope => {
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
671
|
+
return measureOperation("storage:getAll", scope, () => {
|
|
672
|
+
assertValidScope(scope);
|
|
673
|
+
const result = {};
|
|
674
|
+
if (scope === StorageScope.Memory) {
|
|
675
|
+
memoryStore.forEach((value, key) => {
|
|
676
|
+
if (typeof value === "string") result[key] = value;
|
|
677
|
+
});
|
|
678
|
+
return result;
|
|
679
|
+
}
|
|
680
|
+
const keys = WebStorage.getAllKeys(scope);
|
|
681
|
+
keys.forEach(key => {
|
|
682
|
+
const val = WebStorage.get(key, scope);
|
|
683
|
+
if (val !== undefined) result[key] = val;
|
|
417
684
|
});
|
|
418
685
|
return result;
|
|
419
|
-
}
|
|
420
|
-
const keys = WebStorage.getAllKeys(scope);
|
|
421
|
-
keys.forEach(key => {
|
|
422
|
-
const val = WebStorage.get(key, scope);
|
|
423
|
-
if (val !== undefined) result[key] = val;
|
|
424
686
|
});
|
|
425
|
-
return result;
|
|
426
687
|
},
|
|
427
688
|
size: scope => {
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
689
|
+
return measureOperation("storage:size", scope, () => {
|
|
690
|
+
assertValidScope(scope);
|
|
691
|
+
if (scope === StorageScope.Memory) return memoryStore.size;
|
|
692
|
+
return WebStorage.size(scope);
|
|
693
|
+
});
|
|
694
|
+
},
|
|
695
|
+
setAccessControl: _level => {
|
|
696
|
+
recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
|
|
697
|
+
},
|
|
698
|
+
setSecureWritesAsync: _enabled => {
|
|
699
|
+
recordMetric("storage:setSecureWritesAsync", StorageScope.Secure, 0);
|
|
700
|
+
},
|
|
701
|
+
flushSecureWrites: () => {
|
|
702
|
+
measureOperation("storage:flushSecureWrites", StorageScope.Secure, () => {
|
|
703
|
+
flushSecureWrites();
|
|
704
|
+
});
|
|
705
|
+
},
|
|
706
|
+
setKeychainAccessGroup: _group => {
|
|
707
|
+
recordMetric("storage:setKeychainAccessGroup", StorageScope.Secure, 0);
|
|
431
708
|
},
|
|
432
|
-
|
|
433
|
-
|
|
709
|
+
setMetricsObserver: observer => {
|
|
710
|
+
metricsObserver = observer;
|
|
711
|
+
},
|
|
712
|
+
getMetricsSnapshot: () => {
|
|
713
|
+
const snapshot = {};
|
|
714
|
+
metricsCounters.forEach((value, key) => {
|
|
715
|
+
snapshot[key] = {
|
|
716
|
+
count: value.count,
|
|
717
|
+
totalDurationMs: value.totalDurationMs,
|
|
718
|
+
avgDurationMs: value.count === 0 ? 0 : value.totalDurationMs / value.count,
|
|
719
|
+
maxDurationMs: value.maxDurationMs
|
|
720
|
+
};
|
|
721
|
+
});
|
|
722
|
+
return snapshot;
|
|
723
|
+
},
|
|
724
|
+
resetMetrics: () => {
|
|
725
|
+
metricsCounters.clear();
|
|
726
|
+
}
|
|
434
727
|
};
|
|
728
|
+
export function setWebSecureStorageBackend(backend) {
|
|
729
|
+
webSecureStorageBackend = backend ?? createLocalStorageWebSecureBackend();
|
|
730
|
+
hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
|
|
731
|
+
clearScopeRawCache(StorageScope.Secure);
|
|
732
|
+
}
|
|
733
|
+
export function getWebSecureStorageBackend() {
|
|
734
|
+
return webSecureStorageBackend;
|
|
735
|
+
}
|
|
435
736
|
function canUseRawBatchPath(item) {
|
|
436
737
|
return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
|
|
437
738
|
}
|
|
739
|
+
function canUseSecureRawBatchPath(item) {
|
|
740
|
+
return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true;
|
|
741
|
+
}
|
|
438
742
|
function defaultSerialize(value) {
|
|
439
743
|
return serializeWithPrimitiveFastPath(value);
|
|
440
744
|
}
|
|
@@ -446,7 +750,8 @@ export function createStorageItem(config) {
|
|
|
446
750
|
const serialize = config.serialize ?? defaultSerialize;
|
|
447
751
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
448
752
|
const isMemory = config.scope === StorageScope.Memory;
|
|
449
|
-
const
|
|
753
|
+
const resolvedBiometricLevel = config.scope === StorageScope.Secure ? config.biometricLevel ?? (config.biometric === true ? BiometricLevel.BiometryOnly : BiometricLevel.None) : BiometricLevel.None;
|
|
754
|
+
const isBiometric = resolvedBiometricLevel !== BiometricLevel.None;
|
|
450
755
|
const secureAccessControl = config.accessControl;
|
|
451
756
|
const validate = config.validate;
|
|
452
757
|
const onValidationError = config.onValidationError;
|
|
@@ -455,7 +760,8 @@ export function createStorageItem(config) {
|
|
|
455
760
|
const expirationTtlMs = expiration?.ttlMs;
|
|
456
761
|
const memoryExpiration = expiration && isMemory ? new Map() : null;
|
|
457
762
|
const readCache = !isMemory && config.readCache === true;
|
|
458
|
-
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric
|
|
763
|
+
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
|
|
764
|
+
const defaultValue = config.defaultValue;
|
|
459
765
|
const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
|
|
460
766
|
if (expiration && expiration.ttlMs <= 0) {
|
|
461
767
|
throw new Error("expiration.ttlMs must be greater than 0.");
|
|
@@ -465,10 +771,12 @@ export function createStorageItem(config) {
|
|
|
465
771
|
let lastRaw = undefined;
|
|
466
772
|
let lastValue;
|
|
467
773
|
let hasLastValue = false;
|
|
774
|
+
let lastExpiresAt = undefined;
|
|
468
775
|
const invalidateParsedCache = () => {
|
|
469
776
|
lastRaw = undefined;
|
|
470
777
|
lastValue = undefined;
|
|
471
778
|
hasLastValue = false;
|
|
779
|
+
lastExpiresAt = undefined;
|
|
472
780
|
};
|
|
473
781
|
const ensureSubscription = () => {
|
|
474
782
|
if (unsubscribe) {
|
|
@@ -482,6 +790,7 @@ export function createStorageItem(config) {
|
|
|
482
790
|
unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
|
|
483
791
|
return;
|
|
484
792
|
}
|
|
793
|
+
ensureWebStorageEventSubscription();
|
|
485
794
|
unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
|
|
486
795
|
};
|
|
487
796
|
const readStoredRaw = () => {
|
|
@@ -515,12 +824,12 @@ export function createStorageItem(config) {
|
|
|
515
824
|
};
|
|
516
825
|
const writeStoredRaw = rawValue => {
|
|
517
826
|
if (isBiometric) {
|
|
518
|
-
WebStorage.
|
|
827
|
+
WebStorage.setSecureBiometricWithLevel(storageKey, rawValue, resolvedBiometricLevel);
|
|
519
828
|
return;
|
|
520
829
|
}
|
|
521
830
|
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
522
831
|
if (coalesceSecureWrites) {
|
|
523
|
-
scheduleSecureWrite(storageKey, rawValue);
|
|
832
|
+
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? AccessControl.WhenUnlocked);
|
|
524
833
|
return;
|
|
525
834
|
}
|
|
526
835
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -535,7 +844,7 @@ export function createStorageItem(config) {
|
|
|
535
844
|
}
|
|
536
845
|
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
537
846
|
if (coalesceSecureWrites) {
|
|
538
|
-
scheduleSecureWrite(storageKey, undefined);
|
|
847
|
+
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? AccessControl.WhenUnlocked);
|
|
539
848
|
return;
|
|
540
849
|
}
|
|
541
850
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
@@ -568,7 +877,7 @@ export function createStorageItem(config) {
|
|
|
568
877
|
if (onValidationError) {
|
|
569
878
|
return onValidationError(invalidValue);
|
|
570
879
|
}
|
|
571
|
-
return
|
|
880
|
+
return defaultValue;
|
|
572
881
|
};
|
|
573
882
|
const ensureValidatedValue = (candidate, hadStoredValue) => {
|
|
574
883
|
if (!validate || validate(candidate)) {
|
|
@@ -576,40 +885,62 @@ export function createStorageItem(config) {
|
|
|
576
885
|
}
|
|
577
886
|
const resolved = resolveInvalidValue(candidate);
|
|
578
887
|
if (validate && !validate(resolved)) {
|
|
579
|
-
return
|
|
888
|
+
return defaultValue;
|
|
580
889
|
}
|
|
581
890
|
if (hadStoredValue) {
|
|
582
891
|
writeValueWithoutValidation(resolved);
|
|
583
892
|
}
|
|
584
893
|
return resolved;
|
|
585
894
|
};
|
|
586
|
-
const
|
|
895
|
+
const getInternal = () => {
|
|
587
896
|
const raw = readStoredRaw();
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
897
|
+
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
898
|
+
if (!expiration || lastExpiresAt === null) {
|
|
899
|
+
return lastValue;
|
|
900
|
+
}
|
|
901
|
+
if (typeof lastExpiresAt === "number") {
|
|
902
|
+
if (lastExpiresAt > Date.now()) {
|
|
903
|
+
return lastValue;
|
|
904
|
+
}
|
|
905
|
+
removeStoredRaw();
|
|
906
|
+
invalidateParsedCache();
|
|
907
|
+
onExpired?.(storageKey);
|
|
908
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
909
|
+
hasLastValue = true;
|
|
910
|
+
return lastValue;
|
|
911
|
+
}
|
|
591
912
|
}
|
|
592
913
|
lastRaw = raw;
|
|
593
914
|
if (raw === undefined) {
|
|
594
|
-
|
|
915
|
+
lastExpiresAt = undefined;
|
|
916
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
595
917
|
hasLastValue = true;
|
|
596
918
|
return lastValue;
|
|
597
919
|
}
|
|
598
920
|
if (isMemory) {
|
|
921
|
+
lastExpiresAt = undefined;
|
|
599
922
|
lastValue = ensureValidatedValue(raw, true);
|
|
600
923
|
hasLastValue = true;
|
|
601
924
|
return lastValue;
|
|
602
925
|
}
|
|
926
|
+
if (typeof raw !== "string") {
|
|
927
|
+
lastExpiresAt = undefined;
|
|
928
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
929
|
+
hasLastValue = true;
|
|
930
|
+
return lastValue;
|
|
931
|
+
}
|
|
603
932
|
let deserializableRaw = raw;
|
|
604
933
|
if (expiration) {
|
|
934
|
+
let envelopeExpiresAt = null;
|
|
605
935
|
try {
|
|
606
936
|
const parsed = JSON.parse(raw);
|
|
607
937
|
if (isStoredEnvelope(parsed)) {
|
|
938
|
+
envelopeExpiresAt = parsed.expiresAt;
|
|
608
939
|
if (parsed.expiresAt <= Date.now()) {
|
|
609
940
|
removeStoredRaw();
|
|
610
941
|
invalidateParsedCache();
|
|
611
942
|
onExpired?.(storageKey);
|
|
612
|
-
lastValue = ensureValidatedValue(
|
|
943
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
613
944
|
hasLastValue = true;
|
|
614
945
|
return lastValue;
|
|
615
946
|
}
|
|
@@ -618,37 +949,60 @@ export function createStorageItem(config) {
|
|
|
618
949
|
} catch {
|
|
619
950
|
// Keep backward compatibility with legacy raw values.
|
|
620
951
|
}
|
|
952
|
+
lastExpiresAt = envelopeExpiresAt;
|
|
953
|
+
} else {
|
|
954
|
+
lastExpiresAt = undefined;
|
|
621
955
|
}
|
|
622
956
|
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
623
957
|
hasLastValue = true;
|
|
624
958
|
return lastValue;
|
|
625
959
|
};
|
|
960
|
+
const getCurrentVersion = () => {
|
|
961
|
+
const raw = readStoredRaw();
|
|
962
|
+
return toVersionToken(raw);
|
|
963
|
+
};
|
|
964
|
+
const get = () => measureOperation("item:get", config.scope, () => getInternal());
|
|
965
|
+
const getWithVersion = () => measureOperation("item:getWithVersion", config.scope, () => ({
|
|
966
|
+
value: getInternal(),
|
|
967
|
+
version: getCurrentVersion()
|
|
968
|
+
}));
|
|
626
969
|
const set = valueOrFn => {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
970
|
+
measureOperation("item:set", config.scope, () => {
|
|
971
|
+
const newValue = isUpdater(valueOrFn) ? valueOrFn(getInternal()) : valueOrFn;
|
|
972
|
+
invalidateParsedCache();
|
|
973
|
+
if (validate && !validate(newValue)) {
|
|
974
|
+
throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
|
|
975
|
+
}
|
|
976
|
+
writeValueWithoutValidation(newValue);
|
|
977
|
+
});
|
|
634
978
|
};
|
|
979
|
+
const setIfVersion = (version, valueOrFn) => measureOperation("item:setIfVersion", config.scope, () => {
|
|
980
|
+
const currentVersion = getCurrentVersion();
|
|
981
|
+
if (currentVersion !== version) {
|
|
982
|
+
return false;
|
|
983
|
+
}
|
|
984
|
+
set(valueOrFn);
|
|
985
|
+
return true;
|
|
986
|
+
});
|
|
635
987
|
const deleteItem = () => {
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
if (
|
|
639
|
-
memoryExpiration
|
|
988
|
+
measureOperation("item:delete", config.scope, () => {
|
|
989
|
+
invalidateParsedCache();
|
|
990
|
+
if (isMemory) {
|
|
991
|
+
if (memoryExpiration) {
|
|
992
|
+
memoryExpiration.delete(storageKey);
|
|
993
|
+
}
|
|
994
|
+
memoryStore.delete(storageKey);
|
|
995
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
996
|
+
return;
|
|
640
997
|
}
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
return;
|
|
644
|
-
}
|
|
645
|
-
removeStoredRaw();
|
|
998
|
+
removeStoredRaw();
|
|
999
|
+
});
|
|
646
1000
|
};
|
|
647
|
-
const hasItem = () => {
|
|
1001
|
+
const hasItem = () => measureOperation("item:has", config.scope, () => {
|
|
648
1002
|
if (isMemory) return memoryStore.has(storageKey);
|
|
649
1003
|
if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
|
|
650
1004
|
return WebStorage.has(storageKey, config.scope);
|
|
651
|
-
};
|
|
1005
|
+
});
|
|
652
1006
|
const subscribe = callback => {
|
|
653
1007
|
ensureSubscription();
|
|
654
1008
|
listeners.add(callback);
|
|
@@ -662,7 +1016,9 @@ export function createStorageItem(config) {
|
|
|
662
1016
|
};
|
|
663
1017
|
const storageItem = {
|
|
664
1018
|
get,
|
|
1019
|
+
getWithVersion,
|
|
665
1020
|
set,
|
|
1021
|
+
setIfVersion,
|
|
666
1022
|
delete: deleteItem,
|
|
667
1023
|
has: hasItem,
|
|
668
1024
|
subscribe,
|
|
@@ -676,123 +1032,151 @@ export function createStorageItem(config) {
|
|
|
676
1032
|
_hasExpiration: expiration !== undefined,
|
|
677
1033
|
_readCacheEnabled: readCache,
|
|
678
1034
|
_isBiometric: isBiometric,
|
|
679
|
-
|
|
1035
|
+
_defaultValue: defaultValue,
|
|
1036
|
+
...(secureAccessControl !== undefined ? {
|
|
1037
|
+
_secureAccessControl: secureAccessControl
|
|
1038
|
+
} : {}),
|
|
680
1039
|
scope: config.scope,
|
|
681
1040
|
key: storageKey
|
|
682
1041
|
};
|
|
683
1042
|
return storageItem;
|
|
684
1043
|
}
|
|
685
|
-
export
|
|
686
|
-
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
687
|
-
return [value, item.set];
|
|
688
|
-
}
|
|
689
|
-
export function useStorageSelector(item, selector, isEqual = Object.is) {
|
|
690
|
-
const selectedRef = useRef({
|
|
691
|
-
hasValue: false
|
|
692
|
-
});
|
|
693
|
-
const getSelectedSnapshot = () => {
|
|
694
|
-
const nextSelected = selector(item.get());
|
|
695
|
-
const current = selectedRef.current;
|
|
696
|
-
if (current.hasValue && isEqual(current.value, nextSelected)) {
|
|
697
|
-
return current.value;
|
|
698
|
-
}
|
|
699
|
-
selectedRef.current = {
|
|
700
|
-
hasValue: true,
|
|
701
|
-
value: nextSelected
|
|
702
|
-
};
|
|
703
|
-
return nextSelected;
|
|
704
|
-
};
|
|
705
|
-
const selectedValue = useSyncExternalStore(item.subscribe, getSelectedSnapshot, getSelectedSnapshot);
|
|
706
|
-
return [selectedValue, item.set];
|
|
707
|
-
}
|
|
708
|
-
export function useSetStorage(item) {
|
|
709
|
-
return item.set;
|
|
710
|
-
}
|
|
1044
|
+
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
711
1045
|
export function getBatch(items, scope) {
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
const useRawBatchPath = items.every(item => canUseRawBatchPath(item));
|
|
717
|
-
if (!useRawBatchPath) {
|
|
718
|
-
return items.map(item => item.get());
|
|
719
|
-
}
|
|
720
|
-
const useBatchCache = items.every(item => item._readCacheEnabled === true);
|
|
721
|
-
const rawValues = new Array(items.length);
|
|
722
|
-
const keysToFetch = [];
|
|
723
|
-
const keyIndexes = [];
|
|
724
|
-
items.forEach((item, index) => {
|
|
725
|
-
if (scope === StorageScope.Secure) {
|
|
726
|
-
if (hasPendingSecureWrite(item.key)) {
|
|
727
|
-
rawValues[index] = readPendingSecureWrite(item.key);
|
|
728
|
-
return;
|
|
729
|
-
}
|
|
1046
|
+
return measureOperation("batch:get", scope, () => {
|
|
1047
|
+
assertBatchScope(items, scope);
|
|
1048
|
+
if (scope === StorageScope.Memory) {
|
|
1049
|
+
return items.map(item => item.get());
|
|
730
1050
|
}
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
return;
|
|
735
|
-
}
|
|
1051
|
+
const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
|
|
1052
|
+
if (!useRawBatchPath) {
|
|
1053
|
+
return items.map(item => item.get());
|
|
736
1054
|
}
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
1055
|
+
const rawValues = new Array(items.length);
|
|
1056
|
+
const keysToFetch = [];
|
|
1057
|
+
const keyIndexes = [];
|
|
1058
|
+
items.forEach((item, index) => {
|
|
1059
|
+
if (scope === StorageScope.Secure) {
|
|
1060
|
+
if (hasPendingSecureWrite(item.key)) {
|
|
1061
|
+
rawValues[index] = readPendingSecureWrite(item.key);
|
|
1062
|
+
return;
|
|
1063
|
+
}
|
|
1064
|
+
}
|
|
1065
|
+
if (item._readCacheEnabled === true) {
|
|
1066
|
+
if (hasCachedRawValue(scope, item.key)) {
|
|
1067
|
+
rawValues[index] = readCachedRawValue(scope, item.key);
|
|
1068
|
+
return;
|
|
1069
|
+
}
|
|
1070
|
+
}
|
|
1071
|
+
keysToFetch.push(item.key);
|
|
1072
|
+
keyIndexes.push(index);
|
|
747
1073
|
});
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
1074
|
+
if (keysToFetch.length > 0) {
|
|
1075
|
+
const fetchedValues = WebStorage.getBatch(keysToFetch, scope);
|
|
1076
|
+
fetchedValues.forEach((value, index) => {
|
|
1077
|
+
const key = keysToFetch[index];
|
|
1078
|
+
const targetIndex = keyIndexes[index];
|
|
1079
|
+
if (key === undefined || targetIndex === undefined) {
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
rawValues[targetIndex] = value;
|
|
1083
|
+
cacheRawValue(scope, key, value);
|
|
1084
|
+
});
|
|
753
1085
|
}
|
|
754
|
-
return
|
|
755
|
-
|
|
1086
|
+
return items.map((item, index) => {
|
|
1087
|
+
const raw = rawValues[index];
|
|
1088
|
+
if (raw === undefined) {
|
|
1089
|
+
return asInternal(item)._defaultValue;
|
|
1090
|
+
}
|
|
1091
|
+
return item.deserialize(raw);
|
|
1092
|
+
});
|
|
1093
|
+
}, items.length);
|
|
756
1094
|
}
|
|
757
1095
|
export function setBatch(items, scope) {
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
1096
|
+
measureOperation("batch:set", scope, () => {
|
|
1097
|
+
assertBatchScope(items.map(batchEntry => batchEntry.item), scope);
|
|
1098
|
+
if (scope === StorageScope.Memory) {
|
|
1099
|
+
items.forEach(({
|
|
1100
|
+
item,
|
|
1101
|
+
value
|
|
1102
|
+
}) => item.set(value));
|
|
1103
|
+
return;
|
|
1104
|
+
}
|
|
1105
|
+
if (scope === StorageScope.Secure) {
|
|
1106
|
+
const secureEntries = items.map(({
|
|
1107
|
+
item,
|
|
1108
|
+
value
|
|
1109
|
+
}) => ({
|
|
1110
|
+
item,
|
|
1111
|
+
value,
|
|
1112
|
+
internal: asInternal(item)
|
|
1113
|
+
}));
|
|
1114
|
+
const canUseSecureBatchPath = secureEntries.every(({
|
|
1115
|
+
internal
|
|
1116
|
+
}) => canUseSecureRawBatchPath(internal));
|
|
1117
|
+
if (!canUseSecureBatchPath) {
|
|
1118
|
+
items.forEach(({
|
|
1119
|
+
item,
|
|
1120
|
+
value
|
|
1121
|
+
}) => item.set(value));
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
flushSecureWrites();
|
|
1125
|
+
const groupedByAccessControl = new Map();
|
|
1126
|
+
secureEntries.forEach(({
|
|
1127
|
+
item,
|
|
1128
|
+
value,
|
|
1129
|
+
internal
|
|
1130
|
+
}) => {
|
|
1131
|
+
const accessControl = internal._secureAccessControl ?? AccessControl.WhenUnlocked;
|
|
1132
|
+
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
1133
|
+
const group = existingGroup ?? {
|
|
1134
|
+
keys: [],
|
|
1135
|
+
values: []
|
|
1136
|
+
};
|
|
1137
|
+
group.keys.push(item.key);
|
|
1138
|
+
group.values.push(item.serialize(value));
|
|
1139
|
+
if (!existingGroup) {
|
|
1140
|
+
groupedByAccessControl.set(accessControl, group);
|
|
1141
|
+
}
|
|
1142
|
+
});
|
|
1143
|
+
groupedByAccessControl.forEach((group, accessControl) => {
|
|
1144
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
1145
|
+
WebStorage.setBatch(group.keys, group.values, scope);
|
|
1146
|
+
group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
|
|
1147
|
+
});
|
|
1148
|
+
return;
|
|
1149
|
+
}
|
|
1150
|
+
const useRawBatchPath = items.every(({
|
|
1151
|
+
item
|
|
1152
|
+
}) => canUseRawBatchPath(asInternal(item)));
|
|
1153
|
+
if (!useRawBatchPath) {
|
|
1154
|
+
items.forEach(({
|
|
1155
|
+
item,
|
|
1156
|
+
value
|
|
1157
|
+
}) => item.set(value));
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
const keys = items.map(entry => entry.item.key);
|
|
1161
|
+
const values = items.map(entry => entry.item.serialize(entry.value));
|
|
1162
|
+
WebStorage.setBatch(keys, values, scope);
|
|
1163
|
+
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
1164
|
+
}, items.length);
|
|
783
1165
|
}
|
|
784
1166
|
export function removeBatch(items, scope) {
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
1167
|
+
measureOperation("batch:remove", scope, () => {
|
|
1168
|
+
assertBatchScope(items, scope);
|
|
1169
|
+
if (scope === StorageScope.Memory) {
|
|
1170
|
+
items.forEach(item => item.delete());
|
|
1171
|
+
return;
|
|
1172
|
+
}
|
|
1173
|
+
const keys = items.map(item => item.key);
|
|
1174
|
+
if (scope === StorageScope.Secure) {
|
|
1175
|
+
flushSecureWrites();
|
|
1176
|
+
}
|
|
1177
|
+
WebStorage.removeBatch(keys, scope);
|
|
1178
|
+
keys.forEach(key => cacheRawValue(scope, key, undefined));
|
|
1179
|
+
}, items.length);
|
|
796
1180
|
}
|
|
797
1181
|
export function registerMigration(version, migration) {
|
|
798
1182
|
if (!Number.isInteger(version) || version <= 0) {
|
|
@@ -804,93 +1188,133 @@ export function registerMigration(version, migration) {
|
|
|
804
1188
|
registeredMigrations.set(version, migration);
|
|
805
1189
|
}
|
|
806
1190
|
export function migrateToLatest(scope = StorageScope.Disk) {
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
1191
|
+
return measureOperation("migration:run", scope, () => {
|
|
1192
|
+
assertValidScope(scope);
|
|
1193
|
+
const currentVersion = readMigrationVersion(scope);
|
|
1194
|
+
const versions = Array.from(registeredMigrations.keys()).filter(version => version > currentVersion).sort((a, b) => a - b);
|
|
1195
|
+
let appliedVersion = currentVersion;
|
|
1196
|
+
const context = {
|
|
1197
|
+
scope,
|
|
1198
|
+
getRaw: key => getRawValue(key, scope),
|
|
1199
|
+
setRaw: (key, value) => setRawValue(key, value, scope),
|
|
1200
|
+
removeRaw: key => removeRawValue(key, scope)
|
|
1201
|
+
};
|
|
1202
|
+
versions.forEach(version => {
|
|
1203
|
+
const migration = registeredMigrations.get(version);
|
|
1204
|
+
if (!migration) {
|
|
1205
|
+
return;
|
|
1206
|
+
}
|
|
1207
|
+
migration(context);
|
|
1208
|
+
writeMigrationVersion(scope, version);
|
|
1209
|
+
appliedVersion = version;
|
|
1210
|
+
});
|
|
1211
|
+
return appliedVersion;
|
|
825
1212
|
});
|
|
826
|
-
return appliedVersion;
|
|
827
1213
|
}
|
|
828
1214
|
export function runTransaction(scope, transaction) {
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
const rollback = new Map();
|
|
834
|
-
const rememberRollback = key => {
|
|
835
|
-
if (rollback.has(key)) {
|
|
836
|
-
return;
|
|
837
|
-
}
|
|
838
|
-
rollback.set(key, getRawValue(key, scope));
|
|
839
|
-
};
|
|
840
|
-
const tx = {
|
|
841
|
-
scope,
|
|
842
|
-
getRaw: key => getRawValue(key, scope),
|
|
843
|
-
setRaw: (key, value) => {
|
|
844
|
-
rememberRollback(key);
|
|
845
|
-
setRawValue(key, value, scope);
|
|
846
|
-
},
|
|
847
|
-
removeRaw: key => {
|
|
848
|
-
rememberRollback(key);
|
|
849
|
-
removeRawValue(key, scope);
|
|
850
|
-
},
|
|
851
|
-
getItem: item => {
|
|
852
|
-
assertBatchScope([item], scope);
|
|
853
|
-
return item.get();
|
|
854
|
-
},
|
|
855
|
-
setItem: (item, value) => {
|
|
856
|
-
assertBatchScope([item], scope);
|
|
857
|
-
rememberRollback(item.key);
|
|
858
|
-
item.set(value);
|
|
859
|
-
},
|
|
860
|
-
removeItem: item => {
|
|
861
|
-
assertBatchScope([item], scope);
|
|
862
|
-
rememberRollback(item.key);
|
|
863
|
-
item.delete();
|
|
1215
|
+
return measureOperation("transaction:run", scope, () => {
|
|
1216
|
+
assertValidScope(scope);
|
|
1217
|
+
if (scope === StorageScope.Secure) {
|
|
1218
|
+
flushSecureWrites();
|
|
864
1219
|
}
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
1220
|
+
const rollback = new Map();
|
|
1221
|
+
const rememberRollback = key => {
|
|
1222
|
+
if (rollback.has(key)) {
|
|
1223
|
+
return;
|
|
1224
|
+
}
|
|
1225
|
+
rollback.set(key, getRawValue(key, scope));
|
|
1226
|
+
};
|
|
1227
|
+
const tx = {
|
|
1228
|
+
scope,
|
|
1229
|
+
getRaw: key => getRawValue(key, scope),
|
|
1230
|
+
setRaw: (key, value) => {
|
|
1231
|
+
rememberRollback(key);
|
|
1232
|
+
setRawValue(key, value, scope);
|
|
1233
|
+
},
|
|
1234
|
+
removeRaw: key => {
|
|
1235
|
+
rememberRollback(key);
|
|
871
1236
|
removeRawValue(key, scope);
|
|
1237
|
+
},
|
|
1238
|
+
getItem: item => {
|
|
1239
|
+
assertBatchScope([item], scope);
|
|
1240
|
+
return item.get();
|
|
1241
|
+
},
|
|
1242
|
+
setItem: (item, value) => {
|
|
1243
|
+
assertBatchScope([item], scope);
|
|
1244
|
+
rememberRollback(item.key);
|
|
1245
|
+
item.set(value);
|
|
1246
|
+
},
|
|
1247
|
+
removeItem: item => {
|
|
1248
|
+
assertBatchScope([item], scope);
|
|
1249
|
+
rememberRollback(item.key);
|
|
1250
|
+
item.delete();
|
|
1251
|
+
}
|
|
1252
|
+
};
|
|
1253
|
+
try {
|
|
1254
|
+
return transaction(tx);
|
|
1255
|
+
} catch (error) {
|
|
1256
|
+
const rollbackEntries = Array.from(rollback.entries()).reverse();
|
|
1257
|
+
if (scope === StorageScope.Memory) {
|
|
1258
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1259
|
+
if (previousValue === undefined) {
|
|
1260
|
+
removeRawValue(key, scope);
|
|
1261
|
+
} else {
|
|
1262
|
+
setRawValue(key, previousValue, scope);
|
|
1263
|
+
}
|
|
1264
|
+
});
|
|
872
1265
|
} else {
|
|
873
|
-
|
|
1266
|
+
const keysToSet = [];
|
|
1267
|
+
const valuesToSet = [];
|
|
1268
|
+
const keysToRemove = [];
|
|
1269
|
+
rollbackEntries.forEach(([key, previousValue]) => {
|
|
1270
|
+
if (previousValue === undefined) {
|
|
1271
|
+
keysToRemove.push(key);
|
|
1272
|
+
} else {
|
|
1273
|
+
keysToSet.push(key);
|
|
1274
|
+
valuesToSet.push(previousValue);
|
|
1275
|
+
}
|
|
1276
|
+
});
|
|
1277
|
+
if (scope === StorageScope.Secure) {
|
|
1278
|
+
flushSecureWrites();
|
|
1279
|
+
}
|
|
1280
|
+
if (keysToSet.length > 0) {
|
|
1281
|
+
WebStorage.setBatch(keysToSet, valuesToSet, scope);
|
|
1282
|
+
keysToSet.forEach((key, index) => cacheRawValue(scope, key, valuesToSet[index]));
|
|
1283
|
+
}
|
|
1284
|
+
if (keysToRemove.length > 0) {
|
|
1285
|
+
WebStorage.removeBatch(keysToRemove, scope);
|
|
1286
|
+
keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));
|
|
1287
|
+
}
|
|
874
1288
|
}
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
}
|
|
1289
|
+
throw error;
|
|
1290
|
+
}
|
|
1291
|
+
});
|
|
878
1292
|
}
|
|
879
1293
|
export function createSecureAuthStorage(config, options) {
|
|
880
1294
|
const ns = options?.namespace ?? "auth";
|
|
881
1295
|
const result = {};
|
|
882
|
-
for (const key of
|
|
1296
|
+
for (const key of typedKeys(config)) {
|
|
883
1297
|
const itemConfig = config[key];
|
|
1298
|
+
const expirationConfig = itemConfig.ttlMs !== undefined ? {
|
|
1299
|
+
ttlMs: itemConfig.ttlMs
|
|
1300
|
+
} : undefined;
|
|
884
1301
|
result[key] = createStorageItem({
|
|
885
1302
|
key,
|
|
886
1303
|
scope: StorageScope.Secure,
|
|
887
1304
|
defaultValue: "",
|
|
888
1305
|
namespace: ns,
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
1306
|
+
...(itemConfig.biometric !== undefined ? {
|
|
1307
|
+
biometric: itemConfig.biometric
|
|
1308
|
+
} : {}),
|
|
1309
|
+
...(itemConfig.biometricLevel !== undefined ? {
|
|
1310
|
+
biometricLevel: itemConfig.biometricLevel
|
|
1311
|
+
} : {}),
|
|
1312
|
+
...(itemConfig.accessControl !== undefined ? {
|
|
1313
|
+
accessControl: itemConfig.accessControl
|
|
1314
|
+
} : {}),
|
|
1315
|
+
...(expirationConfig !== undefined ? {
|
|
1316
|
+
expiration: expirationConfig
|
|
1317
|
+
} : {})
|
|
894
1318
|
});
|
|
895
1319
|
}
|
|
896
1320
|
return result;
|