react-native-nitro-storage 0.4.4 → 0.5.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 +237 -862
- package/SECURITY.md +26 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +61 -10
- package/docs/api-reference.md +217 -0
- package/docs/batch-transactions-migrations.md +186 -0
- package/docs/benchmarks.md +37 -0
- package/docs/mmkv-migration.md +80 -0
- package/docs/react-hooks.md +113 -0
- package/docs/recipes.md +281 -0
- package/docs/secure-storage.md +171 -0
- package/docs/web-backends.md +141 -0
- package/ios/IOSStorageAdapterCpp.mm +44 -14
- package/lib/commonjs/index.js +271 -5
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +498 -202
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +129 -7
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +41 -0
- package/lib/commonjs/storage-runtime.js.map +1 -0
- package/lib/commonjs/web-storage-backend.js +90 -0
- package/lib/commonjs/web-storage-backend.js.map +1 -0
- package/lib/module/index.js +263 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +490 -202
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +129 -7
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/storage-runtime.js +36 -0
- package/lib/module/storage-runtime.js.map +1 -0
- package/lib/module/web-storage-backend.js +86 -0
- package/lib/module/web-storage-backend.js.map +1 -0
- package/lib/typescript/index.d.ts +14 -7
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +15 -8
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts +6 -2
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +48 -0
- package/lib/typescript/storage-runtime.d.ts.map +1 -0
- package/lib/typescript/web-storage-backend.d.ts +30 -0
- package/lib/typescript/web-storage-backend.d.ts.map +1 -0
- package/package.json +21 -8
- package/src/index.ts +330 -20
- package/src/index.web.ts +673 -245
- package/src/indexeddb-backend.ts +147 -6
- package/src/storage-runtime.ts +129 -0
- package/src/web-storage-backend.ts +129 -0
|
@@ -9,6 +9,12 @@ static NSString* const kKeychainService = @"com.nitrostorage.keychain";
|
|
|
9
9
|
static NSString* const kBiometricKeychainService = @"com.nitrostorage.biometric";
|
|
10
10
|
static NSString* const kDiskSuiteName = @"com.nitrostorage.disk";
|
|
11
11
|
|
|
12
|
+
static std::runtime_error taggedStorageError(const char* code, const std::string& message) {
|
|
13
|
+
return std::runtime_error(
|
|
14
|
+
std::string("[nitro-error:") + code + "] " + message
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
12
18
|
static NSUserDefaults* NitroDiskDefaults() {
|
|
13
19
|
static NSUserDefaults* defaults = [[NSUserDefaults alloc] initWithSuiteName:kDiskSuiteName];
|
|
14
20
|
return defaults ?: [NSUserDefaults standardUserDefaults];
|
|
@@ -191,7 +197,8 @@ static std::vector<std::string> keychainAccountsForService(NSString* service, NS
|
|
|
191
197
|
std::vector<std::string> keys;
|
|
192
198
|
OSStatus status = SecItemCopyMatching((__bridge CFDictionaryRef)query, &result);
|
|
193
199
|
if (status == errSecInteractionNotAllowed) {
|
|
194
|
-
throw
|
|
200
|
+
throw taggedStorageError(
|
|
201
|
+
"keychain_locked",
|
|
195
202
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
196
203
|
"The item is not accessible until the device is unlocked."
|
|
197
204
|
);
|
|
@@ -247,7 +254,8 @@ void IOSStorageAdapterCpp::setSecure(const std::string& key, const std::string&
|
|
|
247
254
|
const OSStatus addStatus = SecItemAdd((__bridge CFDictionaryRef)query, NULL);
|
|
248
255
|
if (addStatus != errSecSuccess) {
|
|
249
256
|
if (addStatus == errSecInteractionNotAllowed) {
|
|
250
|
-
throw
|
|
257
|
+
throw taggedStorageError(
|
|
258
|
+
"keychain_locked",
|
|
251
259
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
252
260
|
"The item is not accessible until the device is unlocked."
|
|
253
261
|
);
|
|
@@ -261,7 +269,8 @@ void IOSStorageAdapterCpp::setSecure(const std::string& key, const std::string&
|
|
|
261
269
|
}
|
|
262
270
|
|
|
263
271
|
if (status == errSecInteractionNotAllowed) {
|
|
264
|
-
throw
|
|
272
|
+
throw taggedStorageError(
|
|
273
|
+
"keychain_locked",
|
|
265
274
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
266
275
|
"The item is not accessible until the device is unlocked."
|
|
267
276
|
);
|
|
@@ -292,7 +301,8 @@ std::optional<std::string> IOSStorageAdapterCpp::getSecure(const std::string& ke
|
|
|
292
301
|
if (str) return std::string([str UTF8String]);
|
|
293
302
|
}
|
|
294
303
|
if (status == errSecInteractionNotAllowed) {
|
|
295
|
-
throw
|
|
304
|
+
throw taggedStorageError(
|
|
305
|
+
"keychain_locked",
|
|
296
306
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
297
307
|
"The item is not accessible until the device is unlocked."
|
|
298
308
|
);
|
|
@@ -312,7 +322,8 @@ void IOSStorageAdapterCpp::deleteSecure(const std::string& key) {
|
|
|
312
322
|
NSMutableDictionary* secureQuery = baseKeychainQuery(nsKey, kKeychainService, group);
|
|
313
323
|
OSStatus secureStatus = SecItemDelete((__bridge CFDictionaryRef)secureQuery);
|
|
314
324
|
if (secureStatus == errSecInteractionNotAllowed) {
|
|
315
|
-
throw
|
|
325
|
+
throw taggedStorageError(
|
|
326
|
+
"keychain_locked",
|
|
316
327
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
317
328
|
"The item is not accessible until the device is unlocked."
|
|
318
329
|
);
|
|
@@ -321,7 +332,8 @@ void IOSStorageAdapterCpp::deleteSecure(const std::string& key) {
|
|
|
321
332
|
NSMutableDictionary* biometricQuery = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
|
|
322
333
|
OSStatus biometricStatus = SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
|
|
323
334
|
if (biometricStatus == errSecInteractionNotAllowed) {
|
|
324
|
-
throw
|
|
335
|
+
throw taggedStorageError(
|
|
336
|
+
"keychain_locked",
|
|
325
337
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
326
338
|
"The item is not accessible until the device is unlocked."
|
|
327
339
|
);
|
|
@@ -427,7 +439,10 @@ void IOSStorageAdapterCpp::clearSecure() {
|
|
|
427
439
|
OSStatus secStatus = SecItemDelete((__bridge CFDictionaryRef)secureQuery);
|
|
428
440
|
if (secStatus != errSecSuccess && secStatus != errSecItemNotFound) {
|
|
429
441
|
if (secStatus == errSecInteractionNotAllowed) {
|
|
430
|
-
throw
|
|
442
|
+
throw taggedStorageError(
|
|
443
|
+
"keychain_locked",
|
|
444
|
+
"NitroStorage: Cannot clear secure storage: keychain is locked (errSecInteractionNotAllowed)"
|
|
445
|
+
);
|
|
431
446
|
}
|
|
432
447
|
throw std::runtime_error(
|
|
433
448
|
std::string("NitroStorage: clearSecure failed with status ") + std::to_string(secStatus));
|
|
@@ -443,7 +458,10 @@ void IOSStorageAdapterCpp::clearSecure() {
|
|
|
443
458
|
OSStatus bioStatus = SecItemDelete((__bridge CFDictionaryRef)biometricQuery);
|
|
444
459
|
if (bioStatus != errSecSuccess && bioStatus != errSecItemNotFound) {
|
|
445
460
|
if (bioStatus == errSecInteractionNotAllowed) {
|
|
446
|
-
throw
|
|
461
|
+
throw taggedStorageError(
|
|
462
|
+
"keychain_locked",
|
|
463
|
+
"NitroStorage: Cannot clear biometric storage: keychain is locked (errSecInteractionNotAllowed)"
|
|
464
|
+
);
|
|
447
465
|
}
|
|
448
466
|
throw std::runtime_error(
|
|
449
467
|
std::string("NitroStorage: clearSecureBiometric failed with status ") + std::to_string(bioStatus));
|
|
@@ -533,7 +551,10 @@ void IOSStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, c
|
|
|
533
551
|
if (error || !access) {
|
|
534
552
|
if (access) CFRelease(access);
|
|
535
553
|
if (error) CFRelease(error);
|
|
536
|
-
throw
|
|
554
|
+
throw taggedStorageError(
|
|
555
|
+
"biometric_unavailable",
|
|
556
|
+
"NitroStorage: Failed to create biometric access control"
|
|
557
|
+
);
|
|
537
558
|
}
|
|
538
559
|
|
|
539
560
|
NSMutableDictionary* attrs = baseKeychainQuery(nsKey, kBiometricKeychainService, group);
|
|
@@ -546,14 +567,16 @@ void IOSStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, c
|
|
|
546
567
|
try {
|
|
547
568
|
setSecure(key, *backup);
|
|
548
569
|
} catch (const std::exception& restoreEx) {
|
|
549
|
-
throw
|
|
570
|
+
throw taggedStorageError(
|
|
571
|
+
"biometric_unavailable",
|
|
550
572
|
std::string("NitroStorage: Biometric set failed with status ") +
|
|
551
573
|
std::to_string(addStatus) +
|
|
552
574
|
" and previous value restoration also failed: " + restoreEx.what());
|
|
553
575
|
}
|
|
554
576
|
}
|
|
555
577
|
if (addStatus == errSecInteractionNotAllowed) {
|
|
556
|
-
throw
|
|
578
|
+
throw taggedStorageError(
|
|
579
|
+
"keychain_locked",
|
|
557
580
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
558
581
|
"The item is not accessible until the device is unlocked."
|
|
559
582
|
);
|
|
@@ -586,13 +609,17 @@ std::optional<std::string> IOSStorageAdapterCpp::getSecureBiometric(const std::s
|
|
|
586
609
|
if (str) return std::string([str UTF8String]);
|
|
587
610
|
}
|
|
588
611
|
if (status == errSecInteractionNotAllowed) {
|
|
589
|
-
throw
|
|
612
|
+
throw taggedStorageError(
|
|
613
|
+
"keychain_locked",
|
|
590
614
|
"NitroStorage: Keychain is locked (errSecInteractionNotAllowed). "
|
|
591
615
|
"The item is not accessible until the device is unlocked."
|
|
592
616
|
);
|
|
593
617
|
}
|
|
594
618
|
if (status == errSecUserCanceled || status == errSecAuthFailed) {
|
|
595
|
-
throw
|
|
619
|
+
throw taggedStorageError(
|
|
620
|
+
"authentication_required",
|
|
621
|
+
"NitroStorage: Biometric authentication failed"
|
|
622
|
+
);
|
|
596
623
|
}
|
|
597
624
|
return std::nullopt;
|
|
598
625
|
}
|
|
@@ -640,7 +667,10 @@ void IOSStorageAdapterCpp::clearSecureBiometric() {
|
|
|
640
667
|
OSStatus status = SecItemDelete((__bridge CFDictionaryRef)query);
|
|
641
668
|
if (status != errSecSuccess && status != errSecItemNotFound) {
|
|
642
669
|
if (status == errSecInteractionNotAllowed) {
|
|
643
|
-
throw
|
|
670
|
+
throw taggedStorageError(
|
|
671
|
+
"keychain_locked",
|
|
672
|
+
"NitroStorage: Cannot clear biometric storage: keychain is locked (errSecInteractionNotAllowed)"
|
|
673
|
+
);
|
|
644
674
|
}
|
|
645
675
|
throw std::runtime_error(
|
|
646
676
|
std::string("NitroStorage: clearSecureBiometric failed with status ") + std::to_string(status));
|
package/lib/commonjs/index.js
CHANGED
|
@@ -29,7 +29,15 @@ Object.defineProperty(exports, "createIndexedDBBackend", {
|
|
|
29
29
|
});
|
|
30
30
|
exports.createSecureAuthStorage = createSecureAuthStorage;
|
|
31
31
|
exports.createStorageItem = createStorageItem;
|
|
32
|
+
exports.flushWebStorageBackends = flushWebStorageBackends;
|
|
32
33
|
exports.getBatch = getBatch;
|
|
34
|
+
Object.defineProperty(exports, "getStorageErrorCode", {
|
|
35
|
+
enumerable: true,
|
|
36
|
+
get: function () {
|
|
37
|
+
return _storageRuntime.getStorageErrorCode;
|
|
38
|
+
}
|
|
39
|
+
});
|
|
40
|
+
exports.getWebDiskStorageBackend = getWebDiskStorageBackend;
|
|
33
41
|
exports.getWebSecureStorageBackend = getWebSecureStorageBackend;
|
|
34
42
|
exports.isKeychainLockedError = isKeychainLockedError;
|
|
35
43
|
Object.defineProperty(exports, "migrateFromMMKV", {
|
|
@@ -43,6 +51,7 @@ exports.registerMigration = registerMigration;
|
|
|
43
51
|
exports.removeBatch = removeBatch;
|
|
44
52
|
exports.runTransaction = runTransaction;
|
|
45
53
|
exports.setBatch = setBatch;
|
|
54
|
+
exports.setWebDiskStorageBackend = setWebDiskStorageBackend;
|
|
46
55
|
exports.setWebSecureStorageBackend = setWebSecureStorageBackend;
|
|
47
56
|
exports.storage = void 0;
|
|
48
57
|
Object.defineProperty(exports, "useSetStorage", {
|
|
@@ -66,6 +75,7 @@ Object.defineProperty(exports, "useStorageSelector", {
|
|
|
66
75
|
var _reactNativeNitroModules = require("react-native-nitro-modules");
|
|
67
76
|
var _Storage = require("./Storage.types");
|
|
68
77
|
var _internal = require("./internal");
|
|
78
|
+
var _storageRuntime = require("./storage-runtime");
|
|
69
79
|
var _migration = require("./migration");
|
|
70
80
|
var _storageHooks = require("./storage-hooks");
|
|
71
81
|
var _indexeddbBackend = require("./indexeddb-backend");
|
|
@@ -82,6 +92,7 @@ const registeredMigrations = new Map();
|
|
|
82
92
|
const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
|
|
83
93
|
Promise.resolve().then(task);
|
|
84
94
|
};
|
|
95
|
+
const now = typeof performance !== "undefined" && typeof performance.now === "function" ? () => performance.now() : () => Date.now();
|
|
85
96
|
let _storageModule = null;
|
|
86
97
|
function getStorageModule() {
|
|
87
98
|
if (!_storageModule) {
|
|
@@ -94,11 +105,15 @@ const memoryListeners = new Map();
|
|
|
94
105
|
const scopedListeners = new Map([[_Storage.StorageScope.Disk, new Map()], [_Storage.StorageScope.Secure, new Map()]]);
|
|
95
106
|
const scopedUnsubscribers = new Map();
|
|
96
107
|
const scopedRawCache = new Map([[_Storage.StorageScope.Disk, new Map()], [_Storage.StorageScope.Secure, new Map()]]);
|
|
108
|
+
const pendingDiskWrites = new Map();
|
|
109
|
+
let diskFlushScheduled = false;
|
|
110
|
+
let diskWritesAsync = false;
|
|
97
111
|
const pendingSecureWrites = new Map();
|
|
98
112
|
let secureFlushScheduled = false;
|
|
99
113
|
let secureDefaultAccessControl = _Storage.AccessControl.WhenUnlocked;
|
|
100
114
|
let metricsObserver;
|
|
101
115
|
const metricsCounters = new Map();
|
|
116
|
+
const nativeSecureBackend = "platform-secure-storage";
|
|
102
117
|
function recordMetric(operation, scope, durationMs, keysCount = 1) {
|
|
103
118
|
const existing = metricsCounters.get(operation);
|
|
104
119
|
if (!existing) {
|
|
@@ -123,11 +138,11 @@ function measureOperation(operation, scope, fn, keysCount = 1) {
|
|
|
123
138
|
if (!metricsObserver) {
|
|
124
139
|
return fn();
|
|
125
140
|
}
|
|
126
|
-
const start =
|
|
141
|
+
const start = now();
|
|
127
142
|
try {
|
|
128
143
|
return fn();
|
|
129
144
|
} finally {
|
|
130
|
-
recordMetric(operation, scope,
|
|
145
|
+
recordMetric(operation, scope, now() - start, keysCount);
|
|
131
146
|
}
|
|
132
147
|
}
|
|
133
148
|
function getScopedListeners(scope) {
|
|
@@ -184,12 +199,50 @@ function addKeyListener(registry, key, listener) {
|
|
|
184
199
|
function readPendingSecureWrite(key) {
|
|
185
200
|
return pendingSecureWrites.get(key)?.value;
|
|
186
201
|
}
|
|
202
|
+
function readPendingDiskWrite(key) {
|
|
203
|
+
return pendingDiskWrites.get(key)?.value;
|
|
204
|
+
}
|
|
205
|
+
function hasPendingDiskWrite(key) {
|
|
206
|
+
return pendingDiskWrites.has(key);
|
|
207
|
+
}
|
|
187
208
|
function hasPendingSecureWrite(key) {
|
|
188
209
|
return pendingSecureWrites.has(key);
|
|
189
210
|
}
|
|
211
|
+
function clearPendingDiskWrite(key) {
|
|
212
|
+
pendingDiskWrites.delete(key);
|
|
213
|
+
}
|
|
190
214
|
function clearPendingSecureWrite(key) {
|
|
191
215
|
pendingSecureWrites.delete(key);
|
|
192
216
|
}
|
|
217
|
+
function flushDiskWrites() {
|
|
218
|
+
diskFlushScheduled = false;
|
|
219
|
+
if (pendingDiskWrites.size === 0) {
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
const writes = Array.from(pendingDiskWrites.values());
|
|
223
|
+
pendingDiskWrites.clear();
|
|
224
|
+
const keysToSet = [];
|
|
225
|
+
const valuesToSet = [];
|
|
226
|
+
const keysToRemove = [];
|
|
227
|
+
writes.forEach(({
|
|
228
|
+
key,
|
|
229
|
+
value
|
|
230
|
+
}) => {
|
|
231
|
+
if (value === undefined) {
|
|
232
|
+
keysToRemove.push(key);
|
|
233
|
+
return;
|
|
234
|
+
}
|
|
235
|
+
keysToSet.push(key);
|
|
236
|
+
valuesToSet.push(value);
|
|
237
|
+
});
|
|
238
|
+
const storageModule = getStorageModule();
|
|
239
|
+
if (keysToSet.length > 0) {
|
|
240
|
+
storageModule.setBatch(keysToSet, valuesToSet, _Storage.StorageScope.Disk);
|
|
241
|
+
}
|
|
242
|
+
if (keysToRemove.length > 0) {
|
|
243
|
+
storageModule.removeBatch(keysToRemove, _Storage.StorageScope.Disk);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
193
246
|
function flushSecureWrites() {
|
|
194
247
|
secureFlushScheduled = false;
|
|
195
248
|
if (pendingSecureWrites.size === 0) {
|
|
@@ -229,6 +282,17 @@ function flushSecureWrites() {
|
|
|
229
282
|
storageModule.removeBatch(keysToRemove, _Storage.StorageScope.Secure);
|
|
230
283
|
}
|
|
231
284
|
}
|
|
285
|
+
function scheduleDiskWrite(key, value) {
|
|
286
|
+
pendingDiskWrites.set(key, {
|
|
287
|
+
key,
|
|
288
|
+
value
|
|
289
|
+
});
|
|
290
|
+
if (diskFlushScheduled) {
|
|
291
|
+
return;
|
|
292
|
+
}
|
|
293
|
+
diskFlushScheduled = true;
|
|
294
|
+
runMicrotask(flushDiskWrites);
|
|
295
|
+
}
|
|
232
296
|
function scheduleSecureWrite(key, value, accessControl) {
|
|
233
297
|
const pendingWrite = {
|
|
234
298
|
key,
|
|
@@ -249,6 +313,13 @@ function ensureNativeScopeSubscription(scope) {
|
|
|
249
313
|
return;
|
|
250
314
|
}
|
|
251
315
|
const unsubscribe = getStorageModule().addOnChange(scope, (key, value) => {
|
|
316
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
317
|
+
if (key === "") {
|
|
318
|
+
pendingDiskWrites.clear();
|
|
319
|
+
} else {
|
|
320
|
+
clearPendingDiskWrite(key);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
252
323
|
if (scope === _Storage.StorageScope.Secure) {
|
|
253
324
|
if (key === "") {
|
|
254
325
|
pendingSecureWrites.clear();
|
|
@@ -284,6 +355,9 @@ function getRawValue(key, scope) {
|
|
|
284
355
|
const value = memoryStore.get(key);
|
|
285
356
|
return typeof value === "string" ? value : undefined;
|
|
286
357
|
}
|
|
358
|
+
if (scope === _Storage.StorageScope.Disk && hasPendingDiskWrite(key)) {
|
|
359
|
+
return readPendingDiskWrite(key);
|
|
360
|
+
}
|
|
287
361
|
if (scope === _Storage.StorageScope.Secure && hasPendingSecureWrite(key)) {
|
|
288
362
|
return readPendingSecureWrite(key);
|
|
289
363
|
}
|
|
@@ -296,6 +370,15 @@ function setRawValue(key, value, scope) {
|
|
|
296
370
|
notifyKeyListeners(memoryListeners, key);
|
|
297
371
|
return;
|
|
298
372
|
}
|
|
373
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
374
|
+
cacheRawValue(scope, key, value);
|
|
375
|
+
if (diskWritesAsync) {
|
|
376
|
+
scheduleDiskWrite(key, value);
|
|
377
|
+
return;
|
|
378
|
+
}
|
|
379
|
+
flushDiskWrites();
|
|
380
|
+
clearPendingDiskWrite(key);
|
|
381
|
+
}
|
|
299
382
|
if (scope === _Storage.StorageScope.Secure) {
|
|
300
383
|
flushSecureWrites();
|
|
301
384
|
clearPendingSecureWrite(key);
|
|
@@ -311,6 +394,15 @@ function removeRawValue(key, scope) {
|
|
|
311
394
|
notifyKeyListeners(memoryListeners, key);
|
|
312
395
|
return;
|
|
313
396
|
}
|
|
397
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
398
|
+
cacheRawValue(scope, key, undefined);
|
|
399
|
+
if (diskWritesAsync) {
|
|
400
|
+
scheduleDiskWrite(key, undefined);
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
flushDiskWrites();
|
|
404
|
+
clearPendingDiskWrite(key);
|
|
405
|
+
}
|
|
314
406
|
if (scope === _Storage.StorageScope.Secure) {
|
|
315
407
|
flushSecureWrites();
|
|
316
408
|
clearPendingSecureWrite(key);
|
|
@@ -337,6 +429,10 @@ const storage = exports.storage = {
|
|
|
337
429
|
notifyAllListeners(memoryListeners);
|
|
338
430
|
return;
|
|
339
431
|
}
|
|
432
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
433
|
+
flushDiskWrites();
|
|
434
|
+
pendingDiskWrites.clear();
|
|
435
|
+
}
|
|
340
436
|
if (scope === _Storage.StorageScope.Secure) {
|
|
341
437
|
flushSecureWrites();
|
|
342
438
|
pendingSecureWrites.clear();
|
|
@@ -365,6 +461,9 @@ const storage = exports.storage = {
|
|
|
365
461
|
return;
|
|
366
462
|
}
|
|
367
463
|
const keyPrefix = (0, _internal.prefixKey)(namespace, "");
|
|
464
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
465
|
+
flushDiskWrites();
|
|
466
|
+
}
|
|
368
467
|
if (scope === _Storage.StorageScope.Secure) {
|
|
369
468
|
flushSecureWrites();
|
|
370
469
|
}
|
|
@@ -388,6 +487,12 @@ const storage = exports.storage = {
|
|
|
388
487
|
if (scope === _Storage.StorageScope.Memory) {
|
|
389
488
|
return memoryStore.has(key);
|
|
390
489
|
}
|
|
490
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
491
|
+
flushDiskWrites();
|
|
492
|
+
}
|
|
493
|
+
if (scope === _Storage.StorageScope.Secure) {
|
|
494
|
+
flushSecureWrites();
|
|
495
|
+
}
|
|
391
496
|
return getStorageModule().has(key, scope);
|
|
392
497
|
});
|
|
393
498
|
},
|
|
@@ -397,6 +502,12 @@ const storage = exports.storage = {
|
|
|
397
502
|
if (scope === _Storage.StorageScope.Memory) {
|
|
398
503
|
return Array.from(memoryStore.keys());
|
|
399
504
|
}
|
|
505
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
506
|
+
flushDiskWrites();
|
|
507
|
+
}
|
|
508
|
+
if (scope === _Storage.StorageScope.Secure) {
|
|
509
|
+
flushSecureWrites();
|
|
510
|
+
}
|
|
400
511
|
return getStorageModule().getAllKeys(scope);
|
|
401
512
|
});
|
|
402
513
|
},
|
|
@@ -406,6 +517,12 @@ const storage = exports.storage = {
|
|
|
406
517
|
if (scope === _Storage.StorageScope.Memory) {
|
|
407
518
|
return Array.from(memoryStore.keys()).filter(key => key.startsWith(prefix));
|
|
408
519
|
}
|
|
520
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
521
|
+
flushDiskWrites();
|
|
522
|
+
}
|
|
523
|
+
if (scope === _Storage.StorageScope.Secure) {
|
|
524
|
+
flushSecureWrites();
|
|
525
|
+
}
|
|
409
526
|
return getStorageModule().getKeysByPrefix(prefix, scope);
|
|
410
527
|
});
|
|
411
528
|
},
|
|
@@ -425,6 +542,12 @@ const storage = exports.storage = {
|
|
|
425
542
|
});
|
|
426
543
|
return result;
|
|
427
544
|
}
|
|
545
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
546
|
+
flushDiskWrites();
|
|
547
|
+
}
|
|
548
|
+
if (scope === _Storage.StorageScope.Secure) {
|
|
549
|
+
flushSecureWrites();
|
|
550
|
+
}
|
|
428
551
|
const values = getStorageModule().getBatch(keys, scope);
|
|
429
552
|
keys.forEach((key, idx) => {
|
|
430
553
|
const value = (0, _internal.decodeNativeBatchValue)(values[idx]);
|
|
@@ -445,6 +568,12 @@ const storage = exports.storage = {
|
|
|
445
568
|
});
|
|
446
569
|
return result;
|
|
447
570
|
}
|
|
571
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
572
|
+
flushDiskWrites();
|
|
573
|
+
}
|
|
574
|
+
if (scope === _Storage.StorageScope.Secure) {
|
|
575
|
+
flushSecureWrites();
|
|
576
|
+
}
|
|
448
577
|
const keys = getStorageModule().getAllKeys(scope);
|
|
449
578
|
if (keys.length === 0) return result;
|
|
450
579
|
const values = getStorageModule().getBatch(keys, scope);
|
|
@@ -461,6 +590,12 @@ const storage = exports.storage = {
|
|
|
461
590
|
if (scope === _Storage.StorageScope.Memory) {
|
|
462
591
|
return memoryStore.size;
|
|
463
592
|
}
|
|
593
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
594
|
+
flushDiskWrites();
|
|
595
|
+
}
|
|
596
|
+
if (scope === _Storage.StorageScope.Secure) {
|
|
597
|
+
flushSecureWrites();
|
|
598
|
+
}
|
|
464
599
|
return getStorageModule().size(scope);
|
|
465
600
|
});
|
|
466
601
|
},
|
|
@@ -475,6 +610,19 @@ const storage = exports.storage = {
|
|
|
475
610
|
getStorageModule().setSecureWritesAsync(enabled);
|
|
476
611
|
});
|
|
477
612
|
},
|
|
613
|
+
setDiskWritesAsync: enabled => {
|
|
614
|
+
measureOperation("storage:setDiskWritesAsync", _Storage.StorageScope.Disk, () => {
|
|
615
|
+
diskWritesAsync = enabled;
|
|
616
|
+
if (!enabled) {
|
|
617
|
+
flushDiskWrites();
|
|
618
|
+
}
|
|
619
|
+
});
|
|
620
|
+
},
|
|
621
|
+
flushDiskWrites: () => {
|
|
622
|
+
measureOperation("storage:flushDiskWrites", _Storage.StorageScope.Disk, () => {
|
|
623
|
+
flushDiskWrites();
|
|
624
|
+
});
|
|
625
|
+
},
|
|
478
626
|
flushSecureWrites: () => {
|
|
479
627
|
measureOperation("storage:flushSecureWrites", _Storage.StorageScope.Secure, () => {
|
|
480
628
|
flushSecureWrites();
|
|
@@ -503,6 +651,67 @@ const storage = exports.storage = {
|
|
|
503
651
|
resetMetrics: () => {
|
|
504
652
|
metricsCounters.clear();
|
|
505
653
|
},
|
|
654
|
+
getCapabilities: () => ({
|
|
655
|
+
platform: "native",
|
|
656
|
+
backend: {
|
|
657
|
+
disk: "platform-preferences",
|
|
658
|
+
secure: nativeSecureBackend
|
|
659
|
+
},
|
|
660
|
+
writeBuffering: {
|
|
661
|
+
disk: true,
|
|
662
|
+
secure: true
|
|
663
|
+
},
|
|
664
|
+
errorClassification: true
|
|
665
|
+
}),
|
|
666
|
+
getSecurityCapabilities: () => ({
|
|
667
|
+
platform: "native",
|
|
668
|
+
secureStorage: {
|
|
669
|
+
backend: nativeSecureBackend,
|
|
670
|
+
encrypted: "available",
|
|
671
|
+
accessControl: "unknown",
|
|
672
|
+
keychainAccessGroup: "unknown",
|
|
673
|
+
hardwareBacked: "unknown"
|
|
674
|
+
},
|
|
675
|
+
biometric: {
|
|
676
|
+
storage: "unknown",
|
|
677
|
+
prompt: "unknown",
|
|
678
|
+
biometryOnly: "unknown",
|
|
679
|
+
biometryOrPasscode: "unknown"
|
|
680
|
+
},
|
|
681
|
+
metadata: {
|
|
682
|
+
perKey: true,
|
|
683
|
+
listsWithoutValues: true,
|
|
684
|
+
persistsTimestamps: false
|
|
685
|
+
}
|
|
686
|
+
}),
|
|
687
|
+
getSecureMetadata: key => {
|
|
688
|
+
return measureOperation("storage:getSecureMetadata", _Storage.StorageScope.Secure, () => {
|
|
689
|
+
flushSecureWrites();
|
|
690
|
+
const storageModule = getStorageModule();
|
|
691
|
+
const biometricProtected = storageModule.hasSecureBiometric(key);
|
|
692
|
+
const exists = biometricProtected || storageModule.has(key, _Storage.StorageScope.Secure);
|
|
693
|
+
let kind = "missing";
|
|
694
|
+
if (exists) {
|
|
695
|
+
kind = biometricProtected ? "biometric" : "secure";
|
|
696
|
+
}
|
|
697
|
+
return {
|
|
698
|
+
key,
|
|
699
|
+
exists,
|
|
700
|
+
kind,
|
|
701
|
+
backend: nativeSecureBackend,
|
|
702
|
+
encrypted: "available",
|
|
703
|
+
hardwareBacked: "unknown",
|
|
704
|
+
biometricProtected,
|
|
705
|
+
valueExposed: false
|
|
706
|
+
};
|
|
707
|
+
});
|
|
708
|
+
},
|
|
709
|
+
getAllSecureMetadata: () => {
|
|
710
|
+
return measureOperation("storage:getAllSecureMetadata", _Storage.StorageScope.Secure, () => {
|
|
711
|
+
flushSecureWrites();
|
|
712
|
+
return getStorageModule().getAllKeys(_Storage.StorageScope.Secure).map(key => storage.getSecureMetadata(key));
|
|
713
|
+
});
|
|
714
|
+
},
|
|
506
715
|
getString: (key, scope) => {
|
|
507
716
|
return measureOperation("storage:getString", scope, () => {
|
|
508
717
|
return getRawValue(key, scope);
|
|
@@ -546,6 +755,15 @@ function setWebSecureStorageBackend(_backend) {
|
|
|
546
755
|
function getWebSecureStorageBackend() {
|
|
547
756
|
return undefined;
|
|
548
757
|
}
|
|
758
|
+
function setWebDiskStorageBackend(_backend) {
|
|
759
|
+
// Native platforms do not use web disk backends.
|
|
760
|
+
}
|
|
761
|
+
function getWebDiskStorageBackend() {
|
|
762
|
+
return undefined;
|
|
763
|
+
}
|
|
764
|
+
async function flushWebStorageBackends() {
|
|
765
|
+
// Native platforms do not use web storage backends.
|
|
766
|
+
}
|
|
549
767
|
function canUseRawBatchPath(item) {
|
|
550
768
|
return item._hasExpiration === false && item._hasValidation === false && item._isBiometric !== true && item._secureAccessControl === undefined;
|
|
551
769
|
}
|
|
@@ -573,6 +791,7 @@ function createStorageItem(config) {
|
|
|
573
791
|
const expirationTtlMs = expiration?.ttlMs;
|
|
574
792
|
const memoryExpiration = expiration && isMemory ? new Map() : null;
|
|
575
793
|
const readCache = !isMemory && config.readCache === true;
|
|
794
|
+
const coalesceDiskWrites = config.scope === _Storage.StorageScope.Disk && config.coalesceDiskWrites === true;
|
|
576
795
|
const coalesceSecureWrites = config.scope === _Storage.StorageScope.Secure && config.coalesceSecureWrites === true && !isBiometric;
|
|
577
796
|
const defaultValue = config.defaultValue;
|
|
578
797
|
const nonMemoryScope = config.scope === _Storage.StorageScope.Disk ? _Storage.StorageScope.Disk : config.scope === _Storage.StorageScope.Secure ? _Storage.StorageScope.Secure : null;
|
|
@@ -620,6 +839,12 @@ function createStorageItem(config) {
|
|
|
620
839
|
}
|
|
621
840
|
return memoryStore.get(storageKey);
|
|
622
841
|
}
|
|
842
|
+
if (nonMemoryScope === _Storage.StorageScope.Disk) {
|
|
843
|
+
const pending = pendingDiskWrites.get(storageKey);
|
|
844
|
+
if (pending !== undefined) {
|
|
845
|
+
return pending.value;
|
|
846
|
+
}
|
|
847
|
+
}
|
|
623
848
|
if (nonMemoryScope === _Storage.StorageScope.Secure && !isBiometric) {
|
|
624
849
|
const pending = pendingSecureWrites.get(storageKey);
|
|
625
850
|
if (pending !== undefined) {
|
|
@@ -646,6 +871,13 @@ function createStorageItem(config) {
|
|
|
646
871
|
return;
|
|
647
872
|
}
|
|
648
873
|
cacheRawValue(nonMemoryScope, storageKey, rawValue);
|
|
874
|
+
if (nonMemoryScope === _Storage.StorageScope.Disk) {
|
|
875
|
+
if (coalesceDiskWrites || diskWritesAsync) {
|
|
876
|
+
scheduleDiskWrite(storageKey, rawValue);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
clearPendingDiskWrite(storageKey);
|
|
880
|
+
}
|
|
649
881
|
if (coalesceSecureWrites) {
|
|
650
882
|
scheduleSecureWrite(storageKey, rawValue, secureAccessControl ?? secureDefaultAccessControl);
|
|
651
883
|
return;
|
|
@@ -662,6 +894,13 @@ function createStorageItem(config) {
|
|
|
662
894
|
return;
|
|
663
895
|
}
|
|
664
896
|
cacheRawValue(nonMemoryScope, storageKey, undefined);
|
|
897
|
+
if (nonMemoryScope === _Storage.StorageScope.Disk) {
|
|
898
|
+
if (coalesceDiskWrites || diskWritesAsync) {
|
|
899
|
+
scheduleDiskWrite(storageKey, undefined);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
clearPendingDiskWrite(storageKey);
|
|
903
|
+
}
|
|
665
904
|
if (coalesceSecureWrites) {
|
|
666
905
|
scheduleSecureWrite(storageKey, undefined, secureAccessControl ?? secureDefaultAccessControl);
|
|
667
906
|
return;
|
|
@@ -822,6 +1061,18 @@ function createStorageItem(config) {
|
|
|
822
1061
|
const hasItem = () => measureOperation("item:has", config.scope, () => {
|
|
823
1062
|
if (isMemory) return memoryStore.has(storageKey);
|
|
824
1063
|
if (isBiometric) return getStorageModule().hasSecureBiometric(storageKey);
|
|
1064
|
+
if (nonMemoryScope === _Storage.StorageScope.Disk) {
|
|
1065
|
+
const pending = pendingDiskWrites.get(storageKey);
|
|
1066
|
+
if (pending !== undefined) {
|
|
1067
|
+
return pending.value !== undefined;
|
|
1068
|
+
}
|
|
1069
|
+
}
|
|
1070
|
+
if (nonMemoryScope === _Storage.StorageScope.Secure) {
|
|
1071
|
+
const pending = pendingSecureWrites.get(storageKey);
|
|
1072
|
+
if (pending !== undefined) {
|
|
1073
|
+
return pending.value !== undefined;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
825
1076
|
return getStorageModule().has(storageKey, config.scope);
|
|
826
1077
|
});
|
|
827
1078
|
const subscribe = callback => {
|
|
@@ -882,6 +1133,13 @@ function getBatch(items, scope) {
|
|
|
882
1133
|
const keysToFetch = [];
|
|
883
1134
|
const keyIndexes = [];
|
|
884
1135
|
items.forEach((item, index) => {
|
|
1136
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
1137
|
+
const pending = pendingDiskWrites.get(item.key);
|
|
1138
|
+
if (pending !== undefined) {
|
|
1139
|
+
rawValues[index] = pending.value;
|
|
1140
|
+
return;
|
|
1141
|
+
}
|
|
1142
|
+
}
|
|
885
1143
|
if (scope === _Storage.StorageScope.Secure) {
|
|
886
1144
|
const pending = pendingSecureWrites.get(item.key);
|
|
887
1145
|
if (pending !== undefined) {
|
|
@@ -1000,6 +1258,7 @@ function setBatch(items, scope) {
|
|
|
1000
1258
|
});
|
|
1001
1259
|
return;
|
|
1002
1260
|
}
|
|
1261
|
+
flushDiskWrites();
|
|
1003
1262
|
const useRawBatchPath = items.every(({
|
|
1004
1263
|
item
|
|
1005
1264
|
}) => canUseRawBatchPath(asInternal(item)));
|
|
@@ -1024,6 +1283,9 @@ function removeBatch(items, scope) {
|
|
|
1024
1283
|
return;
|
|
1025
1284
|
}
|
|
1026
1285
|
const keys = items.map(item => item.key);
|
|
1286
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
1287
|
+
flushDiskWrites();
|
|
1288
|
+
}
|
|
1027
1289
|
if (scope === _Storage.StorageScope.Secure) {
|
|
1028
1290
|
flushSecureWrites();
|
|
1029
1291
|
}
|
|
@@ -1069,6 +1331,9 @@ function migrateToLatest(scope = _Storage.StorageScope.Disk) {
|
|
|
1069
1331
|
function runTransaction(scope, transaction) {
|
|
1070
1332
|
return measureOperation("transaction:run", scope, () => {
|
|
1071
1333
|
(0, _internal.assertValidScope)(scope);
|
|
1334
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
1335
|
+
flushDiskWrites();
|
|
1336
|
+
}
|
|
1072
1337
|
if (scope === _Storage.StorageScope.Secure) {
|
|
1073
1338
|
flushSecureWrites();
|
|
1074
1339
|
}
|
|
@@ -1135,6 +1400,9 @@ function runTransaction(scope, transaction) {
|
|
|
1135
1400
|
valuesToSet.push(previousValue);
|
|
1136
1401
|
}
|
|
1137
1402
|
});
|
|
1403
|
+
if (scope === _Storage.StorageScope.Disk) {
|
|
1404
|
+
flushDiskWrites();
|
|
1405
|
+
}
|
|
1138
1406
|
if (scope === _Storage.StorageScope.Secure) {
|
|
1139
1407
|
flushSecureWrites();
|
|
1140
1408
|
}
|
|
@@ -1152,9 +1420,7 @@ function runTransaction(scope, transaction) {
|
|
|
1152
1420
|
});
|
|
1153
1421
|
}
|
|
1154
1422
|
function isKeychainLockedError(err) {
|
|
1155
|
-
|
|
1156
|
-
const msg = err.message;
|
|
1157
|
-
return msg.includes("errSecInteractionNotAllowed") || msg.includes("UserNotAuthenticatedException") || msg.includes("KeyStoreException") || msg.includes("KeyPermanentlyInvalidatedException") || msg.includes("InvalidKeyException") || msg.includes("android.security.keystore");
|
|
1423
|
+
return (0, _storageRuntime.isLockedStorageErrorCode)((0, _storageRuntime.getStorageErrorCode)(err));
|
|
1158
1424
|
}
|
|
1159
1425
|
function createSecureAuthStorage(config, options) {
|
|
1160
1426
|
const ns = options?.namespace ?? "auth";
|