react-native-nitro-storage 0.10.4 → 0.11.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 (87) hide show
  1. package/CHANGELOG.md +71 -14
  2. package/README.md +127 -41
  3. package/SECURITY.md +16 -6
  4. package/android/build.gradle +0 -1
  5. package/android/consumer-rules.pro +0 -3
  6. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +1 -4
  7. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -3
  8. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +73 -23
  9. package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +15 -0
  10. package/app.plugin.js +51 -51
  11. package/cpp/bindings/HybridStorage.cpp +25 -6
  12. package/cpp/core/NativeStorageAdapter.hpp +14 -1
  13. package/cpp/core/SqliteDiskStore.cpp +84 -11
  14. package/cpp/core/SqliteDiskStore.hpp +10 -0
  15. package/docs/api-reference.md +73 -28
  16. package/docs/benchmarks.md +4 -12
  17. package/docs/mmkv-migration.md +3 -1
  18. package/docs/native-libraries.md +10 -4
  19. package/docs/secure-storage.md +26 -12
  20. package/docs/web-backends.md +6 -2
  21. package/indexeddb-backend/package.json +6 -0
  22. package/ios/IOSStorageAdapterCpp.hpp +4 -0
  23. package/ios/IOSStorageAdapterCpp.mm +96 -18
  24. package/lib/commonjs/capabilities.js +3 -8
  25. package/lib/commonjs/capabilities.js.map +1 -1
  26. package/lib/commonjs/index.js +23 -8
  27. package/lib/commonjs/index.js.map +1 -1
  28. package/lib/commonjs/index.web.js +61 -19
  29. package/lib/commonjs/index.web.js.map +1 -1
  30. package/lib/commonjs/indexeddb-backend.js +21 -37
  31. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  32. package/lib/commonjs/internal.js +6 -0
  33. package/lib/commonjs/internal.js.map +1 -1
  34. package/lib/commonjs/storage-core.js +35 -11
  35. package/lib/commonjs/storage-core.js.map +1 -1
  36. package/lib/commonjs/storage-runtime.js +1 -1
  37. package/lib/commonjs/storage-runtime.js.map +1 -1
  38. package/lib/commonjs/testing.js +68 -23
  39. package/lib/commonjs/testing.js.map +1 -1
  40. package/lib/commonjs/web-backend-contract.js +6 -2
  41. package/lib/commonjs/web-backend-contract.js.map +1 -1
  42. package/lib/module/capabilities.js +3 -8
  43. package/lib/module/capabilities.js.map +1 -1
  44. package/lib/module/index.js +14 -5
  45. package/lib/module/index.js.map +1 -1
  46. package/lib/module/index.web.js +61 -12
  47. package/lib/module/index.web.js.map +1 -1
  48. package/lib/module/indexeddb-backend.js +21 -37
  49. package/lib/module/indexeddb-backend.js.map +1 -1
  50. package/lib/module/internal.js +5 -0
  51. package/lib/module/internal.js.map +1 -1
  52. package/lib/module/storage-core.js +36 -12
  53. package/lib/module/storage-core.js.map +1 -1
  54. package/lib/module/storage-runtime.js +1 -1
  55. package/lib/module/storage-runtime.js.map +1 -1
  56. package/lib/module/testing.js +59 -16
  57. package/lib/module/testing.js.map +1 -1
  58. package/lib/module/web-backend-contract.js +5 -2
  59. package/lib/module/web-backend-contract.js.map +1 -1
  60. package/lib/typescript/capabilities.d.ts.map +1 -1
  61. package/lib/typescript/index.d.ts +8 -1
  62. package/lib/typescript/index.d.ts.map +1 -1
  63. package/lib/typescript/index.web.d.ts +7 -1
  64. package/lib/typescript/index.web.d.ts.map +1 -1
  65. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  66. package/lib/typescript/internal.d.ts +1 -0
  67. package/lib/typescript/internal.d.ts.map +1 -1
  68. package/lib/typescript/storage-core.d.ts +1 -1
  69. package/lib/typescript/storage-core.d.ts.map +1 -1
  70. package/lib/typescript/storage-runtime.d.ts +1 -1
  71. package/lib/typescript/storage-runtime.d.ts.map +1 -1
  72. package/lib/typescript/testing.d.ts +15 -2
  73. package/lib/typescript/testing.d.ts.map +1 -1
  74. package/lib/typescript/web-backend-contract.d.ts +1 -0
  75. package/lib/typescript/web-backend-contract.d.ts.map +1 -1
  76. package/package.json +14 -5
  77. package/react-native-nitro-storage.podspec +4 -2
  78. package/src/capabilities.ts +4 -9
  79. package/src/index.ts +19 -5
  80. package/src/index.web.ts +86 -19
  81. package/src/indexeddb-backend.ts +20 -42
  82. package/src/internal.ts +8 -0
  83. package/src/storage-core.ts +52 -19
  84. package/src/storage-runtime.ts +3 -1
  85. package/src/testing.ts +91 -18
  86. package/src/web-backend-contract.ts +6 -2
  87. package/testing/package.json +8 -0
package/CHANGELOG.md CHANGED
@@ -6,6 +6,60 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
6
6
  and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
