react-native-nitro-storage 0.5.3 → 0.5.5

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 (36) hide show
  1. package/.watchmanconfig +6 -0
  2. package/README.md +45 -5
  3. package/android/build.gradle +5 -5
  4. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +12 -25
  5. package/app.plugin.js +114 -9
  6. package/docs/api-reference.md +39 -36
  7. package/docs/batch-transactions-migrations.md +1 -1
  8. package/docs/recipes.md +1 -1
  9. package/docs/secure-storage.md +15 -4
  10. package/docs/web-backends.md +5 -0
  11. package/lib/commonjs/index.js +129 -27
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/index.web.js +169 -32
  14. package/lib/commonjs/index.web.js.map +1 -1
  15. package/lib/commonjs/indexeddb-backend.js +28 -0
  16. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  17. package/lib/commonjs/web-storage-backend.js.map +1 -1
  18. package/lib/module/index.js +129 -27
  19. package/lib/module/index.js.map +1 -1
  20. package/lib/module/index.web.js +169 -32
  21. package/lib/module/index.web.js.map +1 -1
  22. package/lib/module/indexeddb-backend.js +28 -0
  23. package/lib/module/indexeddb-backend.js.map +1 -1
  24. package/lib/module/web-storage-backend.js.map +1 -1
  25. package/lib/typescript/index.d.ts +10 -3
  26. package/lib/typescript/index.d.ts.map +1 -1
  27. package/lib/typescript/index.web.d.ts +10 -3
  28. package/lib/typescript/index.web.d.ts.map +1 -1
  29. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  30. package/lib/typescript/web-storage-backend.d.ts +1 -0
  31. package/lib/typescript/web-storage-backend.d.ts.map +1 -1
  32. package/package.json +5 -3
  33. package/src/index.ts +197 -32
  34. package/src/index.web.ts +250 -37
  35. package/src/indexeddb-backend.ts +30 -0
  36. package/src/web-storage-backend.ts +1 -0
