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
@@ -89,6 +89,20 @@ function isUpdater(valueOrFn) {
89
89
  function typedKeys(record) {
90
90
  return Object.keys(record);
91
91
  }
92
+ function assertEnumInteger(value, min, max, label) {
93
+ if (!Number.isFinite(value) || value < min || value > max) {
94
+ throw new Error(`NitroStorage: Invalid ${label}`);
95
+ }
96
+ if (value !== Math.trunc(value)) {
97
+ throw new Error(`NitroStorage: Invalid ${label}`);
98
+ }
99
+ }
100
+ function assertAccessControlLevel(level) {
101
+ assertEnumInteger(level, 0, 4, "access control level");
102
+ }
103
+ function assertBiometricLevel(level) {
104
+ assertEnumInteger(level, 0, 2, "biometric level");
105
+ }
92
106
  const registeredMigrations = new Map();
93
107
  const runMicrotask = typeof queueMicrotask === "function" ? queueMicrotask : task => {
94
108
  Promise.resolve().then(task);
@@ -112,6 +126,7 @@ let hasWarnedAboutWebBiometricFallback = false;
112
126
  let hasWindowStorageEventSubscription = false;
113
127
  let metricsObserver;
114
128
  let eventObserver;
129
+ let eventObserverRedactSecureValues = true;
115
130
  const metricsCounters = new Map();
116
131
  const storageEvents = new _storageEvents.StorageEventRegistry();
117
132
  function recordMetric(operation, scope, durationMs, keysCount = 1) {
@@ -307,6 +322,16 @@ function resetBackendChangeSubscription(scope) {
307
322
  externalSyncUnsubscribers.get(scope)?.();
308
323
  externalSyncUnsubscribers.delete(scope);
309
324
  }
325
+ function closeWebBackend(scope, backend) {
326
+ if (!backend?.close) {
327
+ return;
328
+ }
329
+ try {
330
+ backend.close();
331
+ } catch (error) {
332
+ throw createWebStorageError(scope, "close", error, backend);
333
+ }
334
+ }
310
335
  function ensureExternalSyncSubscriptions() {
311
336
  if (!hasWindowStorageEventSubscription && typeof window !== "undefined" && typeof window.addEventListener === "function") {
312
337
  window.addEventListener("storage", handleWebStorageEvent);
@@ -387,10 +412,42 @@ function createKeyChange(scope, key, oldValue, newValue, operation, source) {
387
412
  function hasStorageChangeObservers(scope) {
388
413
  return storageEvents.hasListeners(scope) || eventObserver !== undefined;
389
414
  }
415
+ function shouldReadPreviousEventValues(scope) {
416
+ if (storageEvents.hasListeners(scope)) {
417
+ return true;
418
+ }
419
+ if (!eventObserver) {
420
+ return false;
421
+ }
422
+ return scope !== _Storage.StorageScope.Secure || !eventObserverRedactSecureValues;
423
+ }
424
+ const SECURE_EVENT_REDACTED_VALUE = "[secure]";
425
+ function redactSecureKeyChange(event) {
426
+ if (event.scope !== _Storage.StorageScope.Secure) {
427
+ return event;
428
+ }
429
+ return {
430
+ ...event,
431
+ oldValue: event.oldValue === undefined ? undefined : SECURE_EVENT_REDACTED_VALUE,
432
+ newValue: event.newValue === undefined ? undefined : SECURE_EVENT_REDACTED_VALUE
433
+ };
434
+ }
435
+ function eventForGlobalObserver(event) {
436
+ if (!eventObserverRedactSecureValues || event.scope !== _Storage.StorageScope.Secure) {
437
+ return event;
438
+ }
439
+ if (event.type === "key") {
440
+ return redactSecureKeyChange(event);
441
+ }
442
+ return {
443
+ ...event,
444
+ changes: event.changes.map(redactSecureKeyChange)
445
+ };
446
+ }
390
447
  function emitKeyChange(scope, key, oldValue, newValue, operation, source) {
391
448
  const event = createKeyChange(scope, key, oldValue, newValue, operation, source);
392
449
  storageEvents.emitKey(event);
393
- eventObserver?.(event);
450
+ eventObserver?.(eventForGlobalObserver(event));
394
451
  }
395
452
  function emitBatchChange(scope, operation, source, changes) {
396
453
  if (changes.length === 0) {
@@ -404,7 +461,7 @@ function emitBatchChange(scope, operation, source, changes) {
404
461
  changes
405
462
  };
406
463
  storageEvents.emitBatch(event);
407
- eventObserver?.(event);
464
+ eventObserver?.(eventForGlobalObserver(event));
408
465
  }
409
466
  function readPendingSecureWrite(key) {
410
467
  return pendingSecureWrites.get(key)?.value;
@@ -574,6 +631,9 @@ const WebStorage = {
574
631
  if (scope !== _Storage.StorageScope.Disk && scope !== _Storage.StorageScope.Secure) {
575
632
  return;
576
633
  }
634
+ if (keys.length !== values.length) {
635
+ throw new Error("NitroStorage: Keys and values size mismatch in setBatch");
636
+ }
577
637
  const entries = [];
578
638
  keys.forEach((key, index) => {
579
639
  const value = values[index];
@@ -592,7 +652,7 @@ const WebStorage = {
592
652
  });
593
653
  });
594
654
  const keyIndex = ensureWebScopeKeyIndex(scope);
595
- keys.forEach(key => keyIndex.add(key));
655
+ entries.forEach(([storageKey]) => keyIndex.add(scope === _Storage.StorageScope.Secure ? storageKey.slice(SECURE_WEB_PREFIX.length) : storageKey));
596
656
  const listeners = getScopedListeners(scope);
597
657
  keys.forEach(key => notifyKeyListeners(listeners, key));
598
658
  },
@@ -678,13 +738,25 @@ const WebStorage = {
678
738
  }
679
739
  return 0;
680
740
  },
681
- setSecureAccessControl: () => {},
741
+ setSecureAccessControl: level => {
742
+ assertAccessControlLevel(level);
743
+ },
682
744
  setSecureWritesAsync: _enabled => {},
683
745
  setKeychainAccessGroup: () => {},
684
746
  setSecureBiometric: (key, value) => {
685
747
  WebStorage.setSecureBiometricWithLevel(key, value, _Storage.BiometricLevel.BiometryOnly);
686
748
  },
687
- setSecureBiometricWithLevel: (key, value, _level) => {
749
+ setSecureBiometricWithLevel: (key, value, level) => {
750
+ assertBiometricLevel(level);
751
+ if (level === _Storage.BiometricLevel.None) {
752
+ withWebBackendOperation(_Storage.StorageScope.Secure, "setSecure", backend => {
753
+ backend.removeItem(toBiometricStorageKey(key));
754
+ backend.setItem(toSecureStorageKey(key), value);
755
+ });
756
+ ensureWebScopeKeyIndex(_Storage.StorageScope.Secure).add(key);
757
+ notifyKeyListeners(getScopedListeners(_Storage.StorageScope.Secure), key);
758
+ return;
759
+ }
688
760
  if (typeof __DEV__ !== "undefined" && __DEV__ && !hasWarnedAboutWebBiometricFallback) {
689
761
  hasWarnedAboutWebBiometricFallback = true;
690
762
  console.warn("[NitroStorage] Biometric storage is not supported on web. Using localStorage.");
@@ -837,15 +909,16 @@ const storage = exports.storage = {
837
909
  subscribeNamespace: (namespace, scope, listener) => {
838
910
  return storage.subscribePrefix(scope, (0, _internal.prefixKey)(namespace, ""), listener);
839
911
  },
840
- setEventObserver: observer => {
912
+ setEventObserver: (observer, options = {}) => {
841
913
  eventObserver = observer;
914
+ eventObserverRedactSecureValues = options.redactSecureValues !== false;
842
915
  if (observer) {
843
916
  ensureExternalSyncSubscriptions();
844
917
  }
845
918
  },
846
919
  clear: scope => {
847
920
  measureOperation("storage:clear", scope, () => {
848
- const previousValues = hasStorageChangeObservers(scope) ? storage.getAll(scope) : {};
921
+ const previousValues = shouldReadPreviousEventValues(scope) ? storage.getAll(scope) : {};
849
922
  if (scope === _Storage.StorageScope.Memory) {
850
923
  memoryStore.clear();
851
924
  notifyAllListeners(memoryListeners);
@@ -895,7 +968,7 @@ const storage = exports.storage = {
895
968
  return;
896
969
  }
897
970
  const keyPrefix = (0, _internal.prefixKey)(namespace, "");
898
- const previousValues = hasStorageChangeObservers(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
971
+ const previousValues = shouldReadPreviousEventValues(scope) ? storage.getByPrefix(keyPrefix, scope) : {};
899
972
  if (scope === _Storage.StorageScope.Disk) {
900
973
  flushDiskWrites();
901
974
  }
@@ -1018,9 +1091,15 @@ const storage = exports.storage = {
1018
1091
  return result;
1019
1092
  });
1020
1093
  },
1021
- export: scope => {
1094
+ export: (scope, options = {}) => {
1095
+ if (scope === _Storage.StorageScope.Secure && options.includeSecureValues !== true) {
1096
+ throw new Error("NitroStorage: exporting Secure scope exposes raw secret values. Pass { includeSecureValues: true } or use exportSecureUnsafe().");
1097
+ }
1022
1098
  return measureOperation("storage:export", scope, () => storage.getAll(scope));
1023
1099
  },
1100
+ exportSecureUnsafe: () => {
1101
+ return measureOperation("storage:exportSecureUnsafe", _Storage.StorageScope.Secure, () => storage.getAll(_Storage.StorageScope.Secure));
1102
+ },
1024
1103
  size: scope => {
1025
1104
  return measureOperation("storage:size", scope, () => {
1026
1105
  (0, _internal.assertValidScope)(scope);
@@ -1035,6 +1114,7 @@ const storage = exports.storage = {
1035
1114
  });
1036
1115
  },
1037
1116
  setAccessControl: level => {
1117
+ assertAccessControlLevel(level);
1038
1118
  secureDefaultAccessControl = level;
1039
1119
  recordMetric("storage:setAccessControl", _Storage.StorageScope.Secure, 0);
1040
1120
  },
@@ -1187,23 +1267,33 @@ const storage = exports.storage = {
1187
1267
  }
1188
1268
  };
1189
1269
  function setWebSecureStorageBackend(backend) {
1270
+ const previousBackend = webSecureStorageBackend;
1271
+ const nextBackend = backend ?? createDefaultSecureBackend();
1190
1272
  pendingSecureWrites.clear();
1191
- webSecureStorageBackend = backend ?? createDefaultSecureBackend();
1192
1273
  resetBackendChangeSubscription(_Storage.StorageScope.Secure);
1274
+ webSecureStorageBackend = nextBackend;
1193
1275
  hydratedWebScopeKeyIndex.delete(_Storage.StorageScope.Secure);
1194
1276
  clearScopeRawCache(_Storage.StorageScope.Secure);
1195
1277
  ensureExternalSyncSubscriptions();
1278
+ if (previousBackend !== nextBackend) {
1279
+ closeWebBackend(_Storage.StorageScope.Secure, previousBackend);
1280
+ }
1196
1281
  }
1197
1282
  function getWebSecureStorageBackend() {
1198
1283
  return webSecureStorageBackend;
1199
1284
  }
1200
1285
  function setWebDiskStorageBackend(backend) {
1286
+ const previousBackend = webDiskStorageBackend;
1287
+ const nextBackend = backend ?? createDefaultDiskBackend();
1201
1288
  pendingDiskWrites.clear();
1202
- webDiskStorageBackend = backend ?? createDefaultDiskBackend();
1203
1289
  resetBackendChangeSubscription(_Storage.StorageScope.Disk);
1290
+ webDiskStorageBackend = nextBackend;
1204
1291
  hydratedWebScopeKeyIndex.delete(_Storage.StorageScope.Disk);
1205
1292
  clearScopeRawCache(_Storage.StorageScope.Disk);
1206
1293
  ensureExternalSyncSubscriptions();
1294
+ if (previousBackend !== nextBackend) {
1295
+ closeWebBackend(_Storage.StorageScope.Disk, previousBackend);
1296
+ }
1207
1297
  }
1208
1298
  function getWebDiskStorageBackend() {
1209
1299
  return webDiskStorageBackend;
@@ -1256,6 +1346,12 @@ function createStorageItem(config) {
1256
1346
  if (expiration && expiration.ttlMs <= 0) {
1257
1347
  throw new Error("expiration.ttlMs must be greater than 0.");
1258
1348
  }
1349
+ if (config.scope === _Storage.StorageScope.Secure) {
1350
+ assertBiometricLevel(resolvedBiometricLevel);
1351
+ if (secureAccessControl !== undefined) {
1352
+ assertAccessControlLevel(secureAccessControl);
1353
+ }
1354
+ }
1259
1355
  const listeners = new Set();
1260
1356
  let unsubscribe = null;
1261
1357
  let lastRaw = undefined;
@@ -1595,6 +1691,7 @@ function createStorageItem(config) {
1595
1691
  _hasExpiration: expiration !== undefined,
1596
1692
  _readCacheEnabled: readCache,
1597
1693
  _isBiometric: isBiometric,
1694
+ _biometricLevel: resolvedBiometricLevel,
1598
1695
  _defaultValue: defaultValue,
1599
1696
  ...(secureAccessControl !== undefined ? {
1600
1697
  _secureAccessControl: secureAccessControl
@@ -1724,7 +1821,7 @@ function setBatch(items, scope) {
1724
1821
  const keys = secureEntries.map(({
1725
1822
  item
1726
1823
  }) => item.key);
1727
- const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
1824
+ const oldValues = shouldReadPreviousEventValues(scope) ? WebStorage.getBatch(keys, scope) : [];
1728
1825
  const groupedByAccessControl = new Map();
1729
1826
  secureEntries.forEach(({
1730
1827
  item,
@@ -1767,7 +1864,7 @@ function setBatch(items, scope) {
1767
1864
  }
1768
1865
  const keys = items.map(entry => entry.item.key);
1769
1866
  const values = items.map(entry => entry.item.serialize(entry.value));
1770
- const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
1867
+ const oldValues = shouldReadPreviousEventValues(scope) ? WebStorage.getBatch(keys, scope) : [];
1771
1868
  WebStorage.setBatch(keys, values, scope);
1772
1869
  keys.forEach((key, index) => cacheRawValue(scope, key, values[index]));
1773
1870
  emitBatchChange(scope, "setBatch", "web", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], values[index], "setBatch", "web")));
@@ -1789,7 +1886,7 @@ function removeBatch(items, scope) {
1789
1886
  if (scope === _Storage.StorageScope.Secure) {
1790
1887
  flushSecureWrites();
1791
1888
  }
1792
- const oldValues = hasStorageChangeObservers(scope) ? WebStorage.getBatch(keys, scope) : [];
1889
+ const oldValues = shouldReadPreviousEventValues(scope) ? WebStorage.getBatch(keys, scope) : [];
1793
1890
  WebStorage.removeBatch(keys, scope);
1794
1891
  keys.forEach(key => cacheRawValue(scope, key, undefined));
1795
1892
  emitBatchChange(scope, "removeBatch", "web", keys.map((key, index) => createKeyChange(scope, key, oldValues[index], undefined, "removeBatch", "web")));
@@ -1841,14 +1938,32 @@ function runTransaction(scope, transaction) {
1841
1938
  }
1842
1939
  const NOT_SET = Symbol();
1843
1940
  const rollback = new Map();
1844
- const rememberRollback = key => {
1941
+ const rememberRollback = (key, item) => {
1845
1942
  if (rollback.has(key)) {
1846
1943
  return;
1847
1944
  }
1848
1945
  if (scope === _Storage.StorageScope.Memory) {
1849
- rollback.set(key, memoryStore.has(key) ? memoryStore.get(key) : NOT_SET);
1946
+ rollback.set(key, {
1947
+ kind: "memory",
1948
+ value: memoryStore.has(key) ? memoryStore.get(key) : NOT_SET
1949
+ });
1850
1950
  } else {
1851
- rollback.set(key, getRawValue(key, scope));
1951
+ const internal = item ? item : undefined;
1952
+ if (scope === _Storage.StorageScope.Secure && internal?._isBiometric === true) {
1953
+ rollback.set(key, {
1954
+ kind: "biometric",
1955
+ value: WebStorage.getSecureBiometric(key),
1956
+ level: internal._biometricLevel
1957
+ });
1958
+ return;
1959
+ }
1960
+ rollback.set(key, {
1961
+ kind: "raw",
1962
+ value: getRawValue(key, scope),
1963
+ ...(scope === _Storage.StorageScope.Secure && internal?._secureAccessControl !== undefined ? {
1964
+ accessControl: internal._secureAccessControl
1965
+ } : {})
1966
+ });
1852
1967
  }
1853
1968
  };
1854
1969
  const tx = {
@@ -1868,12 +1983,12 @@ function runTransaction(scope, transaction) {
1868
1983
  },
1869
1984
  setItem: (item, value) => {
1870
1985
  (0, _internal.assertBatchScope)([item], scope);
1871
- rememberRollback(item.key);
1986
+ rememberRollback(item.key, item);
1872
1987
  item.set(value);
1873
1988
  },
1874
1989
  removeItem: item => {
1875
1990
  (0, _internal.assertBatchScope)([item], scope);
1876
- rememberRollback(item.key);
1991
+ rememberRollback(item.key, item);
1877
1992
  item.delete();
1878
1993
  }
1879
1994
  };
@@ -1882,24 +1997,43 @@ function runTransaction(scope, transaction) {
1882
1997
  } catch (error) {
1883
1998
  const rollbackEntries = Array.from(rollback.entries()).reverse();
1884
1999
  if (scope === _Storage.StorageScope.Memory) {
1885
- rollbackEntries.forEach(([key, previousValue]) => {
1886
- if (previousValue === NOT_SET) {
2000
+ rollbackEntries.forEach(([key, record]) => {
2001
+ if (record.value === NOT_SET) {
1887
2002
  memoryStore.delete(key);
1888
2003
  } else {
1889
- memoryStore.set(key, previousValue);
2004
+ memoryStore.set(key, record.value);
1890
2005
  }
1891
2006
  notifyKeyListeners(memoryListeners, key);
1892
2007
  });
1893
2008
  } else {
1894
- const keysToSet = [];
1895
- const valuesToSet = [];
2009
+ const groupedKeysToSet = new Map();
1896
2010
  const keysToRemove = [];
1897
- rollbackEntries.forEach(([key, previousValue]) => {
1898
- if (previousValue === undefined) {
2011
+ rollbackEntries.forEach(([key, record]) => {
2012
+ if (record.kind === "biometric") {
2013
+ if (record.value === undefined) {
2014
+ WebStorage.deleteSecureBiometric(key);
2015
+ } else {
2016
+ WebStorage.setSecureBiometricWithLevel(key, record.value, record.level);
2017
+ }
2018
+ return;
2019
+ }
2020
+ if (record.kind !== "raw") {
2021
+ return;
2022
+ }
2023
+ if (record.value === undefined) {
1899
2024
  keysToRemove.push(key);
1900
2025
  } else {
1901
- keysToSet.push(key);
1902
- valuesToSet.push(previousValue);
2026
+ const accessControl = record.accessControl ?? secureDefaultAccessControl;
2027
+ const existingGroup = groupedKeysToSet.get(accessControl);
2028
+ const group = existingGroup ?? {
2029
+ keys: [],
2030
+ values: []
2031
+ };
2032
+ group.keys.push(key);
2033
+ group.values.push(record.value);
2034
+ if (!existingGroup) {
2035
+ groupedKeysToSet.set(accessControl, group);
2036
+ }
1903
2037
  }
1904
2038
  });
1905
2039
  if (scope === _Storage.StorageScope.Disk) {
@@ -1908,10 +2042,13 @@ function runTransaction(scope, transaction) {
1908
2042
  if (scope === _Storage.StorageScope.Secure) {
1909
2043
  flushSecureWrites();
1910
2044
  }
1911
- if (keysToSet.length > 0) {
1912
- WebStorage.setBatch(keysToSet, valuesToSet, scope);
1913
- keysToSet.forEach((key, index) => cacheRawValue(scope, key, valuesToSet[index]));
1914
- }
2045
+ groupedKeysToSet.forEach((group, accessControl) => {
2046
+ if (scope === _Storage.StorageScope.Secure) {
2047
+ WebStorage.setSecureAccessControl(accessControl);
2048
+ }
2049
+ WebStorage.setBatch(group.keys, group.values, scope);
2050
+ group.keys.forEach((key, index) => cacheRawValue(scope, key, group.values[index]));
2051
+ });
1915
2052
  if (keysToRemove.length > 0) {
1916
2053
  WebStorage.removeBatch(keysToRemove, scope);
1917
2054
  keysToRemove.forEach(key => cacheRawValue(scope, key, undefined));