7
  Breaking changes are always listed first in each release section.
8
8
 
9
+ ## [0.11.0] - 2026-09-30
10
+
11
+ ### Breaking changes
12
+
13
+ - **React Native 0.77 or newer is required.** The `react-native` peer range is now `>=0.77.0` (was `>=0.75.0`), matching the minimum for Nitro Modules 0.37, whose Android package does not compile against React Native 0.76. Supported: React Native 0.77+ / Expo SDK 53+; tested on React Native 0.86.3 / Expo SDK 57. Migration: upgrade to React Native 0.77 or Expo SDK 53 or newer.
14
+ - **iOS `has()` on Secure keys throws instead of returning `false`.** `item.has()` and `storage.has(key, StorageScope.Secure)` throw `keychain_locked` while the keychain is locked and a Keychain status error for any other unexpected status, matching `hasSecureBiometric` and Android. A biometric item that needs authentication counts as present. Migration: wrap Secure existence checks that can run while the device is locked in `try`/`catch` and retry after unlock when `isStorageError(error, "keychain_locked")`.
15
+ - **iOS Secure key listing throws on unexpected Keychain statuses.** `getAllKeys`, `size`, `getKeysByPrefix`, and `getByPrefix` for Secure scope throw a Keychain status error (for example a misconfigured access group) instead of returning an empty result. Migration: catch errors around Secure enumeration and check the Keychain access group.
16
+ - **Android permanent Secure store failures report `storage_corruption`.** When the default Secure master key or store cannot be created, Secure calls throw `storage_corruption` instead of `authentication_required`. Migration: do not retry these errors; tell the user that secure data must be reset.
17
+ - **`fallbackToCacheOnReadError` applies only to `keychain_locked`.** Other read errors such as `authentication_required`, `key_invalidated`, and `storage_corruption` now throw instead of returning the cached value. Only iOS reports `keychain_locked`, so the option has no effect on Android or web. Migration: catch those codes where you relied on the fallback, and handle them explicitly (prompt the user, or recreate the credential).
18
+ - **Native `getCapabilities().backend.disk` is `"sqlite"`.** It was `"platform-preferences"`, although native Disk has used SQLite WAL since 0.10.3. Migration: compare against `"sqlite"` if you branch on this string.
19
+ - **Empty storage keys are rejected.** `createStorageItem({ key: "" })` (also with a `namespace`), `setString("")`, `deleteString("")`, and `import({ "": value })` throw an error with the new `invalid_key` code. On native an empty key was also the clear signal, so writing it cleared pending writes and caches for the whole scope. Migration: rename any empty key; handle `invalid_key` if your `StorageErrorCode` switch is exhaustive.
20
+ - **Deprecated:** the root export of `createIndexedDBBackend`. Migration: import it from `react-native-nitro-storage/indexeddb-backend`. The root export still works in this release.
21
+
22
+ ### Fixed
23
+
24
+ - Deleting an item, `deleteString()`, and transaction rollbacks read the previous value only when an event listener or an unredacted event observer needs it. Deleting or expiring a biometric item no longer shows a Face ID or passcode prompt on iOS, and a cancelled prompt no longer blocks the delete.
25
+ - Memory `setBatch()` keeps string values that start with the reserved escape prefix intact.
26
+ - Memory `clear(scope, { except })` and `deleteString()` drop TTL deadlines, so a later write to the same key no longer expires early or fires `onExpired`.
27
+ - A late native change notification for an older write no longer overwrites a newer cached value for `readCache` items and their subscribers.
28
+ - Reads fill the Disk/Secure raw cache only for items with `readCache` or `fallbackToCacheOnReadError`, so other Secure reads no longer keep plaintext values in the JavaScript heap.
29
+ - Web: when one backend instance is registered for both Disk and Secure, Disk enumeration and export skip Secure keys, `clear()` removes only the cleared scope's keys, and backend `subscribe()` events (for example from another tab) reach only the scope that owns the key.
30
+ - Web: window `storage` events update a scope only while it uses the default `localStorage` backend, and only for events from `localStorage`. Writes to `localStorage` in another tab no longer add phantom keys to IndexedDB-backed scopes.
31
+ - Web IndexedDB backend: cross-tab messages that arrive during startup hydration are applied after the snapshot instead of being overwritten.
32
+ - `react-native-nitro-storage/testing` notifies Disk and Secure subscribers, so hooks re-render for those scopes, and it exports the web backend setters, getters, and `flushWebStorageBackends` as no-ops like the native entry.
33
+ - `describeWebBackendCapabilities`, `isIndexedDBWebBackend`, and `WebBackendCapabilities` are exported from the main entry types.
34
+ - iOS Secure writes update `kSecAttrAccessible` on existing Keychain items, so a changed access control level reaches items that already exist.
35
+ - iOS Secure batch writes notify listeners for the keys that were applied before a failure is rethrown.
36
+ - iOS resolves the Disk database path once instead of on every Disk call, runs the legacy Disk migration on first Disk use instead of at module creation, and a corrupt Disk database no longer leaks a file descriptor per call or blocks Memory and Secure scopes.
37
+ - Android creates the Secure master key and preferences on first Secure use instead of at app start, so a Keystore failure surfaces as a Secure error instead of crashing the app at launch.
38
+ - Android biometric reads and existence checks open stores in the order `BiometryOrPasscode`, legacy biometric, `BiometryOnly` and stop at the first hit (previously `BiometryOnly` first), so reading a `BiometryOrPasscode` item needs only a device-credential authentication. Biometric writes still snapshot every existing store and remove the key from every other store, so no stale duplicates remain.
39
+ - Android Disk deletes also remove the key from the legacy `NitroStorage` SharedPreferences, and `clear(Disk)` clears them, so a logout wipe leaves no pre-SQLite copy. The legacy file is otherwise kept for downgrade safety, like the iOS suite domain.
40
+ - The podspec uses React Native's minimum iOS version and excludes test sources; the Android build no longer pins `kotlin-stdlib`.
41
+ - The Expo config plugin loads `expo/config-plugins`, and `expo` (`>=53.0.0`) is declared as an optional peer dependency.
42
+ - The `/testing` and `/indexeddb-backend` subpaths ship stub `package.json` folders, so they resolve when Metro package exports are disabled (the default before React Native 0.79).
43
+ - The IndexedDB backend no longer installs `pagehide` and `visibilitychange` listeners; they started no IndexedDB work.
44
+
45
+ ### Documentation
46
+
47
+ - Android biometric protection is checked when a biometric store is first opened in a process; later reads in that process do not authenticate again, and the package never shows a prompt on Android. Apps must run their own `BiometricPrompt` before gated reads. The previous "30-second window" wording was wrong.
48
+ - iOS biometric reads block the JavaScript thread while the system prompt is visible.
49
+ - README web backend example uses separate stores for Disk and Secure, the events example uses the `(namespace, scope, listener)` signature, the auth-token example no longer enables `fallbackToCacheOnReadError`, and the error code table lists every code.
50
+ - Compatibility: supports React Native 0.77+ / Expo SDK 53+ (Nitro Modules 0.37 minimum); tested on React Native 0.86.3 / Expo SDK 57.
51
+ - `SECURITY.md` names the supported `0.11.x` line and private vulnerability reporting.
52
+
53
+ ## [0.10.5] - 2026-09-30
54
+
55
+ ### Breaking changes
56
+
57
+ - None.
58
+
59
+ ### Fixed
60
+
61
+ - iOS no longer re-reads the whole UserDefaults suite and re-imports it into SQLite on every cold start. The import now runs once and records completion in the SQLite `meta` table, like Android. Existing values stay readable, and the suite domain is kept so a downgrade still sees its data.
62
+
9
63
  ## [0.10.4] - 2026-09-27
