react-native-nitro-storage 0.13.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 (69) hide show
  1. package/CHANGELOG.md +31 -0
  2. package/README.md +260 -7
  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 +68 -43
  7. package/docs/batch-transactions-migrations.md +7 -0
  8. package/docs/qa/agent-device-replay.md +106 -0
  9. package/docs/secure-storage.md +12 -6
  10. package/ios/IOSStorageAdapterCpp.hpp +12 -0
  11. package/ios/IOSStorageAdapterCpp.mm +180 -2
  12. package/lib/commonjs/Storage.types.js +4 -2
  13. package/lib/commonjs/Storage.types.js.map +1 -1
  14. package/lib/commonjs/core/durability.js +30 -2
  15. package/lib/commonjs/core/durability.js.map +1 -1
  16. package/lib/commonjs/index.js +16 -1
  17. package/lib/commonjs/index.js.map +1 -1
  18. package/lib/commonjs/index.web.js +10 -1
  19. package/lib/commonjs/index.web.js.map +1 -1
  20. package/lib/commonjs/shared.js +6 -0
  21. package/lib/commonjs/shared.js.map +1 -1
  22. package/lib/commonjs/storage-core.js +116 -3
  23. package/lib/commonjs/storage-core.js.map +1 -1
  24. package/lib/commonjs/testing.js +32 -2
  25. package/lib/commonjs/testing.js.map +1 -1
  26. package/lib/module/Storage.types.js +4 -2
  27. package/lib/module/Storage.types.js.map +1 -1
  28. package/lib/module/core/durability.js +30 -2
  29. package/lib/module/core/durability.js.map +1 -1
  30. package/lib/module/index.js +16 -1
  31. package/lib/module/index.js.map +1 -1
  32. package/lib/module/index.web.js +10 -1
  33. package/lib/module/index.web.js.map +1 -1
  34. package/lib/module/shared.js +5 -0
  35. package/lib/module/shared.js.map +1 -1
  36. package/lib/module/storage-core.js +117 -4
  37. package/lib/module/storage-core.js.map +1 -1
  38. package/lib/module/testing.js +31 -1
  39. package/lib/module/testing.js.map +1 -1
  40. package/lib/typescript/Storage.nitro.d.ts +2 -0
  41. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  42. package/lib/typescript/Storage.types.d.ts +4 -2
  43. package/lib/typescript/Storage.types.d.ts.map +1 -1
  44. package/lib/typescript/core/durability.d.ts +7 -1
  45. package/lib/typescript/core/durability.d.ts.map +1 -1
  46. package/lib/typescript/index.d.ts +8 -3
  47. package/lib/typescript/index.d.ts.map +1 -1
  48. package/lib/typescript/index.web.d.ts +8 -3
  49. package/lib/typescript/index.web.d.ts.map +1 -1
  50. package/lib/typescript/shared.d.ts +9 -1
  51. package/lib/typescript/shared.d.ts.map +1 -1
  52. package/lib/typescript/storage-core.d.ts +34 -4
  53. package/lib/typescript/storage-core.d.ts.map +1 -1
  54. package/lib/typescript/storage-platform.d.ts +2 -0
  55. package/lib/typescript/storage-platform.d.ts.map +1 -1
  56. package/lib/typescript/testing.d.ts +16 -5
  57. package/lib/typescript/testing.d.ts.map +1 -1
  58. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
  59. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
  60. package/package.json +2 -2
  61. package/src/Storage.nitro.ts +2 -0
  62. package/src/Storage.types.ts +4 -2
  63. package/src/core/durability.ts +46 -2
  64. package/src/index.ts +24 -0
  65. package/src/index.web.ts +18 -0
  66. package/src/shared.ts +27 -1
  67. package/src/storage-core.ts +182 -8
  68. package/src/storage-platform.ts +2 -0
  69. package/src/testing.ts +42 -0
@@ -26,4 +26,6 @@ export interface Storage extends HybridObject<{ ios: "c++"; android: "c++" }> {
26
26
  deleteSecureBiometric(key: string): void;
27
27
  hasSecureBiometric(key: string): boolean;
28
28
  clearSecureBiometric(): void;
29
+ isProtectedDataAvailable(): boolean;
30
+ onProtectedDataAvailable(listener: () => void): () => void;
29
31
  }
