react-native-nitro-storage 0.14.0 → 0.15.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/CHANGELOG.md +16 -0
- package/README.md +203 -4
- package/cpp/bindings/HybridStorage.cpp +21 -0
- package/cpp/bindings/HybridStorage.hpp +2 -0
- package/cpp/core/NativeStorageAdapter.hpp +6 -0
- package/docs/api-reference.md +63 -44
- package/docs/qa/agent-device-replay.md +41 -5
- package/ios/IOSStorageAdapterCpp.hpp +12 -0
- package/ios/IOSStorageAdapterCpp.mm +180 -2
- package/lib/commonjs/index.js +16 -1
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +10 -1
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/shared.js +6 -0
- package/lib/commonjs/shared.js.map +1 -1
- package/lib/commonjs/storage-core.js +79 -0
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/testing.js +31 -2
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/module/index.js +16 -1
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +10 -1
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/shared.js +5 -0
- package/lib/module/shared.js.map +1 -1
- package/lib/module/storage-core.js +80 -1
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/testing.js +30 -1
- package/lib/module/testing.js.map +1 -1
- package/lib/typescript/Storage.nitro.d.ts +2 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +4 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +4 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/shared.d.ts +1 -0
- package/lib/typescript/shared.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +28 -1
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-platform.d.ts +2 -0
- package/lib/typescript/storage-platform.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +9 -1
- package/lib/typescript/testing.d.ts.map +1 -1
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
- package/package.json +2 -2
- package/src/Storage.nitro.ts +2 -0
- package/src/index.ts +20 -0
- package/src/index.web.ts +14 -0
- package/src/shared.ts +10 -0
- package/src/storage-core.ts +121 -1
- package/src/storage-platform.ts +2 -0
- package/src/testing.ts +37 -0
package/src/storage-core.ts
CHANGED
|
@@ -62,7 +62,12 @@ import {
|
|
|
62
62
|
type StorageKeyChangeEvent,
|
|
63
63
|
} from "./storage-events";
|
|
64
64
|
import type { StorageSetter } from "./storage-hooks";
|
|
65
|
-
import {
|
|
65
|
+
import {
|
|
66
|
+
getStorageErrorCode,
|
|
67
|
+
isStorageError,
|
|
68
|
+
type SecureStorageMetadata,
|
|
69
|
+
type StorageErrorCode,
|
|
70
|
+
} from "./storage-runtime";
|
|
66
71
|
import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
|
|
67
72
|
|
|
68
73
|
export type TransactionContext = {
|
|
@@ -217,6 +222,30 @@ export type StorageClearOptions = {
|
|
|
217
222
|
except?: readonly StorageKeyRef[];
|
|
218
223
|
};
|
|
219
224
|
|
|
225
|
+
export type SecureAccessControlMigrationOptions = {
|
|
226
|
+
/** Secure keys to rewrite. Defaults to every Secure key. */
|
|
227
|
+
keys?: readonly string[];
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
export type SecureAccessControlMigrationResult = {
|
|
231
|
+
/** Keys rewritten so the Keychain item carries the new access level. */
|
|
232
|
+
migrated: string[];
|
|
233
|
+
/** Keys left unchanged because the Keychain was locked (`keychain_locked`). */
|
|
234
|
+
locked: string[];
|
|
235
|
+
/** Keys that no longer exist when the migration reached them. */
|
|
236
|
+
missing: string[];
|
|
237
|
+
/** Biometric-protected keys, and every key on platforms without access classes. */
|
|
238
|
+
skipped: string[];
|
|
239
|
+
/** Keys left unchanged because of another error. */
|
|
240
|
+
failed: { key: string; code?: StorageErrorCode }[];
|
|
241
|
+
/**
|
|
242
|
+
* `true` when flushing queued Secure writes or listing the Secure keys
|
|
243
|
+
* failed with `keychain_locked`. The default level and every item are
|
|
244
|
+
* unchanged; retry once protected data is available.
|
|
245
|
+
*/
|
|
246
|
+
enumerationLocked: boolean;
|
|
247
|
+
};
|
|
248
|
+
|
|
220
249
|
export type SetItemConfig<TMember extends string = string> = Omit<
|
|
221
250
|
StorageItemConfig<Record<string, true>>,
|
|
222
251
|
"defaultValue" | "serialize" | "deserialize"
|
|
@@ -274,6 +303,7 @@ export type StorageCoreAdapter = {
|
|
|
274
303
|
backend: StorageCoreBackend;
|
|
275
304
|
changeSource: StorageChangeSource;
|
|
276
305
|
applyAccessControlOnSecureRawWrite: boolean;
|
|
306
|
+
supportsSecureAccessControlMigration(): boolean;
|
|
277
307
|
ensureScopeSubscription(scope: NonMemoryScope): void;
|
|
278
308
|
maybeCleanupScopeSubscription(scope: NonMemoryScope): void;
|
|
279
309
|
onWillEmitChanges(
|
|
@@ -3367,6 +3397,95 @@ export function createStorageCore(
|
|
|
3367
3397
|
registeredMigrations.set(version, migration);
|
|
3368
3398
|
}
|
|
3369
3399
|
|
|
3400
|
+
function migrateSecureAccessControl(
|
|
3401
|
+
level: AccessControl,
|
|
3402
|
+
options?: SecureAccessControlMigrationOptions,
|
|
3403
|
+
): SecureAccessControlMigrationResult {
|
|
3404
|
+
return measureOperation(
|
|
3405
|
+
"storage:migrateSecureAccessControl",
|
|
3406
|
+
StorageScope.Secure,
|
|
3407
|
+
() => {
|
|
3408
|
+
assertAccessControlLevel(level);
|
|
3409
|
+
const requestedKeys = options?.keys;
|
|
3410
|
+
requestedKeys?.forEach((key) => {
|
|
3411
|
+
assertValidStorageKey(key);
|
|
3412
|
+
});
|
|
3413
|
+
|
|
3414
|
+
let keys: string[];
|
|
3415
|
+
try {
|
|
3416
|
+
flushSecureWrites();
|
|
3417
|
+
keys = Array.from(
|
|
3418
|
+
new Set(
|
|
3419
|
+
requestedKeys ?? adapter.backend.getAllKeys(StorageScope.Secure),
|
|
3420
|
+
),
|
|
3421
|
+
);
|
|
3422
|
+
} catch (error) {
|
|
3423
|
+
if (getStorageErrorCode(error) !== "keychain_locked") {
|
|
3424
|
+
throw error;
|
|
3425
|
+
}
|
|
3426
|
+
return {
|
|
3427
|
+
migrated: [],
|
|
3428
|
+
locked: [],
|
|
3429
|
+
missing: [],
|
|
3430
|
+
skipped: [],
|
|
3431
|
+
failed: [],
|
|
3432
|
+
enumerationLocked: true,
|
|
3433
|
+
};
|
|
3434
|
+
}
|
|
3435
|
+
secureDefaultAccessControl = level;
|
|
3436
|
+
adapter.backend.setSecureAccessControl(level);
|
|
3437
|
+
|
|
3438
|
+
const result: SecureAccessControlMigrationResult = {
|
|
3439
|
+
migrated: [],
|
|
3440
|
+
locked: [],
|
|
3441
|
+
missing: [],
|
|
3442
|
+
skipped: [],
|
|
3443
|
+
failed: [],
|
|
3444
|
+
enumerationLocked: false,
|
|
3445
|
+
};
|
|
3446
|
+
if (!adapter.supportsSecureAccessControlMigration()) {
|
|
3447
|
+
result.skipped = keys;
|
|
3448
|
+
return result;
|
|
3449
|
+
}
|
|
3450
|
+
|
|
3451
|
+
keys.forEach((key) => {
|
|
3452
|
+
try {
|
|
3453
|
+
let hasBiometric: boolean;
|
|
3454
|
+
try {
|
|
3455
|
+
hasBiometric = adapter.backend.hasSecureBiometric(key);
|
|
3456
|
+
} catch (error) {
|
|
3457
|
+
if (getStorageErrorCode(error) !== "keychain_locked") {
|
|
3458
|
+
throw error;
|
|
3459
|
+
}
|
|
3460
|
+
result.skipped.push(key);
|
|
3461
|
+
return;
|
|
3462
|
+
}
|
|
3463
|
+
if (hasBiometric) {
|
|
3464
|
+
result.skipped.push(key);
|
|
3465
|
+
return;
|
|
3466
|
+
}
|
|
3467
|
+
const raw = adapter.backend.get(key, StorageScope.Secure);
|
|
3468
|
+
if (raw === undefined) {
|
|
3469
|
+
result.missing.push(key);
|
|
3470
|
+
return;
|
|
3471
|
+
}
|
|
3472
|
+
adapter.backend.set(key, raw, StorageScope.Secure);
|
|
3473
|
+
result.migrated.push(key);
|
|
3474
|
+
} catch (error) {
|
|
3475
|
+
const code = getStorageErrorCode(error);
|
|
3476
|
+
if (code === "keychain_locked") {
|
|
3477
|
+
result.locked.push(key);
|
|
3478
|
+
} else {
|
|
3479
|
+
result.failed.push(code === undefined ? { key } : { key, code });
|
|
3480
|
+
}
|
|
3481
|
+
}
|
|
3482
|
+
});
|
|
3483
|
+
return result;
|
|
3484
|
+
},
|
|
3485
|
+
options?.keys?.length ?? 1,
|
|
3486
|
+
);
|
|
3487
|
+
}
|
|
3488
|
+
|
|
3370
3489
|
function migrateToLatest(scope: StorageScope = StorageScope.Disk): number {
|
|
3371
3490
|
return measureOperation("migration:run", scope, () => {
|
|
3372
3491
|
assertValidScope(scope);
|
|
@@ -3951,6 +4070,7 @@ export function createStorageCore(
|
|
|
3951
4070
|
removeBatch,
|
|
3952
4071
|
registerMigration,
|
|
3953
4072
|
migrateToLatest,
|
|
4073
|
+
migrateSecureAccessControl,
|
|
3954
4074
|
runTransaction,
|
|
3955
4075
|
createSecureAuthStorage,
|
|
3956
4076
|
internals,
|
package/src/storage-platform.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { AccessControl, StorageScope } from "./Storage.types";
|
|
|
7
7
|
|
|
8
8
|
export type PlatformStorage = StorageCore["storage"] & {
|
|
9
9
|
setAccessControl(level: AccessControl): void;
|
|
10
|
+
isProtectedDataAvailable(): boolean;
|
|
11
|
+
onProtectedDataAvailable(listener: () => void): () => void;
|
|
10
12
|
setSecureWritesAsync(enabled: boolean): void;
|
|
11
13
|
setKeychainAccessGroup(group: string): void;
|
|
12
14
|
getCapabilities(): StorageCapabilities;
|
package/src/testing.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { createIndexedDBBackend as createIndexedDBBackendFromSubpath } from "./indexeddb-backend";
|
|
2
2
|
import {
|
|
3
3
|
assertAccessControlLevel,
|
|
4
|
+
assertProtectedDataListener,
|
|
4
5
|
notifyAllListeners,
|
|
5
6
|
notifyKeyListeners,
|
|
6
7
|
} from "./shared";
|
|
@@ -84,6 +85,8 @@ export type {
|
|
|
84
85
|
WebStorageScope,
|
|
85
86
|
} from "./web-storage-backend";
|
|
86
87
|
export type {
|
|
88
|
+
SecureAccessControlMigrationOptions,
|
|
89
|
+
SecureAccessControlMigrationResult,
|
|
87
90
|
SetItemConfig,
|
|
88
91
|
SetStorageItem,
|
|
89
92
|
StorageBatchSetItem,
|
|
@@ -224,6 +227,7 @@ function buildTestingModule() {
|
|
|
224
227
|
backend,
|
|
225
228
|
changeSource: "native",
|
|
226
229
|
applyAccessControlOnSecureRawWrite: true,
|
|
230
|
+
supportsSecureAccessControlMigration: () => true,
|
|
227
231
|
ensureScopeSubscription: (_scope: NonMemoryScope) => {},
|
|
228
232
|
maybeCleanupScopeSubscription: (_scope: NonMemoryScope) => {},
|
|
229
233
|
onWillEmitChanges: () => {},
|
|
@@ -237,6 +241,8 @@ function buildTestingModule() {
|
|
|
237
241
|
|
|
238
242
|
const core = createStorageCore(buildAdapter);
|
|
239
243
|
const { internals } = core;
|
|
244
|
+
let protectedDataAvailable = true;
|
|
245
|
+
const protectedDataListeners = new Set<() => void>();
|
|
240
246
|
|
|
241
247
|
const storage = {
|
|
242
248
|
...core.storage,
|
|
@@ -244,6 +250,14 @@ function buildTestingModule() {
|
|
|
244
250
|
assertAccessControlLevel(level);
|
|
245
251
|
internals.setSecureDefaultAccessControl(level);
|
|
246
252
|
},
|
|
253
|
+
isProtectedDataAvailable: (): boolean => protectedDataAvailable,
|
|
254
|
+
onProtectedDataAvailable: (listener: () => void): (() => void) => {
|
|
255
|
+
assertProtectedDataListener(listener);
|
|
256
|
+
protectedDataListeners.add(listener);
|
|
257
|
+
return () => {
|
|
258
|
+
protectedDataListeners.delete(listener);
|
|
259
|
+
};
|
|
260
|
+
},
|
|
247
261
|
setSecureWritesAsync: (_enabled: boolean) => {},
|
|
248
262
|
setKeychainAccessGroup: (_group: string) => {},
|
|
249
263
|
getCapabilities: (): StorageCapabilities => ({
|
|
@@ -281,7 +295,24 @@ function buildTestingModule() {
|
|
|
281
295
|
}),
|
|
282
296
|
};
|
|
283
297
|
|
|
298
|
+
const setProtectedDataAvailable = (available: boolean): void => {
|
|
299
|
+
if (typeof available !== "boolean") {
|
|
300
|
+
throw new TypeError(
|
|
301
|
+
"NitroStorage: Protected data availability must be a boolean",
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
const wasAvailable = protectedDataAvailable;
|
|
305
|
+
protectedDataAvailable = available;
|
|
306
|
+
if (available && !wasAvailable) {
|
|
307
|
+
Array.from(protectedDataListeners).forEach((listener) => {
|
|
308
|
+
listener();
|
|
309
|
+
});
|
|
310
|
+
}
|
|
311
|
+
};
|
|
312
|
+
|
|
284
313
|
const reset = (): void => {
|
|
314
|
+
protectedDataAvailable = true;
|
|
315
|
+
protectedDataListeners.clear();
|
|
285
316
|
storage.setEventObserver(undefined);
|
|
286
317
|
storage.setScheduledFlushErrorObserver(undefined);
|
|
287
318
|
storage.setMetricsObserver(undefined);
|
|
@@ -303,8 +334,10 @@ function buildTestingModule() {
|
|
|
303
334
|
removeBatch: core.removeBatch,
|
|
304
335
|
registerMigration: core.registerMigration,
|
|
305
336
|
migrateToLatest: core.migrateToLatest,
|
|
337
|
+
migrateSecureAccessControl: core.migrateSecureAccessControl,
|
|
306
338
|
runTransaction: core.runTransaction,
|
|
307
339
|
createSecureAuthStorage: core.createSecureAuthStorage,
|
|
340
|
+
setProtectedDataAvailable,
|
|
308
341
|
reset,
|
|
309
342
|
};
|
|
310
343
|
}
|
|
@@ -324,8 +357,12 @@ export const setBatch = defaultModule.setBatch;
|
|
|
324
357
|
export const removeBatch = defaultModule.removeBatch;
|
|
325
358
|
export const registerMigration = defaultModule.registerMigration;
|
|
326
359
|
export const migrateToLatest = defaultModule.migrateToLatest;
|
|
360
|
+
export const migrateSecureAccessControl =
|
|
361
|
+
defaultModule.migrateSecureAccessControl;
|
|
327
362
|
export const runTransaction = defaultModule.runTransaction;
|
|
328
363
|
export const createSecureAuthStorage = defaultModule.createSecureAuthStorage;
|
|
364
|
+
export const setMockProtectedDataAvailable =
|
|
365
|
+
defaultModule.setProtectedDataAvailable;
|
|
329
366
|
|
|
330
367
|
export function setWebSecureStorageBackend(
|
|
331
368
|
_backend?: WebSecureStorageBackend,
|