10
64
 
11
65
  ### Breaking changes
@@ -183,14 +237,17 @@ Breaking changes are always listed first in each release section.
183
237
  ### Changed
184
238
 
185
239
  - `getCapabilities().writeBuffering` now reports real per-mode durability: native Secure writes report buffering only while `setSecureWritesAsync(true)` is active on Android, and web backends report buffering only for IndexedDB-based backends.
186
- - The IndexedDB backend reports affected keys when `flush()` fails and starts a best-effort flush on `pagehide` and hidden visibility changes.
240
+ - The IndexedDB backend reports affected keys when `flush()` fails and adds `pagehide` and hidden-visibility listeners. Those listeners started no IndexedDB work and were removed in 0.11.0.
187
241
  - `setIfVersion()` is documented as optimistic (no backend-level atomicity); CAS guarantees are covered by race tests.
188
242
 
189
243
  ## [0.7.0] - 2026-07-30
190
244
 
245
+ ### Breaking changes
246
+
247
+ - Android secure key discovery, existence checks, and cleanup now surface locked, unavailable, or invalidated biometric-store errors instead of treating inaccessible protected values as absent. Catch storage errors around these operations and use `isKeychainLockedError()` when retrying after device authentication is appropriate.
248
+
191
249
  ### Changes
192
250
 
193
- - **Breaking change:** Android secure key discovery, existence checks, and cleanup now surface locked, unavailable, or invalidated biometric-store errors instead of treating inaccessible protected values as absent. Catch storage errors around these operations and use `isKeychainLockedError()` when retrying after device authentication is appropriate.
194
251
  - Upgrade the validated package baseline to Expo SDK 57, React Native 0.86.2, and Nitro Modules/Nitrogen 0.36.4.
195
252
  - Preserve each item/value relationship in heterogeneous `setBatch()` calls so TypeScript rejects values assigned to the wrong storage item.
196
253
  - Serialize native key-index hydration with concurrent mutations so `has`, `size`, and key queries cannot remain stale after a racing write.
@@ -199,6 +256,15 @@ Breaking changes are always listed first in each release section.
199
256
 
200
257
  ## [0.6.0] - 2026-06-15
201
258
 
