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.
Files changed (53) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/README.md +203 -4
  3. package/cpp/bindings/HybridStorage.cpp +21 -0
  4. package/cpp/bindings/HybridStorage.hpp +2 -0
  5. package/cpp/core/NativeStorageAdapter.hpp +6 -0
  6. package/docs/api-reference.md +63 -44
  7. package/docs/qa/agent-device-replay.md +41 -5
  8. package/ios/IOSStorageAdapterCpp.hpp +12 -0
  9. package/ios/IOSStorageAdapterCpp.mm +180 -2
  10. package/lib/commonjs/index.js +16 -1
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/index.web.js +10 -1
  13. package/lib/commonjs/index.web.js.map +1 -1
  14. package/lib/commonjs/shared.js +6 -0
  15. package/lib/commonjs/shared.js.map +1 -1
  16. package/lib/commonjs/storage-core.js +79 -0
  17. package/lib/commonjs/storage-core.js.map +1 -1
  18. package/lib/commonjs/testing.js +31 -2
  19. package/lib/commonjs/testing.js.map +1 -1
  20. package/lib/module/index.js +16 -1
  21. package/lib/module/index.js.map +1 -1
  22. package/lib/module/index.web.js +10 -1
  23. package/lib/module/index.web.js.map +1 -1
  24. package/lib/module/shared.js +5 -0
  25. package/lib/module/shared.js.map +1 -1
  26. package/lib/module/storage-core.js +80 -1
  27. package/lib/module/storage-core.js.map +1 -1
  28. package/lib/module/testing.js +30 -1
  29. package/lib/module/testing.js.map +1 -1
  30. package/lib/typescript/Storage.nitro.d.ts +2 -0
  31. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  32. package/lib/typescript/index.d.ts +4 -1
  33. package/lib/typescript/index.d.ts.map +1 -1
  34. package/lib/typescript/index.web.d.ts +4 -1
  35. package/lib/typescript/index.web.d.ts.map +1 -1
  36. package/lib/typescript/shared.d.ts +1 -0
  37. package/lib/typescript/shared.d.ts.map +1 -1
  38. package/lib/typescript/storage-core.d.ts +28 -1
  39. package/lib/typescript/storage-core.d.ts.map +1 -1
  40. package/lib/typescript/storage-platform.d.ts +2 -0
  41. package/lib/typescript/storage-platform.d.ts.map +1 -1
  42. package/lib/typescript/testing.d.ts +9 -1
  43. package/lib/typescript/testing.d.ts.map +1 -1
  44. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
  45. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
  46. package/package.json +2 -2
  47. package/src/Storage.nitro.ts +2 -0
  48. package/src/index.ts +20 -0
  49. package/src/index.web.ts +14 -0
  50. package/src/shared.ts +10 -0
  51. package/src/storage-core.ts +121 -1
  52. package/src/storage-platform.ts +2 -0
  53. package/src/testing.ts +37 -0
@@ -62,7 +62,12 @@ import {
62
62
  type StorageKeyChangeEvent,
63
63
  } from "./storage-events";
64
64
  import type { StorageSetter } from "./storage-hooks";
65
- import { isStorageError, type SecureStorageMetadata } from "./storage-runtime";
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,
@@ -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,