react-native-nitro-storage 0.3.0 → 0.3.2
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 +594 -247
- package/android/CMakeLists.txt +2 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +102 -11
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +16 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +154 -34
- package/android/src/main/java/com/nitrostorage/NitroStoragePackage.kt +2 -2
- package/cpp/bindings/HybridStorage.cpp +176 -21
- package/cpp/bindings/HybridStorage.hpp +29 -2
- package/cpp/core/NativeStorageAdapter.hpp +16 -0
- package/ios/IOSStorageAdapterCpp.hpp +20 -0
- package/ios/IOSStorageAdapterCpp.mm +239 -32
- package/lib/commonjs/Storage.types.js +23 -1
- package/lib/commonjs/Storage.types.js.map +1 -1
- package/lib/commonjs/index.js +292 -75
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +473 -86
- 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/commonjs/storage-hooks.js +36 -0
- package/lib/commonjs/storage-hooks.js.map +1 -0
- package/lib/module/Storage.types.js +22 -0
- package/lib/module/Storage.types.js.map +1 -1
- package/lib/module/index.js +264 -75
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +445 -86
- 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/module/storage-hooks.js +30 -0
- package/lib/module/storage-hooks.js.map +1 -0
- package/lib/typescript/Storage.nitro.d.ts +12 -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 +33 -10
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +45 -10
- 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/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 +12 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +12 -0
- package/package.json +8 -3
- package/src/Storage.nitro.ts +13 -2
- package/src/Storage.types.ts +22 -0
- package/src/index.ts +382 -123
- package/src/index.web.ts +618 -134
- package/src/internal.ts +14 -4
- package/src/migration.ts +1 -1
- package/src/storage-hooks.ts +48 -0
package/lib/module/index.web.js
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
export { StorageScope } from "./Storage.types";
|
|
3
|
+
import { StorageScope, AccessControl } from "./Storage.types";
|
|
4
|
+
import { MIGRATION_VERSION_KEY, isStoredEnvelope, assertBatchScope, assertValidScope, serializeWithPrimitiveFastPath, deserializeWithPrimitiveFastPath, prefixKey, isNamespaced } from "./internal";
|
|
5
|
+
export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
7
6
|
export { migrateFromMMKV } from "./migration";
|
|
7
|
+
function asInternal(item) {
|
|
8
|
+
return item;
|
|
9
|
+
}
|
|
10
|
+
function isUpdater(valueOrFn) {
|
|
11
|
+
return typeof valueOrFn === "function";
|
|
12
|
+
}
|
|
13
|
+
function typedKeys(record) {
|
|
14
|
+
return Object.keys(record);
|
|
15
|
+
}
|
|
8
16
|
const registeredMigrations = new Map();
|
|
9
17
|
const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
|
|
10
18
|
Promise.resolve().then(task);
|
|
@@ -13,17 +21,71 @@ const memoryStore = new Map();
|
|
|
13
21
|
const memoryListeners = new Map();
|
|
14
22
|
const webScopeListeners = new Map([[StorageScope.Disk, new Map()], [StorageScope.Secure, new Map()]]);
|
|
15
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();
|
|
16
26
|
const pendingSecureWrites = new Map();
|
|
17
27
|
let secureFlushScheduled = false;
|
|
28
|
+
const SECURE_WEB_PREFIX = "__secure_";
|
|
29
|
+
const BIOMETRIC_WEB_PREFIX = "__bio_";
|
|
30
|
+
let hasWarnedAboutWebBiometricFallback = false;
|
|
18
31
|
function getBrowserStorage(scope) {
|
|
19
32
|
if (scope === StorageScope.Disk) {
|
|
20
33
|
return globalThis.localStorage;
|
|
21
34
|
}
|
|
22
35
|
if (scope === StorageScope.Secure) {
|
|
23
|
-
return globalThis.
|
|
36
|
+
return globalThis.localStorage;
|
|
24
37
|
}
|
|
25
38
|
return undefined;
|
|
26
39
|
}
|
|
40
|
+
function toSecureStorageKey(key) {
|
|
41
|
+
return `${SECURE_WEB_PREFIX}${key}`;
|
|
42
|
+
}
|
|
43
|
+
function fromSecureStorageKey(key) {
|
|
44
|
+
return key.slice(SECURE_WEB_PREFIX.length);
|
|
45
|
+
}
|
|
46
|
+
function toBiometricStorageKey(key) {
|
|
47
|
+
return `${BIOMETRIC_WEB_PREFIX}${key}`;
|
|
48
|
+
}
|
|
49
|
+
function fromBiometricStorageKey(key) {
|
|
50
|
+
return key.slice(BIOMETRIC_WEB_PREFIX.length);
|
|
51
|
+
}
|
|
52
|
+
function getWebScopeKeyIndex(scope) {
|
|
53
|
+
return webScopeKeyIndex.get(scope);
|
|
54
|
+
}
|
|
55
|
+
function hydrateWebScopeKeyIndex(scope) {
|
|
56
|
+
if (hydratedWebScopeKeyIndex.has(scope)) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const storage = getBrowserStorage(scope);
|
|
60
|
+
const keyIndex = getWebScopeKeyIndex(scope);
|
|
61
|
+
keyIndex.clear();
|
|
62
|
+
if (storage) {
|
|
63
|
+
for (let index = 0; index < storage.length; index += 1) {
|
|
64
|
+
const key = storage.key(index);
|
|
65
|
+
if (!key) {
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (scope === StorageScope.Disk) {
|
|
69
|
+
if (!key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
70
|
+
keyIndex.add(key);
|
|
71
|
+
}
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
if (key.startsWith(SECURE_WEB_PREFIX)) {
|
|
75
|
+
keyIndex.add(fromSecureStorageKey(key));
|
|
76
|
+
continue;
|
|
77
|
+
}
|
|
78
|
+
if (key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
79
|
+
keyIndex.add(fromBiometricStorageKey(key));
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
hydratedWebScopeKeyIndex.add(scope);
|
|
84
|
+
}
|
|
85
|
+
function ensureWebScopeKeyIndex(scope) {
|
|
86
|
+
hydrateWebScopeKeyIndex(scope);
|
|
87
|
+
return getWebScopeKeyIndex(scope);
|
|
88
|
+
}
|
|
27
89
|
function getScopedListeners(scope) {
|
|
28
90
|
return webScopeListeners.get(scope);
|
|
29
91
|
}
|
|
@@ -122,26 +184,65 @@ const WebStorage = {
|
|
|
122
184
|
dispose: () => {},
|
|
123
185
|
set: (key, value, scope) => {
|
|
124
186
|
const storage = getBrowserStorage(scope);
|
|
125
|
-
storage
|
|
187
|
+
if (!storage) {
|
|
188
|
+
return;
|
|
189
|
+
}
|
|
190
|
+
const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
191
|
+
storage.setItem(storageKey, value);
|
|
126
192
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
193
|
+
ensureWebScopeKeyIndex(scope).add(key);
|
|
127
194
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
128
195
|
}
|
|
129
196
|
},
|
|
130
197
|
get: (key, scope) => {
|
|
131
198
|
const storage = getBrowserStorage(scope);
|
|
132
|
-
|
|
199
|
+
const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
200
|
+
return storage?.getItem(storageKey) ?? undefined;
|
|
133
201
|
},
|
|
134
202
|
remove: (key, scope) => {
|
|
135
203
|
const storage = getBrowserStorage(scope);
|
|
136
|
-
storage
|
|
204
|
+
if (!storage) {
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
207
|
+
if (scope === StorageScope.Secure) {
|
|
208
|
+
storage.removeItem(toSecureStorageKey(key));
|
|
209
|
+
storage.removeItem(toBiometricStorageKey(key));
|
|
210
|
+
} else {
|
|
211
|
+
storage.removeItem(key);
|
|
212
|
+
}
|
|
137
213
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
214
|
+
ensureWebScopeKeyIndex(scope).delete(key);
|
|
138
215
|
notifyKeyListeners(getScopedListeners(scope), key);
|
|
139
216
|
}
|
|
140
217
|
},
|
|
141
218
|
clear: scope => {
|
|
142
219
|
const storage = getBrowserStorage(scope);
|
|
143
|
-
storage
|
|
220
|
+
if (!storage) {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
if (scope === StorageScope.Secure) {
|
|
224
|
+
const keysToRemove = [];
|
|
225
|
+
for (let i = 0; i < storage.length; i++) {
|
|
226
|
+
const key = storage.key(i);
|
|
227
|
+
if (key?.startsWith(SECURE_WEB_PREFIX) || key?.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
228
|
+
keysToRemove.push(key);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
keysToRemove.forEach(key => storage.removeItem(key));
|
|
232
|
+
} else if (scope === StorageScope.Disk) {
|
|
233
|
+
const keysToRemove = [];
|
|
234
|
+
for (let i = 0; i < storage.length; i++) {
|
|
235
|
+
const key = storage.key(i);
|
|
236
|
+
if (key && !key.startsWith(SECURE_WEB_PREFIX) && !key.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
237
|
+
keysToRemove.push(key);
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
keysToRemove.forEach(key => storage.removeItem(key));
|
|
241
|
+
} else {
|
|
242
|
+
storage.clear();
|
|
243
|
+
}
|
|
144
244
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
245
|
+
ensureWebScopeKeyIndex(scope).clear();
|
|
145
246
|
notifyAllListeners(getScopedListeners(scope));
|
|
146
247
|
}
|
|
147
248
|
},
|
|
@@ -151,32 +252,129 @@ const WebStorage = {
|
|
|
151
252
|
return;
|
|
152
253
|
}
|
|
153
254
|
keys.forEach((key, index) => {
|
|
154
|
-
|
|
255
|
+
const value = values[index];
|
|
256
|
+
if (value === undefined) {
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
260
|
+
storage.setItem(storageKey, value);
|
|
155
261
|
});
|
|
156
262
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
263
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
264
|
+
keys.forEach(key => keyIndex.add(key));
|
|
157
265
|
const listeners = getScopedListeners(scope);
|
|
158
266
|
keys.forEach(key => notifyKeyListeners(listeners, key));
|
|
159
267
|
}
|
|
160
268
|
},
|
|
161
269
|
getBatch: (keys, scope) => {
|
|
162
270
|
const storage = getBrowserStorage(scope);
|
|
163
|
-
return keys.map(key =>
|
|
271
|
+
return keys.map(key => {
|
|
272
|
+
const storageKey = scope === StorageScope.Secure ? toSecureStorageKey(key) : key;
|
|
273
|
+
return storage?.getItem(storageKey) ?? undefined;
|
|
274
|
+
});
|
|
164
275
|
},
|
|
165
276
|
removeBatch: (keys, scope) => {
|
|
166
277
|
const storage = getBrowserStorage(scope);
|
|
167
278
|
if (!storage) {
|
|
168
279
|
return;
|
|
169
280
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
281
|
+
if (scope === StorageScope.Secure) {
|
|
282
|
+
keys.forEach(key => {
|
|
283
|
+
storage.removeItem(toSecureStorageKey(key));
|
|
284
|
+
storage.removeItem(toBiometricStorageKey(key));
|
|
285
|
+
});
|
|
286
|
+
} else {
|
|
287
|
+
keys.forEach(key => {
|
|
288
|
+
storage.removeItem(key);
|
|
289
|
+
});
|
|
290
|
+
}
|
|
173
291
|
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
292
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
293
|
+
keys.forEach(key => keyIndex.delete(key));
|
|
174
294
|
const listeners = getScopedListeners(scope);
|
|
175
295
|
keys.forEach(key => notifyKeyListeners(listeners, key));
|
|
176
296
|
}
|
|
177
297
|
},
|
|
298
|
+
removeByPrefix: (prefix, scope) => {
|
|
299
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
300
|
+
return;
|
|
301
|
+
}
|
|
302
|
+
const keyIndex = ensureWebScopeKeyIndex(scope);
|
|
303
|
+
const keys = Array.from(keyIndex).filter(key => key.startsWith(prefix));
|
|
304
|
+
if (keys.length === 0) {
|
|
305
|
+
return;
|
|
306
|
+
}
|
|
307
|
+
WebStorage.removeBatch(keys, scope);
|
|
308
|
+
},
|
|
178
309
|
addOnChange: (_scope, _callback) => {
|
|
179
310
|
return () => {};
|
|
311
|
+
},
|
|
312
|
+
has: (key, scope) => {
|
|
313
|
+
const storage = getBrowserStorage(scope);
|
|
314
|
+
if (scope === StorageScope.Secure) {
|
|
315
|
+
return storage?.getItem(toSecureStorageKey(key)) !== null || storage?.getItem(toBiometricStorageKey(key)) !== null;
|
|
316
|
+
}
|
|
317
|
+
return storage?.getItem(key) !== null;
|
|
318
|
+
},
|
|
319
|
+
getAllKeys: scope => {
|
|
320
|
+
if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
|
|
321
|
+
return [];
|
|
322
|
+
}
|
|
323
|
+
return Array.from(ensureWebScopeKeyIndex(scope));
|
|
324
|
+
},
|
|
325
|
+
size: scope => {
|
|
326
|
+
if (scope === StorageScope.Disk || scope === StorageScope.Secure) {
|
|
327
|
+
return ensureWebScopeKeyIndex(scope).size;
|
|
328
|
+
}
|
|
329
|
+
return 0;
|
|
330
|
+
},
|
|
331
|
+
setSecureAccessControl: () => {},
|
|
332
|
+
setSecureWritesAsync: _enabled => {},
|
|
333
|
+
setKeychainAccessGroup: () => {},
|
|
334
|
+
setSecureBiometric: (key, value) => {
|
|
335
|
+
if (typeof __DEV__ !== "undefined" && __DEV__ && !hasWarnedAboutWebBiometricFallback) {
|
|
336
|
+
hasWarnedAboutWebBiometricFallback = true;
|
|
337
|
+
console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
|
|
338
|
+
}
|
|
339
|
+
globalThis.localStorage?.setItem(toBiometricStorageKey(key), value);
|
|
340
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
|
|
341
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
342
|
+
},
|
|
343
|
+
getSecureBiometric: key => {
|
|
344
|
+
return globalThis.localStorage?.getItem(toBiometricStorageKey(key)) ?? undefined;
|
|
345
|
+
},
|
|
346
|
+
deleteSecureBiometric: key => {
|
|
347
|
+
const storage = globalThis.localStorage;
|
|
348
|
+
storage?.removeItem(toBiometricStorageKey(key));
|
|
349
|
+
if (storage?.getItem(toSecureStorageKey(key)) === null) {
|
|
350
|
+
ensureWebScopeKeyIndex(StorageScope.Secure).delete(key);
|
|
351
|
+
}
|
|
352
|
+
notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
|
|
353
|
+
},
|
|
354
|
+
hasSecureBiometric: key => {
|
|
355
|
+
return globalThis.localStorage?.getItem(toBiometricStorageKey(key)) !== null;
|
|
356
|
+
},
|
|
357
|
+
clearSecureBiometric: () => {
|
|
358
|
+
const storage = globalThis.localStorage;
|
|
359
|
+
if (!storage) return;
|
|
360
|
+
const keysToNotify = [];
|
|
361
|
+
const toRemove = [];
|
|
362
|
+
for (let i = 0; i < storage.length; i++) {
|
|
363
|
+
const k = storage.key(i);
|
|
364
|
+
if (k?.startsWith(BIOMETRIC_WEB_PREFIX)) {
|
|
365
|
+
toRemove.push(k);
|
|
366
|
+
keysToNotify.push(fromBiometricStorageKey(k));
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
toRemove.forEach(k => storage.removeItem(k));
|
|
370
|
+
const keyIndex = ensureWebScopeKeyIndex(StorageScope.Secure);
|
|
371
|
+
keysToNotify.forEach(key => {
|
|
372
|
+
if (storage.getItem(toSecureStorageKey(key)) === null) {
|
|
373
|
+
keyIndex.delete(key);
|
|
374
|
+
}
|
|
375
|
+
});
|
|
376
|
+
const listeners = getScopedListeners(StorageScope.Secure);
|
|
377
|
+
keysToNotify.forEach(key => notifyKeyListeners(listeners, key));
|
|
180
378
|
}
|
|
181
379
|
};
|
|
182
380
|
function getRawValue(key, scope) {
|
|
@@ -247,10 +445,71 @@ export const storage = {
|
|
|
247
445
|
storage.clear(StorageScope.Memory);
|
|
248
446
|
storage.clear(StorageScope.Disk);
|
|
249
447
|
storage.clear(StorageScope.Secure);
|
|
250
|
-
}
|
|
448
|
+
},
|
|
449
|
+
clearNamespace: (namespace, scope) => {
|
|
450
|
+
assertValidScope(scope);
|
|
451
|
+
if (scope === StorageScope.Memory) {
|
|
452
|
+
for (const key of memoryStore.keys()) {
|
|
453
|
+
if (isNamespaced(key, namespace)) {
|
|
454
|
+
memoryStore.delete(key);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
notifyAllListeners(memoryListeners);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const keyPrefix = prefixKey(namespace, "");
|
|
461
|
+
if (scope === StorageScope.Secure) {
|
|
462
|
+
flushSecureWrites();
|
|
463
|
+
}
|
|
464
|
+
clearScopeRawCache(scope);
|
|
465
|
+
WebStorage.removeByPrefix(keyPrefix, scope);
|
|
466
|
+
},
|
|
467
|
+
clearBiometric: () => {
|
|
468
|
+
WebStorage.clearSecureBiometric();
|
|
469
|
+
},
|
|
470
|
+
has: (key, scope) => {
|
|
471
|
+
assertValidScope(scope);
|
|
472
|
+
if (scope === StorageScope.Memory) return memoryStore.has(key);
|
|
473
|
+
return WebStorage.has(key, scope);
|
|
474
|
+
},
|
|
475
|
+
getAllKeys: scope => {
|
|
476
|
+
assertValidScope(scope);
|
|
477
|
+
if (scope === StorageScope.Memory) return Array.from(memoryStore.keys());
|
|
478
|
+
return WebStorage.getAllKeys(scope);
|
|
479
|
+
},
|
|
480
|
+
getAll: scope => {
|
|
481
|
+
assertValidScope(scope);
|
|
482
|
+
const result = {};
|
|
483
|
+
if (scope === StorageScope.Memory) {
|
|
484
|
+
memoryStore.forEach((value, key) => {
|
|
485
|
+
if (typeof value === "string") result[key] = value;
|
|
486
|
+
});
|
|
487
|
+
return result;
|
|
488
|
+
}
|
|
489
|
+
const keys = WebStorage.getAllKeys(scope);
|
|
490
|
+
keys.forEach(key => {
|
|
491
|
+
const val = WebStorage.get(key, scope);
|
|
492
|
+
if (val !== undefined) result[key] = val;
|
|
493
|
+
});
|
|
494
|
+
return result;
|
|
495
|
+
},
|
|
496
|
+
size: scope => {
|
|
497
|
+
assertValidScope(scope);
|
|
498
|
+
if (scope === StorageScope.Memory) return memoryStore.size;
|
|
499
|
+
return WebStorage.size(scope);
|
|
500
|
+
},
|
|
501
|
+
setAccessControl: _level => {},
|
|
502
|
+
setSecureWritesAsync: _enabled => {},
|
|
503
|
+
flushSecureWrites: () => {
|
|
504
|
+
flushSecureWrites();
|
|
505
|
+
},
|
|
506
|
+
setKeychainAccessGroup: _group => {}
|
|
251
507
|
};
|
|
252
508
|
function canUseRawBatchPath(item) {
|
|
253
|
-
return item._hasExpiration === false && item._hasValidation === false;
|
|
509
|
+
return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
|
|
510
|
+
}
|
|
511
|
+
function canUseSecureRawBatchPath(item) {
|
|
512
|
+
return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true;
|
|
254
513
|
}
|
|
255
514
|
function defaultSerialize(value) {
|
|
256
515
|
return serializeWithPrimitiveFastPath(value);
|
|
@@ -259,16 +518,21 @@ function defaultDeserialize(value) {
|
|
|
259
518
|
return deserializeWithPrimitiveFastPath(value);
|
|
260
519
|
}
|
|
261
520
|
export function createStorageItem(config) {
|
|
521
|
+
const storageKey = prefixKey(config.namespace, config.key);
|
|
262
522
|
const serialize = config.serialize ?? defaultSerialize;
|
|
263
523
|
const deserialize = config.deserialize ?? defaultDeserialize;
|
|
264
524
|
const isMemory = config.scope === StorageScope.Memory;
|
|
525
|
+
const isBiometric = config.biometric === true && config.scope === StorageScope.Secure;
|
|
526
|
+
const secureAccessControl = config.accessControl;
|
|
265
527
|
const validate = config.validate;
|
|
266
528
|
const onValidationError = config.onValidationError;
|
|
267
529
|
const expiration = config.expiration;
|
|
530
|
+
const onExpired = config.onExpired;
|
|
268
531
|
const expirationTtlMs = expiration?.ttlMs;
|
|
269
532
|
const memoryExpiration = expiration && isMemory ? new Map() : null;
|
|
270
533
|
const readCache = !isMemory && config.readCache === true;
|
|
271
|
-
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true;
|
|
534
|
+
const coalesceSecureWrites = config.scope === StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric && secureAccessControl === undefined;
|
|
535
|
+
const defaultValue = config.defaultValue;
|
|
272
536
|
const nonMemoryScope = config.scope === StorageScope.Disk ? StorageScope.Disk : config.scope === StorageScope.Secure ? StorageScope.Secure : null;
|
|
273
537
|
if (expiration && expiration.ttlMs <= 0) {
|
|
274
538
|
throw new Error("expiration.ttlMs must be greater than 0.");
|
|
@@ -278,10 +542,12 @@ export function createStorageItem(config) {
|
|
|
278
542
|
let lastRaw = undefined;
|
|
279
543
|
let lastValue;
|
|
280
544
|
let hasLastValue = false;
|
|
545
|
+
let lastExpiresAt = undefined;
|
|
281
546
|
const invalidateParsedCache = () => {
|
|
282
547
|
lastRaw = undefined;
|
|
283
548
|
lastValue = undefined;
|
|
284
549
|
hasLastValue = false;
|
|
550
|
+
lastExpiresAt = undefined;
|
|
285
551
|
};
|
|
286
552
|
const ensureSubscription = () => {
|
|
287
553
|
if (unsubscribe) {
|
|
@@ -292,65 +558,77 @@ export function createStorageItem(config) {
|
|
|
292
558
|
listeners.forEach(callback => callback());
|
|
293
559
|
};
|
|
294
560
|
if (isMemory) {
|
|
295
|
-
unsubscribe = addKeyListener(memoryListeners,
|
|
561
|
+
unsubscribe = addKeyListener(memoryListeners, storageKey, listener);
|
|
296
562
|
return;
|
|
297
563
|
}
|
|
298
|
-
unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope),
|
|
564
|
+
unsubscribe = addKeyListener(getScopedListeners(nonMemoryScope), storageKey, listener);
|
|
299
565
|
};
|
|
300
566
|
const readStoredRaw = () => {
|
|
301
567
|
if (isMemory) {
|
|
302
568
|
if (memoryExpiration) {
|
|
303
|
-
const expiresAt = memoryExpiration.get(
|
|
569
|
+
const expiresAt = memoryExpiration.get(storageKey);
|
|
304
570
|
if (expiresAt !== undefined && expiresAt <= Date.now()) {
|
|
305
|
-
memoryExpiration.delete(
|
|
306
|
-
memoryStore.delete(
|
|
307
|
-
notifyKeyListeners(memoryListeners,
|
|
571
|
+
memoryExpiration.delete(storageKey);
|
|
572
|
+
memoryStore.delete(storageKey);
|
|
573
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
574
|
+
onExpired?.(storageKey);
|
|
308
575
|
return undefined;
|
|
309
576
|
}
|
|
310
577
|
}
|
|
311
|
-
return memoryStore.get(
|
|
578
|
+
return memoryStore.get(storageKey);
|
|
312
579
|
}
|
|
313
|
-
if (nonMemoryScope === StorageScope.Secure && hasPendingSecureWrite(
|
|
314
|
-
return readPendingSecureWrite(
|
|
580
|
+
if (nonMemoryScope === StorageScope.Secure && !isBiometric && hasPendingSecureWrite(storageKey)) {
|
|
581
|
+
return readPendingSecureWrite(storageKey);
|
|
315
582
|
}
|
|
316
583
|
if (readCache) {
|
|
317
|
-
if (hasCachedRawValue(nonMemoryScope,
|
|
318
|
-
return readCachedRawValue(nonMemoryScope,
|
|
584
|
+
if (hasCachedRawValue(nonMemoryScope, storageKey)) {
|
|
585
|
+
return readCachedRawValue(nonMemoryScope, storageKey);
|
|
319
586
|
}
|
|
320
587
|
}
|
|
321
|
-
|
|
322
|
-
|
|
588
|
+
if (isBiometric) {
|
|
589
|
+
return WebStorage.getSecureBiometric(storageKey);
|
|
590
|
+
}
|
|
591
|
+
const raw = WebStorage.get(storageKey, config.scope);
|
|
592
|
+
cacheRawValue(nonMemoryScope, storageKey, raw);
|
|
323
593
|
return raw;
|
|
324
594
|
};
|
|
325
595
|
const writeStoredRaw = rawValue => {
|
|
326
|
-
|
|
596
|
+
if (isBiometric) {
|
|
597
|
+
WebStorage.setSecureBiometric(storageKey, rawValue);
|
|
598
|
+
return;
|
|
599
|
+
}
|
|
600
|
+
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
327
601
|
if (coalesceSecureWrites) {
|
|
328
|
-
scheduleSecureWrite(
|
|
602
|
+
scheduleSecureWrite(storageKey, rawValue);
|
|
329
603
|
return;
|
|
330
604
|
}
|
|
331
605
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
332
|
-
clearPendingSecureWrite(
|
|
606
|
+
clearPendingSecureWrite(storageKey);
|
|
333
607
|
}
|
|
334
|
-
WebStorage.set(
|
|
608
|
+
WebStorage.set(storageKey, rawValue, config.scope);
|
|
335
609
|
};
|
|
336
610
|
const removeStoredRaw = () => {
|
|
337
|
-
|
|
611
|
+
if (isBiometric) {
|
|
612
|
+
WebStorage.deleteSecureBiometric(storageKey);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
338
616
|
if (coalesceSecureWrites) {
|
|
339
|
-
scheduleSecureWrite(
|
|
617
|
+
scheduleSecureWrite(storageKey, undefined);
|
|
340
618
|
return;
|
|
341
619
|
}
|
|
342
620
|
if (nonMemoryScope === StorageScope.Secure) {
|
|
343
|
-
clearPendingSecureWrite(
|
|
621
|
+
clearPendingSecureWrite(storageKey);
|
|
344
622
|
}
|
|
345
|
-
WebStorage.remove(
|
|
623
|
+
WebStorage.remove(storageKey, config.scope);
|
|
346
624
|
};
|
|
347
625
|
const writeValueWithoutValidation = value => {
|
|
348
626
|
if (isMemory) {
|
|
349
627
|
if (memoryExpiration) {
|
|
350
|
-
memoryExpiration.set(
|
|
628
|
+
memoryExpiration.set(storageKey, Date.now() + (expirationTtlMs ?? 0));
|
|
351
629
|
}
|
|
352
|
-
memoryStore.set(
|
|
353
|
-
notifyKeyListeners(memoryListeners,
|
|
630
|
+
memoryStore.set(storageKey, value);
|
|
631
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
354
632
|
return;
|
|
355
633
|
}
|
|
356
634
|
const serialized = serialize(value);
|
|
@@ -369,7 +647,7 @@ export function createStorageItem(config) {
|
|
|
369
647
|
if (onValidationError) {
|
|
370
648
|
return onValidationError(invalidValue);
|
|
371
649
|
}
|
|
372
|
-
return
|
|
650
|
+
return defaultValue;
|
|
373
651
|
};
|
|
374
652
|
const ensureValidatedValue = (candidate, hadStoredValue) => {
|
|
375
653
|
if (!validate || validate(candidate)) {
|
|
@@ -377,7 +655,7 @@ export function createStorageItem(config) {
|
|
|
377
655
|
}
|
|
378
656
|
const resolved = resolveInvalidValue(candidate);
|
|
379
657
|
if (validate && !validate(resolved)) {
|
|
380
|
-
return
|
|
658
|
+
return defaultValue;
|
|
381
659
|
}
|
|
382
660
|
if (hadStoredValue) {
|
|
383
661
|
writeValueWithoutValidation(resolved);
|
|
@@ -386,30 +664,53 @@ export function createStorageItem(config) {
|
|
|
386
664
|
};
|
|
387
665
|
const get = () => {
|
|
388
666
|
const raw = readStoredRaw();
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
667
|
+
if (!memoryExpiration && raw === lastRaw && hasLastValue) {
|
|
668
|
+
if (!expiration || lastExpiresAt === null) {
|
|
669
|
+
return lastValue;
|
|
670
|
+
}
|
|
671
|
+
if (typeof lastExpiresAt === "number") {
|
|
672
|
+
if (lastExpiresAt > Date.now()) {
|
|
673
|
+
return lastValue;
|
|
674
|
+
}
|
|
675
|
+
removeStoredRaw();
|
|
676
|
+
invalidateParsedCache();
|
|
677
|
+
onExpired?.(storageKey);
|
|
678
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
679
|
+
hasLastValue = true;
|
|
680
|
+
return lastValue;
|
|
681
|
+
}
|
|
392
682
|
}
|
|
393
683
|
lastRaw = raw;
|
|
394
684
|
if (raw === undefined) {
|
|
395
|
-
|
|
685
|
+
lastExpiresAt = undefined;
|
|
686
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
396
687
|
hasLastValue = true;
|
|
397
688
|
return lastValue;
|
|
398
689
|
}
|
|
399
690
|
if (isMemory) {
|
|
691
|
+
lastExpiresAt = undefined;
|
|
400
692
|
lastValue = ensureValidatedValue(raw, true);
|
|
401
693
|
hasLastValue = true;
|
|
402
694
|
return lastValue;
|
|
403
695
|
}
|
|
696
|
+
if (typeof raw !== "string") {
|
|
697
|
+
lastExpiresAt = undefined;
|
|
698
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
699
|
+
hasLastValue = true;
|
|
700
|
+
return lastValue;
|
|
701
|
+
}
|
|
404
702
|
let deserializableRaw = raw;
|
|
405
703
|
if (expiration) {
|
|
704
|
+
let envelopeExpiresAt = null;
|
|
406
705
|
try {
|
|
407
706
|
const parsed = JSON.parse(raw);
|
|
408
707
|
if (isStoredEnvelope(parsed)) {
|
|
708
|
+
envelopeExpiresAt = parsed.expiresAt;
|
|
409
709
|
if (parsed.expiresAt <= Date.now()) {
|
|
410
710
|
removeStoredRaw();
|
|
411
711
|
invalidateParsedCache();
|
|
412
|
-
|
|
712
|
+
onExpired?.(storageKey);
|
|
713
|
+
lastValue = ensureValidatedValue(defaultValue, false);
|
|
413
714
|
hasLastValue = true;
|
|
414
715
|
return lastValue;
|
|
415
716
|
}
|
|
@@ -418,17 +719,19 @@ export function createStorageItem(config) {
|
|
|
418
719
|
} catch {
|
|
419
720
|
// Keep backward compatibility with legacy raw values.
|
|
420
721
|
}
|
|
722
|
+
lastExpiresAt = envelopeExpiresAt;
|
|
723
|
+
} else {
|
|
724
|
+
lastExpiresAt = undefined;
|
|
421
725
|
}
|
|
422
726
|
lastValue = ensureValidatedValue(deserialize(deserializableRaw), true);
|
|
423
727
|
hasLastValue = true;
|
|
424
728
|
return lastValue;
|
|
425
729
|
};
|
|
426
730
|
const set = valueOrFn => {
|
|
427
|
-
const
|
|
428
|
-
const newValue = typeof valueOrFn === "function" ? valueOrFn(currentValue) : valueOrFn;
|
|
731
|
+
const newValue = isUpdater(valueOrFn) ? valueOrFn(get()) : valueOrFn;
|
|
429
732
|
invalidateParsedCache();
|
|
430
733
|
if (validate && !validate(newValue)) {
|
|
431
|
-
throw new Error(`Validation failed for key "${
|
|
734
|
+
throw new Error(`Validation failed for key "${storageKey}" in scope "${StorageScope[config.scope]}".`);
|
|
432
735
|
}
|
|
433
736
|
writeValueWithoutValidation(newValue);
|
|
434
737
|
};
|
|
@@ -436,14 +739,19 @@ export function createStorageItem(config) {
|
|
|
436
739
|
invalidateParsedCache();
|
|
437
740
|
if (isMemory) {
|
|
438
741
|
if (memoryExpiration) {
|
|
439
|
-
memoryExpiration.delete(
|
|
742
|
+
memoryExpiration.delete(storageKey);
|
|
440
743
|
}
|
|
441
|
-
memoryStore.delete(
|
|
442
|
-
notifyKeyListeners(memoryListeners,
|
|
744
|
+
memoryStore.delete(storageKey);
|
|
745
|
+
notifyKeyListeners(memoryListeners, storageKey);
|
|
443
746
|
return;
|
|
444
747
|
}
|
|
445
748
|
removeStoredRaw();
|
|
446
749
|
};
|
|
750
|
+
const hasItem = () => {
|
|
751
|
+
if (isMemory) return memoryStore.has(storageKey);
|
|
752
|
+
if (isBiometric) return WebStorage.hasSecureBiometric(storageKey);
|
|
753
|
+
return WebStorage.has(storageKey, config.scope);
|
|
754
|
+
};
|
|
447
755
|
const subscribe = callback => {
|
|
448
756
|
ensureSubscription();
|
|
449
757
|
listeners.add(callback);
|
|
@@ -459,6 +767,7 @@ export function createStorageItem(config) {
|
|
|
459
767
|
get,
|
|
460
768
|
set,
|
|
461
769
|
delete: deleteItem,
|
|
770
|
+
has: hasItem,
|
|
462
771
|
subscribe,
|
|
463
772
|
serialize,
|
|
464
773
|
deserialize,
|
|
@@ -469,43 +778,22 @@ export function createStorageItem(config) {
|
|
|
469
778
|
_hasValidation: validate !== undefined,
|
|
470
779
|
_hasExpiration: expiration !== undefined,
|
|
471
780
|
_readCacheEnabled: readCache,
|
|
781
|
+
_isBiometric: isBiometric,
|
|
782
|
+
...(secureAccessControl !== undefined ? {
|
|
783
|
+
_secureAccessControl: secureAccessControl
|
|
784
|
+
} : {}),
|
|
472
785
|
scope: config.scope,
|
|
473
|
-
key:
|
|
786
|
+
key: storageKey
|
|
474
787
|
};
|
|
475
788
|
return storageItem;
|
|
476
789
|
}
|
|
477
|
-
export
|
|
478
|
-
const value = useSyncExternalStore(item.subscribe, item.get, item.get);
|
|
479
|
-
return [value, item.set];
|
|
480
|
-
}
|
|
481
|
-
export function useStorageSelector(item, selector, isEqual = Object.is) {
|
|
482
|
-
const selectedRef = useRef({
|
|
483
|
-
hasValue: false
|
|
484
|
-
});
|
|
485
|
-
const getSelectedSnapshot = () => {
|
|
486
|
-
const nextSelected = selector(item.get());
|
|
487
|
-
const current = selectedRef.current;
|
|
488
|
-
if (current.hasValue && isEqual(current.value, nextSelected)) {
|
|
489
|
-
return current.value;
|
|
490
|
-
}
|
|
491
|
-
selectedRef.current = {
|
|
492
|
-
hasValue: true,
|
|
493
|
-
value: nextSelected
|
|
494
|
-
};
|
|
495
|
-
return nextSelected;
|
|
496
|
-
};
|
|
497
|
-
const selectedValue = useSyncExternalStore(item.subscribe, getSelectedSnapshot, getSelectedSnapshot);
|
|
498
|
-
return [selectedValue, item.set];
|
|
499
|
-
}
|
|
500
|
-
export function useSetStorage(item) {
|
|
501
|
-
return item.set;
|
|
502
|
-
}
|
|
790
|
+
export { useStorage, useStorageSelector, useSetStorage } from "./storage-hooks";
|
|
503
791
|
export function getBatch(items, scope) {
|
|
504
792
|
assertBatchScope(items, scope);
|
|
505
793
|
if (scope === StorageScope.Memory) {
|
|
506
794
|
return items.map(item => item.get());
|
|
507
795
|
}
|
|
508
|
-
const useRawBatchPath = items.every(item => canUseRawBatchPath(item));
|
|
796
|
+
const useRawBatchPath = items.every(item => scope === StorageScope.Secure ? canUseSecureRawBatchPath(item) : canUseRawBatchPath(item));
|
|
509
797
|
if (!useRawBatchPath) {
|
|
510
798
|
return items.map(item => item.get());
|
|
511
799
|
}
|
|
@@ -534,6 +822,9 @@ export function getBatch(items, scope) {
|
|
|
534
822
|
fetchedValues.forEach((value, index) => {
|
|
535
823
|
const key = keysToFetch[index];
|
|
536
824
|
const targetIndex = keyIndexes[index];
|
|
825
|
+
if (key === undefined || targetIndex === undefined) {
|
|
826
|
+
return;
|
|
827
|
+
}
|
|
537
828
|
rawValues[targetIndex] = value;
|
|
538
829
|
cacheRawValue(scope, key, value);
|
|
539
830
|
});
|
|
@@ -555,9 +846,54 @@ export function setBatch(items, scope) {
|
|
|
555
846
|
}) => item.set(value));
|
|
556
847
|
return;
|
|
557
848
|
}
|
|
849
|
+
if (scope === StorageScope.Secure) {
|
|
850
|
+
const secureEntries = items.map(({
|
|
851
|
+
item,
|
|
852
|
+
value
|
|
853
|
+
}) => ({
|
|
854
|
+
item,
|
|
855
|
+
value,
|
|
856
|
+
internal: asInternal(item)
|
|
857
|
+
}));
|
|
858
|
+
const canUseSecureBatchPath = secureEntries.every(({
|
|
859
|
+
internal
|
|
860
|
+
}) => canUseSecureRawBatchPath(internal));
|
|
861
|
+
if (!canUseSecureBatchPath) {
|
|
862
|
+
items.forEach(({
|
|
863
|
+
item,
|
|
864
|
+
value
|
|
865
|
+
}) => item.set(value));
|
|
866
|
+
return;
|
|
867
|
+
}
|
|
868
|
+
flushSecureWrites();
|
|
869
|
+
const groupedByAccessControl = new Map();
|
|
870
|
+
secureEntries.forEach(({
|
|
871
|
+
item,
|
|
872
|
+
value,
|
|
873
|
+
internal
|
|
874
|
+
}) => {
|
|
875
|
+
const accessControl = internal._secureAccessControl ?? AccessControl.WhenUnlocked;
|
|
876
|
+
const existingGroup = groupedByAccessControl.get(accessControl);
|
|
877
|
+
const group = existingGroup ?? {
|
|
878
|
+
keys: [],
|
|
879
|
+
values: []
|
|
880
|
+
};
|
|
881
|
+
group.keys.push(item.key);
|
|
882
|
+
group.values.push(item.serialize(value));
|
|
883
|
+
if (!existingGroup) {
|
|
884
|
+
groupedByAccessControl.set(accessControl, group);
|
|
885
|
+
}
|
|
886
|
+
});
|
|
887
|
+
groupedByAccessControl.forEach((group, accessControl) => {
|
|
888
|
+
WebStorage.setSecureAccessControl(accessControl);
|
|
889
|
+
WebStorage.setBatch(group.keys, group.values, scope);
|
|
890
|
+
group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
|
|
891
|
+
});
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
558
894
|
const useRawBatchPath = items.every(({
|
|
559
895
|
item
|
|
560
|
-
}) => canUseRawBatchPath(item));
|
|
896
|
+
}) => canUseRawBatchPath(asInternal(item)));
|
|
561
897
|
if (!useRawBatchPath) {
|
|
562
898
|
items.forEach(({
|
|
563
899
|
item,
|
|
@@ -567,9 +903,6 @@ export function setBatch(items, scope) {
|
|
|
567
903
|
}
|
|
568
904
|
const keys = items.map(entry => entry.item.key);
|
|
569
905
|
const values = items.map(entry => entry.item.serialize(entry.value));
|
|
570
|
-
if (scope === StorageScope.Secure) {
|
|
571
|
-
flushSecureWrites();
|
|
572
|
-
}
|
|
573
906
|
WebStorage.setBatch(keys, values, scope);
|
|
574
907
|
keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
|
|
575
908
|
}
|
|
@@ -668,4 +1001,30 @@ export function runTransaction(scope, transaction) {
|
|
|
668
1001
|
throw error;
|
|
669
1002
|
}
|
|
670
1003
|
}
|
|
1004
|
+
export function createSecureAuthStorage(config, options) {
|
|
1005
|
+
const ns = options?.namespace ?? "auth";
|
|
1006
|
+
const result = {};
|
|
1007
|
+
for (const key of typedKeys(config)) {
|
|
1008
|
+
const itemConfig = config[key];
|
|
1009
|
+
const expirationConfig = itemConfig.ttlMs !== undefined ? {
|
|
1010
|
+
ttlMs: itemConfig.ttlMs
|
|
1011
|
+
} : undefined;
|
|
1012
|
+
result[key] = createStorageItem({
|
|
1013
|
+
key,
|
|
1014
|
+
scope: StorageScope.Secure,
|
|
1015
|
+
defaultValue: "",
|
|
1016
|
+
namespace: ns,
|
|
1017
|
+
...(itemConfig.biometric !== undefined ? {
|
|
1018
|
+
biometric: itemConfig.biometric
|
|
1019
|
+
} : {}),
|
|
1020
|
+
...(itemConfig.accessControl !== undefined ? {
|
|
1021
|
+
accessControl: itemConfig.accessControl
|
|
1022
|
+
} : {}),
|
|
1023
|
+
...(expirationConfig !== undefined ? {
|
|
1024
|
+
expiration: expirationConfig
|
|
1025
|
+
} : {})
|
|
1026
|
+
});
|
|
1027
|
+
}
|
|
1028
|
+
return result;
|
|
1029
|
+
}
|
|
671
1030
|
//# sourceMappingURL=index.web.js.map
|