259
+ ### Breaking changes
260
+
261
+ All new APIs are additive — existing code keeps working. These behavior and
262
+ type changes can affect advanced consumers:
263
+
264
+ - TTL expiry now emits a `"expire"` change event instead of `"remove"`. Previously, a disk/secure value expiring on read emitted `operation: "remove"` and an expiring memory value emitted no event at all. If you subscribe to storage events and branch on `operation === "remove"` to detect expiry, also handle `"expire"` (or use the new `storage.subscribeExpired()`).
265
+ - `StorageChangeOperation` gained the `"expire"` and `"clearGroup"` members. Exhaustive `switch` statements over a change event's `operation` need cases for the new members.
266
+ - `useStorage()` now returns a three-element tuple `[value, setter, actions]` (was two). Array destructuring such as `const [value, setStore] = useStorage(item)` is unaffected; only code that annotated the result with an explicit two-element tuple type needs to widen the annotation.
267
+
202
268
  ### Added
203
269
 
204
270
  - Object-state ergonomics on `StorageItem<T>`: `item.merge(partial)` for shallow object updates, `item.reset()` to return to the default value, and `item.setOrDelete(value)` which deletes on `null`/`undefined` and sets otherwise.
@@ -214,16 +280,7 @@ Breaking changes are always listed first in each release section.
214
280
 
215
281
  ### Changed
216
282
 
217
- - Faster writes when nothing is subscribed: the native write/notify path now takes a lock-free fast path (per-scope atomic listener counts) and skips locking and copying the listener vector when a scope has no listeners. Applies to both iOS and Android via the shared C++ `HybridStorage`, and is thread-safe (verified under the C++ AddressSanitizer, ThreadSanitizer, and UndefinedBehaviorSanitizer suites).
218
-
219
- ### Breaking Changes
220
-
221
- All new APIs are additive — existing code keeps working. These behavior and
222
- type changes can affect advanced consumers:
223
-
224
- - TTL expiry now emits a `"expire"` change event instead of `"remove"`. Previously, a disk/secure value expiring on read emitted `operation: "remove"` and an expiring memory value emitted no event at all. If you subscribe to storage events and branch on `operation === "remove"` to detect expiry, also handle `"expire"` (or use the new `storage.subscribeExpired()`).
225
- - `StorageChangeOperation` gained the `"expire"` and `"clearGroup"` members. Exhaustive `switch` statements over a change event's `operation` need cases for the new members.
226
- - `useStorage()` now returns a three-element tuple `[value, setter, actions]` (was two). Array destructuring such as `const [value, setStore] = useStorage(item)` is unaffected; only code that annotated the result with an explicit two-element tuple type needs to widen the annotation.
283
+ - Writes skip listener locking when nothing is subscribed: the native write/notify path now takes a lock-free fast path (per-scope atomic listener counts) and skips locking and copying the listener vector when a scope has no listeners. Applies to both iOS and Android via the shared C++ `HybridStorage`, and is thread-safe (verified under the C++ AddressSanitizer, ThreadSanitizer, and UndefinedBehaviorSanitizer suites).
227
284
 
228
285
  ## [0.5.9] - 2026-06-11
229
286
 
@@ -256,7 +313,7 @@ type changes can affect advanced consumers:
256
313
 
257
314
  ### Changed
258
315
 
259
- - Speed up iOS Secure batch operations by reusing the resolved Keychain access group and access-control level across each batch instead of re-reading configuration per key.
316
+ - iOS Secure batch operations reuse the resolved Keychain access group and access-control level across each batch instead of re-reading configuration per key.
260
317
  - Refactor iOS Secure set/get/delete helpers so single-item and batch paths share Keychain status handling and cache updates.
261
318
  - Strengthen TypeScript inference parity on web by exporting `StorageSetter` and preserving tuple value types from `getBatch()`.
262
319
 
@@ -442,7 +499,7 @@ type changes can affect advanced consumers:
442
499
  - Group secure raw batch writes by per-item access control so secure batch paths stay fast even with mixed access-control settings.
443
500
  - Optimize C++ batch listener dispatch by copying scoped listeners once per batch operation.
444
501
  - Avoid duplicate secure biometric clearing calls by relying on secure clear paths that already include biometric cleanup.
445
- - Optimize web secure/disk key bookkeeping with an indexed key cache (faster `size`, `getAllKeys`, and namespace clears without repeated `localStorage` scans).
502
+ - Optimize web secure/disk key bookkeeping with an indexed key cache (`size`, `getAllKeys`, and namespace clears no longer rescan `localStorage`).
446
503
  - Improve iOS secure key union performance by deduplicating with an `unordered_set`.
447
504
  - Extract shared React hooks into `src/storage-hooks.ts` to reduce native/web entrypoint duplication.
448
505
  - Expand benchmark coverage to include Disk and Secure scope throughput checks and tighten regression thresholds.
package/README.md CHANGED
@@ -61,15 +61,28 @@ Peer dependencies:
61
61
  | Package | Version |
62
62
  | ---------------------------- | ------------------ |
63
63
  | `react` | `>=18.2.0` |
64
- | `react-native` | `>=0.75.0` |
64
+ | `react-native` | `>=0.77.0` |
65
65
  | `react-native-nitro-modules` | `>=0.37.0 <0.38.0` |
66
66
 
67
67
  Nitro peer requirement: `react-native-nitro-modules >=0.37.0 <0.38.0`.
68
68
 