@@ -17,6 +17,20 @@ function isUpdater(valueOrFn) {
17
17
  function typedKeys(record) {
18
18
  return Object.keys(record);
19
19
  }
20
+ function assertEnumInteger(value, min, max, label) {
21
+ if (!Number.isFinite(value) || value < min || value > max) {
22
+ throw new Error(`NitroStorage: Invalid ${label}`);
23
+ }
24
+ if (value !== Math.trunc(value)) {
25
+ throw new Error(`NitroStorage: Invalid ${label}`);
26
+ }
27
+ }
28
+ function assertAccessControlLevel(level) {
29
+ assertEnumInteger(level, 0, 4, "access control level");
30
+ }
31
+ function assertBiometricLevel(level) {
32
+ assertEnumInteger(level, 0, 2, "biometric level");
33
+ }
20
34
  const registeredMigrations = new Map();
21
35
  const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
22
36
  Promise.resolve().then(task);
@@ -40,6 +54,7 @@ let hasWarnedAboutWebBiometricFallback = false;
40
54
  let hasWindowStorageEventSubscription = false;
41
55
  let metricsObserver;
42
56
  let eventObserver;
57
+ let eventObserverRedactSecureValues = true;
43
58
  const metricsCounters = new Map();
44
59
  const storageEvents = new StorageEventRegistry();
45
60
  function recordMetric(operation, scope, durationMs, keysCount = 1) {
@@ -235,6 +250,16 @@ function resetBackendChangeSubscription(scope) {
235
250
  externalSyncUnsubscribers.get(scope)?.();
236
251
  externalSyncUnsubscribers.delete(scope);
237
252
  }
253
+ function closeWebBackend(scope, backend) {
254
+ if (!backend?.close) {
255
+ return;
256
+ }
257
+ try {
258
+ backend.close();
259
+ } catch (error) {
260
+ throw createWebStorageError(scope, "close", error, backend);
261
+ }
262
+ }
238
263
  function ensureExternalSyncSubscriptions() {
239
264
  if (!hasWindowStorageEventSubscription && typeof window !== "undefined" && typeof window.addEventListener === "function") {
240
265
  window.addEventListener("storage", handleWebStorageEvent);
@@ -315,10 +340,42 @@ function createKeyChange(scope, key, oldValue, newValue, operation, source) {
315
340
  function hasStorageChangeObservers(scope) {
316
341
  return storageEvents.hasListeners(scope) || eventObserver !== undefined;
317
342
  }
343
+ function shouldReadPreviousEventValues(scope) {
344
+ if (storageEvents.hasListeners(scope)) {
345
+ return true;
346
+ }
347
+ if (!eventObserver) {
348
+ return false;
349
+ }
350
+ return scope !== StorageScope.Secure || !eventObserverRedactSecureValues;
351
+ }
352
+ const SECURE_EVENT_REDACTED_VALUE = "[secure]";
353
+ function redactSecureKeyChange(event) {
354
+ if (event.scope !== StorageScope.Secure) {
355
+ return event;
356
+ }
357
+ return {
358
+ ...event,
359
+ oldValue: event.oldValue === undefined ? undefined : SECURE_EVENT_REDACTED_VALUE,
360
+ newValue: event.newValue === undefined ? undefined : SECURE_EVENT_REDACTED_VALUE
361
+ };
362
+ }
363
+ function eventForGlobalObserver(event) {
364
+ if (!eventObserverRedactSecureValues || event.scope !== StorageScope.Secure) {
365
+ return event;
366
+ }
367
+ if (event.type === "key") {
368
+ return redactSecureKeyChange(event);
369
+ }
370
+ return {
371
+ ...event,
372
+ changes: event.changes.map(redactSecureKeyChange)
373
+ };
374
+ }
318
375
  function emitKeyChange(scope, key, oldValue, newValue, operation, source) {
319
376
  const event = createKeyChange(scope, key, oldValue, newValue, operation, source);
320
377
  storageEvents.emitKey(event);
321
- eventObserver?.(event);
378
+ eventObserver?.(eventForGlobalObserver(event));
322
379
  }
323
380
  function emitBatchChange(scope, operation, source, changes) {
324
381
  if (changes.length === 0) {
@@ -332,7 +389,7 @@ function emitBatchChange(scope, operation, source, changes) {
332
389
  changes
333
390
  };
334
391
  storageEvents.emitBatch(event);
335
- eventObserver?.(event);
392
+ eventObserver?.(eventForGlobalObserver(event));
336
393
  }
337
394
  function readPendingSecureWrite(key) {
338
395
  return pendingSecureWrites.get(key)?.value;
@@ -502,6 +559,9 @@ const WebStorage = {
502
559
  if (scope !== StorageScope.Disk && scope !== StorageScope.Secure) {
503
560
  return;
504
561
  }
562
+ if (keys.length !== values.length) {
563
+ throw new Error("NitroStorage: Keys and values size mismatch in setBatch");
564
+ }
505
565
  const entries = [];
506
566
  keys.forEach((key, index) => {
507
567
  const value = values[index];
@@ -520,7 +580,7 @@ const WebStorage = {
520
580
  });
521
581
  });
522
582
  const keyIndex = ensureWebScopeKeyIndex(scope);
523
- keys.forEach(key => keyIndex.add(key));
583
+ entries.forEach(([storageKey]) => keyIndex.add(scope === StorageScope.Secure ? storageKey.slice(SECURE_WEB_PREFIX.length) : storageKey));
524
584
  const listeners = getScopedListeners(scope);
525
585
  keys.forEach(key => notifyKeyListeners(listeners, key));
526
586
  },
@@ -606,13 +666,25 @@ const WebStorage = {
606
666
  }
607
667
  return 0;
608
668
  },
609
- setSecureAccessControl: () => {},
669
+ setSecureAccessControl: level => {
670
+ assertAccessControlLevel(level);
671
+ },
610
672
  setSecureWritesAsync: _enabled => {},
611
673
  setKeychainAccessGroup: () => {},
612
674
  setSecureBiometric: (key, value) => {
613
675
  WebStorage.setSecureBiometricWithLevel(key, value, BiometricLevel.BiometryOnly);
614
676
  },
615
- setSecureBiometricWithLevel: (key, value, _level) => {
677
+ setSecureBiometricWithLevel: (key, value, level) => {
678
+ assertBiometricLevel(level);
679
+ if (level === BiometricLevel.None) {
680
+ withWebBackendOperation(StorageScope.Secure, "setSecure", backend => {
681
+ backend.removeItem(toBiometricStorageKey(key));
682
+ backend.setItem(toSecureStorageKey(key), value);
683
+ });
684
+ ensureWebScopeKeyIndex(StorageScope.Secure).add(key);
685
+ notifyKeyListeners(getScopedListeners(StorageScope.Secure), key);
686
+ return;
687
+ }
616
688
  if (typeof __DEV__ !== "undefined" && __DEV__ && !hasWarnedAboutWebBiometricFallback) {
617
689
  hasWarnedAboutWebBiometricFallback = true;
618
690
  console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
@@ -765,15 +837,16 @@ export const storage = {
765
837
  subscribeNamespace: (namespace, scope, listener) => {
766
838
  return storage.subscribePrefix(scope, prefixKey(namespace, ""), listener);
767
839
  },
768
- setEventObserver: observer => {
840
+ setEventObserver: (observer, options = {}) => {
769
841
  eventObserver = observer;
842
+ eventObserverRedactSecureValues = options.redactSecureValues !== false;
770
843
  if (observer) {
771
844
  ensureExternalSyncSubscriptions();
772
845
  }
773
846
  },
774
847
  clear: scope => {
775
848
  measureOperation("storage:clear", scope, () => {
776
- const previousValues = hasStorageChangeObservers(scope) ? storage.getAll(scope) : {};
849
+ const previousValues = shouldReadPreviousEventValues(scope) ? storage.getAll(scope) : {};
777
850
  if (scope === StorageScope.Memory) {
778
851
  memoryStore.clear();
779
852
  notifyAllListeners(memoryListeners);
@@ -823,7 +896,7 @@ export const storage = {
823
896
  return;
824
897
  }
825
898
  const keyPrefix = prefixKey(namespace, "");
826
- const previousValues = hasStorageChangeObservers(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
899
+ const previousValues = shouldReadPreviousEventValues(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
827
900
  if (scope === StorageScope.Disk) {
828
901
  flushDiskWrites();
829
902
  }
@@ -946,9 +1019,15 @@ export const storage = {
946
1019
  return result;
947
1020
  });
948
1021
  },
949
- export: scope => {
1022
+ export: (scope, options = {}) => {
1023
+ if (scope === StorageScope.Secure && options.includeSecureValues !== true) {
1024
+ throw new Error("NitroStorage: exporting Secure scope exposes raw secret values. Pass { includeSecureValues: true } or use exportSecureUnsafe().");
1025
+ }
950
1026
  return measureOperation("storage:export", scope, () => storage.getAll(scope));
951
1027
  },
1028
+ exportSecureUnsafe: () => {
1029
+ return measureOperation("storage:exportSecureUnsafe", StorageScope.Secure, () => storage.getAll(StorageScope.Secure));
1030
+ },
952
1031
  size: scope => {
953
1032
  return measureOperation("storage:size", scope, () => {
954
1033
  assertValidScope(scope);
@@ -963,6 +1042,7 @@ export const storage = {
963
1042
  });
964
1043
  },
965
1044
  setAccessControl: level => {
1045
+ assertAccessControlLevel(level);
966
1046
  secureDefaultAccessControl = level;
967
1047
  recordMetric("storage:setAccessControl", StorageScope.Secure, 0);
968
1048
  },
@@ -1115,23 +1195,33 @@ export const storage = {
1115
1195
  }
1116
1196
  };
1117
1197
  export function setWebSecureStorageBackend(backend) {
1198
+ const previousBackend = webSecureStorageBackend;
1199
+ const nextBackend = backend ?? createDefaultSecureBackend();
1118
1200
  pendingSecureWrites.clear();
1119
- webSecureStorageBackend = backend ?? createDefaultSecureBackend();
1120
1201
  resetBackendChangeSubscription(StorageScope.Secure);
1202
+ webSecureStorageBackend = nextBackend;
1121
1203
  hydratedWebScopeKeyIndex.delete(StorageScope.Secure);
1122
1204
  clearScopeRawCache(StorageScope.Secure);
1123
1205
  ensureExternalSyncSubscriptions();
1206
+ if (previousBackend !== nextBackend) {
1207
+ closeWebBackend(StorageScope.Secure, previousBackend);
1208
+ }
1124
1209
  }
1125
1210
  export function getWebSecureStorageBackend() {
1126
1211
  return webSecureStorageBackend;
1127
1212
  }
1128
1213
  export function setWebDiskStorageBackend(backend) {
1214
+ const previousBackend = webDiskStorageBackend;
1215
+ const nextBackend = backend ?? createDefaultDiskBackend();
1129
1216
  pendingDiskWrites.clear();
1130
- webDiskStorageBackend = backend ?? createDefaultDiskBackend();
1131
1217
  resetBackendChangeSubscription(StorageScope.Disk);
1218
+ webDiskStorageBackend = nextBackend;
1132
1219
  hydratedWebScopeKeyIndex.delete(StorageScope.Disk);
1133
1220
  clearScopeRawCache(StorageScope.Disk);
1134
1221
  ensureExternalSyncSubscriptions();
1222
+ if (previousBackend !== nextBackend) {
1223
+ closeWebBackend(StorageScope.Disk, previousBackend);
1224
+ }
1135
1225
  }
1136
1226
  export function getWebDiskStorageBackend() {
1137
1227
  return webDiskStorageBackend;
@@ -1184,6 +1274,12 @@ export function createStorageItem(config) {
1184
1274
  if (expiration && expiration.ttlMs <= 0) {
1185
1275
  throw new Error("expiration.ttlMs must be greater than 0.");
1186
1276
  }
1277
+ if (config.scope === StorageScope.Secure) {
1278
+ assertBiometricLevel(resolvedBiometricLevel);
1279
+ if (secureAccessControl !== undefined) {
1280
+ assertAccessControlLevel(secureAccessControl);
1281
+ }
1282
+ }
1187
1283
  const listeners = new Set();
1188
1284
  let unsubscribe = null;
1189
1285
  let lastRaw = undefined;
@@ -1523,6 +1619,7 @@ export function createStorageItem(config) {
1523
1619
  _hasExpiration: expiration !== undefined,
1524
1620
  _readCacheEnabled: readCache,
1525
1621
  _isBiometric: isBiometric,
1622
+ _biometricLevel: resolvedBiometricLevel,
1526
1623
  _defaultValue: defaultValue,
1527
1624
  ...(secureAccessControl !== undefined ? {
1528
1625
  _secureAccessControl: secureAccessControl
@@ -1654,7 +1751,7 @@ export function setBatch(items, scope) {
1654
1751
  const keys = secureEntries.map(({
1655
1752
  item
1656
1753
  }) => item.key);
1657
- const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
1754
+ const oldValues = shouldReadPreviousEventValues(scope) ? WebStorage.getBatch(keys, scope) : [];
1658
1755
  const groupedByAccessControl = new Map();
1659
1756
  secureEntries.forEach(({
1660
1757
  item,
@@ -1697,7 +1794,7 @@ export function setBatch(items, scope) {
1697
1794
  }
1698
1795
  const keys = items.map(entry => entry.item.key);
1699
1796
  const values = items.map(entry => entry.item.serialize(entry.value));
1700
- const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
1797
+ const oldValues = shouldReadPreviousEventValues(scope) ? WebStorage.getBatch(keys, scope) : [];
1701
1798
  WebStorage.setBatch(keys, values, scope);
1702
1799
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1703
1800
  emitBatchChange(scope, "setBatch", "web", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], values[index], "setBatch", "web")));
@@ -1719,7 +1816,7 @@ export function removeBatch(items, scope) {
1719
1816
  if (scope === StorageScope.Secure) {
1720
1817
  flushSecureWrites();
1721
1818
  }
1722
- const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
1819
+ const oldValues = shouldReadPreviousEventValues(scope) ? WebStorage.getBatch(keys, scope) : [];
1723
1820
  WebStorage.removeBatch(keys, scope);
1724
1821
  keys.forEach(key => cacheRawValue(scope, key, undefined));
1725
1822
  emitBatchChange(scope, "removeBatch", "web", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], undefined, "removeBatch", "web")));
@@ -1771,14 +1868,32 @@ export function runTransaction(scope, transaction) {
1771
1868
  }
1772
1869
  const NOT_SET = Symbol();
1773
1870
  const rollback = new Map();
1774
- const rememberRollback = key => {
1871
+ const rememberRollback = (key, item) => {
1775
1872
  if (rollback.has(key)) {
1776
1873
  return;
1777
1874
  }
1778
1875
  if (scope === StorageScope.Memory) {
1779
- rollback.set(key, memoryStore.has(key) ? memoryStore.get(key) : NOT_SET);
1876
+ rollback.set(key, {
1877
+ kind: "memory",
1878
+ value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET
1879
+ });
1780
1880
  } else {
1781
- rollback.set(key, getRawValue(key, scope));
1881
+ const internal = item ? item : undefined;
1882
+ if (scope === StorageScope.Secure && internal?._isBiometric === true) {
1883
+ rollback.set(key, {
1884
+ kind: "biometric",
1885
+ value: WebStorage.getSecureBiometric(key),
1886
+ level: internal._biometricLevel
1887
+ });
1888
+ return;
1889
+ }
1890
+ rollback.set(key, {
1891
+ kind: "raw",
1892
+ value: getRawValue(key, scope),
1893
+ ...(scope === StorageScope.Secure && internal?._secureAccessControl !== undefined ? {
1894
+ accessControl: internal._secureAccessControl
1895
+ } : {})
1896
+ });
1782
1897
  }
1783
1898
  };
1784
1899
  const tx = {
@@ -1798,12 +1913,12 @@ export function runTransaction(scope, transaction) {
1798
1913
  },
1799
1914
  setItem: (item, value) => {
1800
1915
  assertBatchScope([item], scope);
1801
- rememberRollback(item.key);
1916
+ rememberRollback(item.key, item);
1802
1917
  item.set(value);
1803
1918
  },
1804
1919
  removeItem: item => {
1805
1920
  assertBatchScope([item], scope);
1806
- rememberRollback(item.key);
1921
+ rememberRollback(item.key, item);
1807
1922
  item.delete();
1808
1923
  }
1809
1924
  };
@@ -1812,24 +1927,43 @@ export function runTransaction(scope, transaction) {
1812
1927
  } catch (error) {
1813
1928
  const rollbackEntries = Array.from(rollback.entries()).reverse();
1814
1929
  if (scope === StorageScope.Memory) {
1815
- rollbackEntries.forEach(([key, previousValue]) => {
1816
- if (previousValue === NOT_SET) {
1930
+ rollbackEntries.forEach(([key, record]) => {
1931
+ if (record.value === NOT_SET) {
1817
1932
  memoryStore.delete(key);
1818
1933
  } else {
1819
- memoryStore.set(key, previousValue);
1934
+ memoryStore.set(key, record.value);
1820
1935
  }
1821
1936
  notifyKeyListeners(memoryListeners, key);
1822
1937
  });
1823
1938
  } else {
1824
- const keysToSet = [];
1825
- const valuesToSet = [];
1939
+ const groupedKeysToSet = new Map();
1826
1940
  const keysToRemove = [];
1827
- rollbackEntries.forEach(([key, previousValue]) => {
1828
- if (previousValue === undefined) {
1941
+ rollbackEntries.forEach(([key, record]) => {
1942
+ if (record.kind === "biometric") {
1943
+ if (record.value === undefined) {
1944
+ WebStorage.deleteSecureBiometric(key);
1945
+ } else {
1946
+ WebStorage.setSecureBiometricWithLevel(key, record.value, record.level);
1947
+ }
1948
+ return;
1949
+ }
1950
+ if (record.kind !== "raw") {
1951
+ return;
1952
+ }
1953
+ if (record.value === undefined) {
1829
1954
  keysToRemove.push(key);
1830
1955
  } else {
1831
- keysToSet.push(key);
1832
- valuesToSet.push(previousValue);
1956
+ const accessControl = record.accessControl ?? secureDefaultAccessControl;
1957
+ const existingGroup = groupedKeysToSet.get(accessControl);
1958
+ const group = existingGroup ?? {
1959
+ keys: [],
1960
+ values: []
1961
+ };
1962
+ group.keys.push(key);
1963
+ group.values.push(record.value);
1964
+ if (!existingGroup) {
1965
+ groupedKeysToSet.set(accessControl, group);
1966
+ }
1833
1967
  }
1834
1968
  });
1835
1969
  if (scope === StorageScope.Disk) {
@@ -1838,10 +1972,13 @@ export function runTransaction(scope, transaction) {
1838
1972
  if (scope === StorageScope.Secure) {
1839
1973
  flushSecureWrites();
1840
1974
  }
1841
- if (keysToSet.length > 0) {
1842
- WebStorage.setBatch(keysToSet, valuesToSet, scope);
1843
- keysToSet.forEach((key, index) => cacheRawValue(scope, key, valuesToSet[index]));
1844
- }
1975
+ groupedKeysToSet.forEach((group, accessControl) => {
1976
+ if (scope === StorageScope.Secure) {
1977
+ WebStorage.setSecureAccessControl(accessControl);
1978
+ }
1979
+ WebStorage.setBatch(group.keys, group.values, scope);
1980
+ group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
1981
+ });
1845
1982
  if (keysToRemove.length > 0) {
1846
1983
  WebStorage.removeBatch(keysToRemove, scope);
1847
1984
  keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));