@@ -20,8 +20,10 @@ export enum AccessControl {
20
20
  export enum BiometricLevel {
21
21
  /** No biometric requirement (default). */
22
22
  None = 0,
23
- /** Require biometric or passcode for each access. */
23
+ /** iOS: Keychain user presence. Android: recent authentication when opening
24
+ * the protected store; later access uses its cached keyset. Web: no biometric enforcement. */
24
25
  BiometryOrPasscode = 1,
25
- /** Require biometric only (no passcode fallback). */
26
+ /** Biometric-only Keychain/Keystore policy, without passcode fallback.
27
+ * Android authenticates when opening the store, not each access. Web: no biometric enforcement. */
26
28
  BiometryOnly = 2,
27
29
  }
@@ -2,6 +2,15 @@ import { runMicrotask } from "../shared";
2
2
  import { StorageScope, type AccessControl } from "../Storage.types";
3
3
  import type { PendingDiskWrite, PendingSecureWrite } from "../shared";
4
4
 
5
+ export type StorageScheduledFlushError = {
6
+ readonly scope: StorageScope.Disk | StorageScope.Secure;
7
+ readonly error: unknown;
8
+ };
9
+
10
+ export type StorageScheduledFlushErrorObserver = (
11
+ error: StorageScheduledFlushError,
12
+ ) => void;
13
+
5
14
  export type DurabilityBackend = {
6
15
  setBatch(keys: string[], values: string[], scope: number): void;
7
16
  removeBatch(keys: string[], scope: number): void;
@@ -9,6 +18,9 @@ export type DurabilityBackend = {
9
18
  };
10
19
 
11
20
  export type DurabilityCoordinator = {
21
+ setScheduledFlushErrorObserver(
22
+ observer: StorageScheduledFlushErrorObserver | undefined,
23
+ ): void;
12
24
  setDiskWritesAsync(enabled: boolean): void;
13
25
  isDiskWritesAsync(): boolean;
14
26
  hasPendingDiskWrite(key: string): boolean;
@@ -42,6 +54,8 @@ export function createDurabilityCoordinator(options: {
42
54
  let nextGeneration = 0;
43
55
  let diskFlushScheduled = false;
44
56
  let diskWritesAsync = false;
57
+ let scheduledFlushErrorObserver:
58
+ StorageScheduledFlushErrorObserver | undefined;
45
59
  const pendingSecureWrites = new Map<string, PendingSecureWrite>();
46
60
  let secureFlushScheduled = false;
47
61
  const securePromotionsInProgress = new Set<string>();
@@ -168,6 +182,33 @@ export function createDurabilityCoordinator(options: {
168
182
  }
169
183
  }
170
184
 
185
+ function reportScheduledFlushError(
186
+ scope: StorageScheduledFlushError["scope"],
187
+ error: unknown,
188
+ ): void {
189
+ const observer = scheduledFlushErrorObserver;
190
+ if (observer === undefined) {
191
+ throw error;
192
+ }
193
+ observer({ scope, error });
194
+ }
195
+
196
+ function flushScheduledDiskWrites(): void {
197
+ try {
198
+ flushDiskWrites();
199
+ } catch (error) {
200
+ reportScheduledFlushError(StorageScope.Disk, error);
201
+ }
202
+ }
203
+
204
+ function flushScheduledSecureWrites(): void {
205
+ try {
206
+ flushSecureWrites();
207
+ } catch (error) {
208
+ reportScheduledFlushError(StorageScope.Secure, error);
209
+ }
210
+ }
211
+
171
212
  function scheduleDiskWrite(
172
213
  key: string,
173
214
  value: string | undefined,
@@ -182,7 +223,7 @@ export function createDurabilityCoordinator(options: {
182
223
  return pendingWrite;
183
224
  }
184
225
  diskFlushScheduled = true;
185
- runMicrotask(flushDiskWrites);
226
+ runMicrotask(flushScheduledDiskWrites);
186
227
  return pendingWrite;
187
228
  }
188
229
 
@@ -204,7 +245,7 @@ export function createDurabilityCoordinator(options: {
204
245
  return pendingWrite;
205
246
  }
206
247
  secureFlushScheduled = true;
207
- runMicrotask(flushSecureWrites);
248
+ runMicrotask(flushScheduledSecureWrites);
208
249
  return pendingWrite;
209
250
  }
210
251
 
@@ -225,6 +266,9 @@ export function createDurabilityCoordinator(options: {
225
266
  }
226
267
 
227
268
  return {
269
+ setScheduledFlushErrorObserver(observer) {
270
+ scheduledFlushErrorObserver = observer;
271
+ },
228
272
  setDiskWritesAsync(enabled) {
229
273
  diskWritesAsync = enabled;
230
274
  if (!enabled) {
package/src/index.ts CHANGED
@@ -8,6 +8,7 @@ import { createIndexedDBBackend as createIndexedDBBackendFromSubpath } from "./i
8
8
  import { unescapeCollidingRawValue } from "./internal";
9
9
  import {
10
10
  assertAccessControlLevel,
11
+ assertProtectedDataListener,
11
12
  notifyAllListeners,
12
13
  notifyKeyListeners,
13
14
  type NonMemoryScope,
@@ -49,6 +50,10 @@ export type {
49
50
  VersionedValue,
50
51
  } from "./shared";
51
52
  export { isKeychainLockedError } from "./shared";
53
+ export type {
54
+ StorageScheduledFlushError,
55
+ StorageScheduledFlushErrorObserver,
56
+ } from "./core/durability";
52
57
 
53
58
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
54
59
  export type { Storage } from "./Storage.nitro";
@@ -78,6 +83,8 @@ export type {
78
83
  WebStorageScope,
79
84
  } from "./web-storage-backend";
80
85
  export type {
86
+ SecureAccessControlMigrationOptions,
87
+ SecureAccessControlMigrationResult,
81
88
  SetItemConfig,
82
89
  SetStorageItem,
83
90
  StorageBatchSetItem,
@@ -260,6 +267,7 @@ function buildNativeAdapter(
260
267
  backend: nativeBackend,
261
268
  changeSource: "native",
262
269
  applyAccessControlOnSecureRawWrite: true,
270
+ supportsSecureAccessControlMigration: () => Platform.OS === "ios",
263
271
  ensureScopeSubscription: ensureNativeScopeSubscription,
264
272
  maybeCleanupScopeSubscription: maybeCleanupNativeScopeSubscription,
265
273
  onWillEmitChanges: (scope, keys, operation, source) => {
@@ -300,6 +308,21 @@ export const storage = {
300
308
  },
301
309
  );
302
310
  },
311
+ isProtectedDataAvailable: (): boolean => {
312
+ const module = getStorageModule();
313
+ return typeof module.isProtectedDataAvailable === "function"
314
+ ? module.isProtectedDataAvailable()
315
+ : true;
316
+ },
317
+ onProtectedDataAvailable: (listener: () => void): (() => void) => {
318
+ assertProtectedDataListener(listener);
319
+ const module = getStorageModule();
320
+ if (typeof module.onProtectedDataAvailable !== "function") {
321
+ return () => {};
322
+ }
323
+ const unsubscribe = module.onProtectedDataAvailable(listener);
324
+ return typeof unsubscribe === "function" ? unsubscribe : () => {};
325
+ },
303
326
  setSecureWritesAsync: (enabled: boolean) => {
304
327
  internals.measureOperation(
305
328
  "storage:setSecureWritesAsync",
@@ -364,6 +387,7 @@ export const setBatch = core.setBatch;
364
387
  export const removeBatch = core.removeBatch;
365
388
  export const registerMigration = core.registerMigration;
366
389
  export const migrateToLatest = core.migrateToLatest;
390
+ export const migrateSecureAccessControl = core.migrateSecureAccessControl;
367
391
  export const runTransaction = core.runTransaction;
368
392
  export const createSecureAuthStorage = core.createSecureAuthStorage;
369
393
 
package/src/index.web.ts CHANGED
@@ -3,6 +3,7 @@ import { createIndexedDBBackend as createIndexedDBBackendFromSubpath } from "./i
3
3
  import { unescapeCollidingRawValue } from "./internal";
4
4
  import {
5
5
  assertAccessControlLevel,
6
+ assertProtectedDataListener,
6
7
  createStorageCompositeError,
7
8
  assertBiometricLevel,
8
9
  notifyAllListeners,
@@ -48,6 +49,10 @@ export type {
48
49
  VersionedValue,
49
50
  } from "./shared";
50
51
  export { isKeychainLockedError } from "./shared";
52
+ export type {
53
+ StorageScheduledFlushError,
54
+ StorageScheduledFlushErrorObserver,
55
+ } from "./core/durability";
51
56
 
52
57
  export { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
53
58
  export type { Storage } from "./Storage.nitro";
@@ -82,6 +87,8 @@ export {
82
87
  type WebBackendCapabilities,
83
88
  } from "./web-backend-contract";
84
89
  export type {
90
+ SecureAccessControlMigrationOptions,
91
+ SecureAccessControlMigrationResult,
85
92
  SetItemConfig,
86
93
  SetStorageItem,
87
94
  StorageBatchSetItem,
@@ -807,6 +814,11 @@ const WebStorage: Storage = {
807
814
  },
808
815
  setSecureWritesAsync: (_enabled: boolean) => {},
809
816
  setKeychainAccessGroup: () => {},
817
+ isProtectedDataAvailable: () => true,
818
+ onProtectedDataAvailable: (listener: () => void) => {
819
+ assertProtectedDataListener(listener);
820
+ return () => {};
821
+ },
810
822
  setSecureBiometric: (key: string, value: string) => {
811
823
  WebStorage.setSecureBiometricWithLevel(
812
824
  key,
@@ -1019,6 +1031,7 @@ function buildWebAdapter(
1019
1031
  backend: WebStorage,
1020
1032
  changeSource: "web",
1021
1033
  applyAccessControlOnSecureRawWrite: false,
1034
+ supportsSecureAccessControlMigration: () => false,
1022
1035
  ensureScopeSubscription: () => {
1023
1036
  ensureExternalSyncSubscriptions();
1024
1037
  },
@@ -1045,6 +1058,10 @@ export const storage = {
1045
1058
  0,
1046
1059
  );
1047
1060
  },
1061
+ isProtectedDataAvailable: (): boolean =>
1062
+ WebStorage.isProtectedDataAvailable(),
1063
+ onProtectedDataAvailable: (listener: () => void): (() => void) =>
1064
+ WebStorage.onProtectedDataAvailable(listener),
1048
1065
  setSecureWritesAsync: (_enabled: boolean) => {
1049
1066
  getInternals().recordMetric(
1050
1067
  "storage:setSecureWritesAsync",
@@ -1110,6 +1127,7 @@ export const setBatch = core.setBatch;
1110
1127
  export const removeBatch = core.removeBatch;
1111
1128
  export const registerMigration = core.registerMigration;
1112
1129
  export const migrateToLatest = core.migrateToLatest;
1130
+ export const migrateSecureAccessControl = core.migrateSecureAccessControl;
1113
1131
  export const runTransaction = core.runTransaction;
1114
1132
  export const createSecureAuthStorage = core.createSecureAuthStorage;
1115
1133
 
package/src/shared.ts CHANGED
@@ -75,7 +75,23 @@ export type MigrationContext = {
75
75
  removeRaw: (key: string) => void;
76
76
  };
77
77
 
78
- export type Migration = (context: MigrationContext) => void;
78
+ type CallableThenableResult<TResult> = TResult extends {
79
+ then?: infer Then;
80
+ }
81
+ ? Extract<Then, (...args: never[]) => unknown> extends never
82
+ ? never
83
+ : TResult
84
+ : never;
85
+
86
+ export type SynchronousCallbackResult<TResult> = [
87
+ CallableThenableResult<TResult>,
88
+ ] extends [never]
89
+ ? TResult
90
+ : never;
91
+
92
+ export type Migration<TResult = unknown> = (
93
+ context: MigrationContext,
94
+ ) => SynchronousCallbackResult<TResult>;
79
95
 
80
96
  export type KeyListenerRegistry = Map<string, Set<() => void>>;
81
97
 
@@ -212,6 +228,16 @@ export function assertBiometricLevel(level: number): void {
212
228
  assertEnumInteger(level, 0, 2, "biometric level");
213
229
  }
214
230
 
231
+ export function assertProtectedDataListener(
232
+ listener: unknown,
233
+ ): asserts listener is () => void {
234
+ if (typeof listener !== "function") {
235
+ throw new TypeError(
236
+ "NitroStorage: Protected data listener must be a function",
237
+ );
238
+ }
239
+ }
240
+
215
241
  export type NonMemoryScope = StorageScope.Disk | StorageScope.Secure;
216
242
 
217
243
  export type PendingDiskWrite = {
@@ -1,4 +1,7 @@
1
- import { createDurabilityCoordinator } from "./core/durability";
1
+ import {
2
+ createDurabilityCoordinator,
3
+ type StorageScheduledFlushErrorObserver,
4
+ } from "./core/durability";
2
5
  import { createMetricsRegistry } from "./core/metrics";
3
6
  import {
4
7
  MIGRATION_VERSION_KEY,
@@ -44,6 +47,7 @@ import {
44
47
  type StorageMetricsObserver,
45
48
  type StorageSelectorListener,
46
49
  type StorageSelectorSubscribeOptions,
50
+ type SynchronousCallbackResult,
47
51
  type StorageVersion,
48
52
  type Validator,
49
53
  type VersionedValue,
@@ -58,7 +62,12 @@ import {
58
62
  type StorageKeyChangeEvent,
59
63
  } from "./storage-events";
60
64
  import type { StorageSetter } from "./storage-hooks";
61
- import { isStorageError, type SecureStorageMetadata } from "./storage-runtime";
65
+ import {
66
+ getStorageErrorCode,
67
+ isStorageError,
68
+ type SecureStorageMetadata,
69
+ type StorageErrorCode,
70
+ } from "./storage-runtime";
62
71
  import { StorageScope, AccessControl, BiometricLevel } from "./Storage.types";
63
72
 
64
73
  export type TransactionContext = {
@@ -76,6 +85,28 @@ export type TransactionContext = {
76
85
  ) => void;
77
86
  };
78
87
 
88
+ function assertSynchronousCallbackResult(
89
+ value: unknown,
90
+ callbackKind: "Transaction" | "Migration",
91
+ ): void {
92
+ if (
93
+ value === null ||
94
+ (typeof value !== "object" && typeof value !== "function")
95
+ ) {
96
+ return;
97
+ }
98
+
99
+ const then = (value as { then?: unknown }).then;
100
+ if (typeof then !== "function") {
101
+ return;
102
+ }
103
+
104
+ void Promise.resolve(value).catch(() => undefined);
105
+ throw new TypeError(
106
+ `${callbackKind} callbacks must be synchronous and cannot return a Promise or thenable.`,
107
+ );
108
+ }
109
+
79
110
  export type StorageItemConfig<T> = {
80
111
  key: string;
81
112
  scope: StorageScope;
@@ -191,6 +222,30 @@ export type StorageClearOptions = {
191
222
  except?: readonly StorageKeyRef[];
192
223
  };
193
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
+
194
249
  export type SetItemConfig<TMember extends string = string> = Omit<
195
250
  StorageItemConfig<Record<string, true>>,
196
251
  "defaultValue" | "serialize" | "deserialize"
@@ -248,6 +303,7 @@ export type StorageCoreAdapter = {
248
303
  backend: StorageCoreBackend;
249
304
  changeSource: StorageChangeSource;
250
305
  applyAccessControlOnSecureRawWrite: boolean;
306
+ supportsSecureAccessControlMigration(): boolean;
251
307
  ensureScopeSubscription(scope: NonMemoryScope): void;
252
308
  maybeCleanupScopeSubscription(scope: NonMemoryScope): void;
253
309
  onWillEmitChanges(
@@ -318,7 +374,7 @@ function asInternal<T>(item: StorageItem<T>): StorageItemInternal<T> {
318
374
  export function createStorageCore(
319
375
  buildAdapter: (internals: StorageCoreInternals) => StorageCoreAdapter,
320
376
  ) {
321
- const registeredMigrations = new Map<number, Migration>();
377
+ const registeredMigrations = new Map<number, Migration<unknown>>();
322
378
  const itemGroups = new Map<string, Set<StorageItemInternal<unknown>>>();
323
379
  const registeredKeyCounts = new Map<string, number>();
324
380
  const memoryStore = new Map<string, unknown>();
@@ -1485,6 +1541,12 @@ export function createStorageCore(
1485
1541
  flushSecureWrites();
1486
1542
  });
1487
1543
  },
1544
+ /** Observes scheduled flush failures; explicit flush calls still throw. */
1545
+ setScheduledFlushErrorObserver: (
1546
+ observer?: StorageScheduledFlushErrorObserver,
1547
+ ): void => {
1548
+ durability.setScheduledFlushErrorObserver(observer);
1549
+ },
1488
1550
  setMetricsObserver: (observer?: StorageMetricsObserver) => {
1489
1551
  metrics.setObserver(observer);
1490
1552
  },
@@ -3320,7 +3382,10 @@ export function createStorageCore(
3320
3382
  );
3321
3383
  }
3322
3384
 
3323
- function registerMigration(version: number, migration: Migration): void {
3385
+ function registerMigration<TResult>(
3386
+ version: number,
3387
+ migration: Migration<TResult>,
3388
+ ): void {
3324
3389
  if (!Number.isInteger(version) || version <= 0) {
3325
3390
  throw new Error("Migration version must be a positive integer.");
3326
3391
  }
@@ -3332,6 +3397,95 @@ export function createStorageCore(
3332
3397
  registeredMigrations.set(version, migration);
3333
3398
  }
3334
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
+
3335
3489
  function migrateToLatest(scope: StorageScope = StorageScope.Disk): number {
3336
3490
  return measureOperation("migration:run", scope, () => {
3337
3491
  assertValidScope(scope);
@@ -3348,7 +3502,8 @@ export function createStorageCore(
3348
3502
  return;
3349
3503
  }
3350
3504
  runTransaction(scope, (tx) => {
3351
- migration(tx);
3505
+ const migrationResult: unknown = migration(tx);
3506
+ assertSynchronousCallbackResult(migrationResult, "Migration");
3352
3507
  tx.setRaw(MIGRATION_VERSION_KEY, String(version));
3353
3508
  });
3354
3509
  appliedVersion = version;
@@ -3360,7 +3515,7 @@ export function createStorageCore(
3360
3515
 
3361
3516
  function runTransaction<T>(
3362
3517
  scope: StorageScope,
3363
- transaction: (context: TransactionContext) => T,
3518
+ transaction: (context: TransactionContext) => SynchronousCallbackResult<T>,
3364
3519
  ): T {
3365
3520
  return measureOperation("transaction:run", scope, () => {
3366
3521
  assertValidScope(scope);
@@ -3379,6 +3534,12 @@ export function createStorageCore(
3379
3534
  };
3380
3535
  const rollback = new Map<string, TransactionRollbackEntry>();
3381
3536
  const itemRenameStates = new Map<StorageItemInternal<unknown>, boolean>();
3537
+ let transactionOpen = true;
3538
+ const assertTransactionOpen = (): void => {
3539
+ if (!transactionOpen) {
3540
+ throw new TypeError("Transaction context is closed.");
3541
+ }
3542
+ };
3382
3543
 
3383
3544
  const rememberRollback = (
3384
3545
  key: string,
@@ -3486,26 +3647,34 @@ export function createStorageCore(
3486
3647
 
3487
3648
  const tx: TransactionContext = {
3488
3649
  scope,
3489
- getRaw: (key) => getRawValue(key, scope),
3650
+ getRaw: (key) => {
3651
+ assertTransactionOpen();
3652
+ return getRawValue(key, scope);
3653
+ },
3490
3654
  setRaw: (key, value) => {
3655
+ assertTransactionOpen();
3491
3656
  rememberRollback(key);
3492
3657
  setRawValue(key, value, scope);
3493
3658
  },
3494
3659
  removeRaw: (key) => {
3660
+ assertTransactionOpen();
3495
3661
  rememberRollback(key, undefined, scope === StorageScope.Secure);
3496
3662
  removeRawValue(key, scope);
3497
3663
  },
3498
3664
  getItem: (item) => {
3665
+ assertTransactionOpen();
3499
3666
  assertBatchScope([item], scope);
3500
3667
  rememberItemRollback(item);
3501
3668
  return item.get();
3502
3669
  },
3503
3670
  setItem: (item, value) => {
3671
+ assertTransactionOpen();
3504
3672
  assertBatchScope([item], scope);
3505
3673
  rememberItemRollback(item);
3506
3674
  item.set(value);
3507
3675
  },
3508
3676
  removeItem: (item) => {
3677
+ assertTransactionOpen();
3509
3678
  assertBatchScope([item], scope);
3510
3679
  rememberItemRollback(item);
3511
3680
  item.delete();
@@ -3513,8 +3682,12 @@ export function createStorageCore(
3513
3682
  };
3514
3683
 
3515
3684
  try {
3516
- return transaction(tx);
3685
+ const result = transaction(tx);
3686
+ assertSynchronousCallbackResult(result, "Transaction");
3687
+ transactionOpen = false;
3688
+ return result;
3517
3689
  } catch (error) {
3690
+ transactionOpen = false;
3518
3691
  itemRenameStates.forEach((migrated, item) => {
3519
3692
  item._setRenameMigrationState(migrated);
3520
3693
  });
@@ -3897,6 +4070,7 @@ export function createStorageCore(
3897
4070
  removeBatch,
3898
4071
  registerMigration,
3899
4072
  migrateToLatest,
4073
+ migrateSecureAccessControl,
3900
4074
  runTransaction,
3901
4075
  createSecureAuthStorage,
3902
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;