69
+ | Tested on | Supported floor |
70
+ | ------------------------------------------ | ----------------------------------- |
71
+ | React Native `0.86.3` / Expo SDK `57.0.26` | React Native `0.77` / Expo SDK `53` |
72
+
73
+ Nitro Storage supports React Native 0.77 or newer and Expo SDK 53 or newer,
74
+ which is the minimum for Nitro Modules 0.37: its Android package does not
75
+ compile against React Native 0.76. It is tested on React Native 0.86.3 and Expo
76
+ SDK 57.
77
+
78
+ The `react-native-nitro-storage/testing` and
79
+ `react-native-nitro-storage/indexeddb-backend` subpaths also resolve when Metro
80
+ package exports are disabled (the default before React Native 0.79).
81
+
69
82
  The package gate uses React Native `0.86.3` and the Strict TypeScript API.
70
83
  `check:ci` also compiles the public source against React Native `0.87.0`'s
71
84
  Strict TypeScript API; this does not change the runtime baseline. The Expo
72
- example uses Expo SDK `57.0.25`, React Native
85
+ example uses Expo SDK `57.0.26`, React Native
73
86
  `0.86.3`, React `19.2.3`, and Nitro Modules `0.37.1`, which is the React Native
74
87
  version supported by that Expo SDK. Do not override Expo's React Native version.
75
88
 
@@ -78,7 +91,7 @@ before installing this package, then rebuild the native app so the generated
78
91
  Nitro bindings and native runtime use the same major-minor version:
79
92
 
80
93
  ```sh
81
- bun add react-native-nitro-modules@0.37.1 react-native-nitro-storage@0.10.4
94
+ bun add react-native-nitro-modules@0.37.1 react-native-nitro-storage@0.11.0
82
95
  bunx expo prebuild
83
96
  ```
84
97
 
@@ -125,11 +138,15 @@ Add the config plugin before prebuilding native iOS and Android projects:
125
138
  }
126
139
  ```
127
140
 
128
- | Option | Default | What it does |
129
- | ------------------------- | ------------------------ | -------------------------------------------------------------- |
130
- | `faceIDPermission` | Built-in Face ID message | Sets `NSFaceIDUsageDescription`. |
131
- | `addBiometricPermissions` | `false` | Adds Android biometric and fingerprint permissions. |
132
- | `configureAndroidBackup` | `true` | Writes Android backup rules that exclude secure storage files. |
141
+ | Option | Default | What it does |
142
+ | ------------------------- | ------------------------ | --------------------------------------------------------------- |
143
+ | `faceIDPermission` | Built-in Face ID message | Sets `NSFaceIDUsageDescription`. |
144
+ | `addBiometricPermissions` | `false` | Adds Android `USE_BIOMETRIC` and `USE_FINGERPRINT` permissions. |
145
+ | `configureAndroidBackup` | `true` | Writes Android backup rules that exclude secure storage files. |
146
+
147
+ Nitro Storage does not show a biometric prompt on Android. Enable
148
+ `addBiometricPermissions` when your app runs its own `BiometricPrompt` before it
149
+ reads biometric items; the permissions are for that prompt.
133
150
 
134
151
  Android adapter initialization is owned by the package through an Android
135
152
  manifest initializer, so apps should not edit `MainApplication` to call
@@ -165,7 +182,10 @@ application state.
165
182
  Native storage calls are synchronous JSI operations. Keep values and batches
166
183
  small enough for the JavaScript event loop; native and configured web-backend
167
184
  failures throw errors. Secure cache fallback is opt-in through
168
- `fallbackToCacheOnReadError`.
185
+ `fallbackToCacheOnReadError` and applies only to `keychain_locked` read errors,
186
+ which only iOS reports.
187
+ Storage keys must be non-empty strings; an empty key throws an `invalid_key`
188
+ error before it reaches native storage.
169
189
 
170
190
  ## Auth Tokens
171
191
 
@@ -194,9 +214,9 @@ boundary. `createSecureAuthStorage` already namespaces keys, notifies
194
214
  subscribers, and migrates legacy keys.
195
215
 
196
216
  Do not enable `fallbackToCacheOnReadError` for access or refresh tokens unless
197
- the application explicitly accepts stale or revoked credentials. Handle
198
- temporary secure-storage errors and retry from application lifecycle state
199
- instead.
217
+ the application explicitly accepts stale credentials while the keychain is
218
+ locked. Handle temporary secure-storage errors and retry from application
219
+ lifecycle state instead.
200
220
 
201
221
  ## Typed Storage Items
202
222
 
@@ -310,8 +330,11 @@ storage.clear(StorageScope.Disk, {
310
330
  ## Legacy Key Migration And Secure Resilience
311
331
 
312
332
  `renameFrom` migrates an old key to a new one on first read and deletes the
313
- legacy entry. Secure items can fall back to the last cached value when the
314
- keychain is locked instead of throwing.
333
+ legacy entry. Secure items that set `fallbackToCacheOnReadError` return the last
334
+ value read in this process when a read fails with `keychain_locked`. Every other
335
+ read error, such as `authentication_required`, `key_invalidated`, or
336
+ `storage_corruption`, still throws. Only iOS reports `keychain_locked`, so the fallback has an effect only on iOS; Android and web never use it. Use the fallback for values where a stale
337
+ copy is acceptable, not for credentials.
315
338
 
316
339
  ```ts
317
340
  import {
@@ -319,11 +342,11 @@ import {
319
342
  createSecureAuthStorage,
320
343
  } from "react-native-nitro-storage";
321
344
 
322
- const accessToken = secureItem<string>({
323
- key: "accessToken",
324
- namespace: "auth",
325
- defaultValue: "",
326
- renameFrom: "authToken", // copied + cleaned up on first read
345
+ const cachedProfile = secureItem<{ name: string } | null>({
346
+ key: "profile",
347
+ namespace: "account",
348
+ defaultValue: null,
349
+ renameFrom: "userProfile", // copied + cleaned up on first read
327
350
  fallbackToCacheOnReadError: true,
328
351
  onReadError: (error) => reportSecureReadError(error),
329
352
  });
@@ -333,7 +356,7 @@ const auth = createSecureAuthStorage(
333
356
  accessToken: { renameFrom: "authToken" },
334
357
  refreshToken: { renameFrom: "refreshToken" },
335
358
  },
336
- { namespace: "auth", group: "session", fallbackToCacheOnReadError: true },
359
+ { namespace: "auth", group: "session" },
337
360
  );
338
361
  ```
339
362
 
@@ -488,6 +511,33 @@ can also throw when a protected store is locked or its key is invalidated. Use
488
511
  `authentication_required`, and rebuild the affected credential for
489
512
  `key_invalidated`.
490
513
 
514
+ Changing the access control level applies to later writes of existing items
515
+ too: on iOS every Secure write now updates `kSecAttrAccessible` of an item that
516
+ already exists. On iOS, `item.has()` and `storage.has(key, StorageScope.Secure)` throw
517
+ `keychain_locked` while the keychain is locked, and a Keychain status error for
518
+ any other unexpected status, instead of returning `false`. A biometric item that
519
+ the Keychain reports as needing authentication counts as present, so `has()` on
520
+ a biometric item returns `true` without a prompt; while the device is locked the
521
+ same status also returns `true`. Listing, counting, and prefix queries on Secure
522
+ keys throw a Keychain status error for unexpected statuses instead of returning
523
+ an empty result. Deleting an item reads its previous value only when an event listener
524
+ or an unredacted event observer needs it, so deleting a biometric item does not
525
+ show a biometric prompt.
526
+
527
+ Biometric behaviour differs by platform:
528
+
529
+ - **iOS:** `getSecureBiometric` reads run through the Keychain with user
530
+ interaction allowed. The system shows the Face ID, Touch ID, or passcode sheet
531
+ and the synchronous JSI call blocks the JavaScript thread until the user
532
+ answers. Read biometric items from a user action, not during render.
533
+ - **Android:** Nitro Storage never shows a prompt. Each biometric store is an
534
+ `EncryptedSharedPreferences` file whose Keystore key requires recent user
535
+ authentication. The key is checked only when the store is first opened in a
536
+ process; later reads and writes in that process use the already-decrypted
537
+ keyset and do not check authentication again. Run your own `BiometricPrompt`
538
+ before every read that must be gated. Reading a `BiometryOrPasscode` item
539
+ opens only that store, so a device-credential authentication is enough.
540
+
491
541
  ## Batch Operations
492
542
 
493
543
  `getBatch()` preserves tuple value types, so IDEs infer each result from the
@@ -527,9 +577,17 @@ removeBatch([themeItem, localeItem], StorageScope.Disk);
527
577
  ## Events And Observability
528
578
 
529
579
  ```ts
530
- const unsubscribe = storage.subscribeNamespace("settings", (event) => {
531
- console.log(event.key, event.operation, event.source);
532
- });
580
+ const unsubscribe = storage.subscribeNamespace(
581
+ "settings",
582
+ StorageScope.Disk,
583
+ (event) => {
584
+ if (event.type === "key") {
585
+ console.log(event.key, event.operation, event.source);
586
+ } else {
587
+ console.log(event.changes.length, event.operation, event.source);
588
+ }
589
+ },
590
+ );
533
591
 
534
592
  storage.setEventObserver((event) => {
535
593
  console.log(event.type, event.scope);
@@ -549,9 +607,10 @@ unsubscribe();
549
607
  `getMetricsSnapshot()` aggregates each operation across scopes for backward
550
608
  compatibility. `getScopedMetricsSnapshot()` adds the numeric scope suffix for
551
609
  per-scope analysis, for example `item:set:1`. `getCacheMetrics()` reports live
552
- Disk/Secure raw-cache hits, misses, entries, and estimated bytes. The cache is
553
- unbounded; `resetMetrics()` zeros the hit/miss counters and leaves entries in
554
- place.
610
+ Disk/Secure raw-cache hits, misses, entries, and estimated bytes. Reads fill
611
+ the cache only for items with `readCache` or `fallbackToCacheOnReadError`. The
612
+ cache is unbounded; `resetMetrics()` zeros the hit/miss counters and leaves
613
+ entries in place.
555
614
 
556
615
  The example app includes hidden integrity, keychain, and Disk/Secure stress
557
616
  labs at `nitrostorage://e2e-integrity`, `nitrostorage://e2e-keychain`, and
@@ -621,12 +680,19 @@ import {
621
680
  } from "react-native-nitro-storage";
622
681
  import { createIndexedDBBackend } from "react-native-nitro-storage/indexeddb-backend";
623
682
 
624
- const backend = await createIndexedDBBackend("app-storage", "kv");
683
+ const diskBackend = await createIndexedDBBackend("app-storage", "disk");
684
+ const secureBackend = await createIndexedDBBackend("app-storage", "secure");
625
685
 
626
- setWebDiskStorageBackend(backend);
627
- setWebSecureStorageBackend(backend);
686
+ setWebDiskStorageBackend(diskBackend);
687
+ setWebSecureStorageBackend(secureBackend);
628
688
  ```
629
689
 
690
+ Use one backend instance per scope. If one instance is registered for both
691
+ scopes, Disk enumeration skips Secure keys and each scope's `clear()` removes
692
+ only its own keys, but separate stores keep the two scopes fully isolated.
693
+ Import `createIndexedDBBackend` from the `react-native-nitro-storage/indexeddb-backend`
694
+ subpath; the root export is deprecated.
695
+
630
696
  Web reads and mutations stay synchronous against the backend's in-memory
631
697
  contract; use `flushWebStorageBackends()` for asynchronous persistence
632
698
  boundaries. The native entry keeps the web backend setters, getters, and flush
@@ -635,11 +701,19 @@ function as typed no-ops for cross-platform code.
635
701
  Browser storage cannot provide iOS Keychain or Android Keystore guarantees. Web
636
702
  Secure scope is only as strong as the backend you configure.
637
703
 
704
+ Cross-tab `storage` events update a scope only while that scope uses the
705
+ default `localStorage` backend. Custom backends sync through their own
706
+ `subscribe()` channel; the IndexedDB backend uses a `BroadcastChannel`.
707
+
638
708
  ## Testing
639
709
 
640
- The `react-native-nitro-storage/testing` entrypoint is a faithful in-memory
641
- implementation of the full public surface, so unit tests and Storybook run
642
- without native modules. Mock the package with it, or use it directly.
710
+ The `react-native-nitro-storage/testing` entrypoint is an in-memory
711
+ implementation with the same runtime exports as the main entry, so unit tests
712
+ and Storybook run without native modules. Item subscribers and hooks re-render
713
+ for Memory, Disk, and Secure writes. It does not model platform behaviour:
714
+ there are no keychain locks, biometric prompts, access-control levels,
715
+ coalesced native write timing, or web backends (the web backend functions are
716
+ no-ops). Mock the package with it, or use it directly.
643
717
 
644
718
  ```ts
645
719
  import {
@@ -674,13 +748,23 @@ backends. The full reference lives in
674
748
  ## Error Contract
675
749
 
676
750
  Native and web adapters tag classified failures with stable error codes. Use
677
- `getStorageErrorCode(error)` or `isStorageError(error, code)` to branch on them:
678
- `keychain_locked` reports a locked Keychain that a retry can recover after
679
- authentication, secure-scope write or biometric failures carry their own
680
- codes, and invalid inputs (bad scope, malformed keys, numeric guard
681
- violations) are rejected before reaching native storage. Errors never swallow
682
- the underlying cause silently: the original platform message is preserved on
683
- the error for diagnostics.
751
+ `getStorageErrorCode(error)` or `isStorageError(error, code)` to branch on them.
752
+ Errors never swallow the underlying cause silently: the original platform
753
+ message is preserved on the error for diagnostics.
754
+
755
+ | Code | Meaning |
756
+ | ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
757
+ | `keychain_locked` | The protected store is locked. Retry after the device unlocks. |
758
+ | `authentication_required` | The item needs user authentication, or the user cancelled the prompt. |
759
+ | `key_invalidated` | The protecting key was invalidated, for example by a biometric enrolment change. |
760
+ | `biometric_unavailable` | The requested biometric level is not available on this device or OS version. |
761
+ | `storage_corruption` | Stored secure data could not be decrypted, or (Android) the Secure master key or store cannot be created. Do not retry. |
762
+ | `storage_compensation_failed` | A multi-step write failed and restoring the previous state also failed. |
763
+ | `unsupported` | The operation is not available on this platform or environment. |
764
+ | `invalid_key` | The storage key is empty. Keys must be non-empty strings. |
765
+
766
+ Invalid scopes and non-finite numeric levels are rejected with untagged errors
767
+ before they reach native storage.
684
768
 
685
769
  ## Platform Support
686
770
 
@@ -716,8 +800,10 @@ the error for diagnostics.
716
800
  upgrading the package so the Android manifest initializer is merged.
717
801
  - **Secure values fail after Android restore:** keep `configureAndroidBackup:
718
802
  true` or provide equivalent backup exclusions.
719
- - **Biometric prompt does not appear:** set `biometric: true` on the item and
720
- add native biometric permissions when your app needs them.
803
+ - **Biometric prompt does not appear on Android:** this is expected. The
804
+ package never prompts on Android; run `BiometricPrompt` in your app before
805
+ reading the item, and enable `addBiometricPermissions` for that prompt. On
806
+ iOS, set `biometric: true` on the item and a `faceIDPermission` message.
721
807
  - **Web secure storage is unavailable:** configure a secure backend before using
722
808
  Secure scope on web.
723
809
  - **TypeScript cannot infer `getBatch()` tuple values:** pass readonly tuples
package/SECURITY.md CHANGED
@@ -4,14 +4,18 @@
4
4
 
5
5
  Security fixes are shipped for the latest published `0.x` release line.
6
6
 
7
- | Version | Supported |
8
- | ------- | --------- |
9
- | `0.8.x` | Yes |
10
- | `<0.8` | No |
7
+ | Version | Supported |
8
+ | -------- | --------- |
9
+ | `0.11.x` | Yes |
10
+ | `< 0.11` | No |
11
11
 
12
12
  ## Reporting a Vulnerability
13
13
 
14
- Report security issues through GitHub Security Advisories with:
14
+ Report security issues privately through GitHub private vulnerability reporting: https://github.com/JoaoPauloCMarra/react-native-nitro-storage/security/advisories/new
15
+
16
+ Never report vulnerabilities in public issues, pull requests, or discussions.
17
+
18
+ Include:
15
19
 
16
20
  - affected package version
17
21
  - platform and OS version
@@ -23,4 +27,10 @@ Do not publish proof-of-concept exploit details until a fix is available.
23
27
 
24
28
  ## Storage Boundary
25
29
 
26
- Native Secure scope delegates encryption to platform storage APIs: iOS Keychain and Android Jetpack Security `EncryptedSharedPreferences`. Web Secure scope is API-compatible but defaults to namespaced `localStorage`; use a custom web secure backend when browser-side storage must meet a stricter threat model.
30
+ Memory scope keeps values in process memory only.
31
+
32
+ Native Disk scope stores values unencrypted in an app-private SQLite database in WAL mode on iOS and Android. Do not store secrets in Disk scope.
33
+
34
+ Native Secure scope delegates encryption to platform storage APIs: iOS Keychain and Android Jetpack Security `EncryptedSharedPreferences`.
35
+
36
+ Web Disk and Secure scopes default to namespaced `localStorage`, or to the custom backend set with `setWebDiskStorageBackend` / `setWebSecureStorageBackend`. Web Secure scope is API-compatible but not encrypted by default; use a custom web secure backend when browser-side storage must meet a stricter threat model.
@@ -82,6 +82,5 @@ dependencies {
82
82
  //noinspection GradleDynamicVersion
83
83
  implementation "com.facebook.react:react-native:+"
84
84
  implementation "androidx.security:security-crypto:1.1.0"
85
- implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.0"
86
85
  implementation project(":react-native-nitro-modules")
87
86
  }
@@ -7,11 +7,8 @@
7
7
  public static *** has*(...);
8
8
  public static *** clear*(...);
9
9
  public static *** size*(...);
10
- public static *** flush*(...);
11
10
  public static void init(android.content.Context);
12
11
  public static void setSecureWritesAsync(boolean);
13
- public static void setSecureAccessControl(int);
14
- public static void removeByPrefix(java.lang.String, int);
15
12
  }
16
13
  -keep class com.nitrostorage.AndroidStorageAdapter$Companion {
17
14
  public <methods>;
@@ -69,10 +69,7 @@ std::vector<std::string> fromJavaStringArray(alias_ref<JavaStringArray> values)
69
69
 
70
70
  } // namespace
71
71
 
72
- AndroidStorageAdapterCpp::AndroidStorageAdapterCpp(alias_ref<JObject> /*context*/) {
73
- // Context is validated by AndroidStorageAdapter.getContext() on the Java side.
74
- // The adapter calls static Java methods directly via fbjni.
75
- }
72
+ AndroidStorageAdapterCpp::AndroidStorageAdapterCpp() = default;
76
73
 
77
74
  AndroidStorageAdapterCpp::~AndroidStorageAdapterCpp() = default;
78
75
 
@@ -9,16 +9,16 @@ namespace NitroStorage {
9
9
  struct AndroidStorageAdapterJava : facebook::jni::JavaClass<AndroidStorageAdapterJava> {
10
10
  static constexpr auto kJavaDescriptor = "Lcom/nitrostorage/AndroidStorageAdapter;";
11
11
 
12
- static facebook::jni::alias_ref<facebook::jni::JObject> getContext() {
12
+ static void ensureInitialized() {
13
13
  static auto method = javaClassStatic()->getStaticMethod<facebook::jni::JObject()>("getContext", "()Landroid/content/Context;");
14
- return method(javaClassStatic());
14
+ method(javaClassStatic());
15
15
  }
16
16
 
17
17
  };
18
18
 
19
19
  class AndroidStorageAdapterCpp : public NativeStorageAdapter {
20
20
  public:
21
- explicit AndroidStorageAdapterCpp(facebook::jni::alias_ref<facebook::jni::JObject> context);
21
+ AndroidStorageAdapterCpp();
22
22
  ~AndroidStorageAdapterCpp() override;
23
23
 
24
24
  void setDisk(const std::string& key, const std::string& value) override;