react-native-nitro-storage 0.10.3 → 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 +85 -14
  2. package/README.md +148 -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 +48 -33
  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 +22 -4
  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 +94 -17
  14. package/cpp/core/SqliteDiskStore.hpp +10 -0
  15. package/docs/api-reference.md +73 -28
  16. package/docs/benchmarks.md +41 -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 +149 -57
  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 +61 -22
  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 +62 -23
  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 +84 -33
  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,74 @@ 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
+
63
+ ## [0.10.4] - 2026-09-27
64
+
65
+ ### Breaking changes
66
+
67
+ - None.
68
+
69
+ ### Fixed
70
+
71
+ - Disk prefix queries and namespace removal now distinguish ASCII case, so clearing `user` does not remove `User` keys.
72
+ - Android Disk and Secure storage preserve embedded NUL characters in keys and values across scalar and batch operations, preventing truncated-key collisions.
73
+ - iOS Secure storage preserves embedded NUL keys and values, and legacy Disk migration no longer aliases a shorter host-app defaults key when a key contains NUL.
74
+ - Memory prefix reads return the original raw string, including values beginning with the reserved internal prefix.
75
+ - Raw enumeration, export, and set items preserve arbitrary keys such as `__proto__`, `constructor`, and `toString` as own properties.
76
+
9
77
  ## [0.10.3] - 2026-09-18
10
78
 
11
79
  ### Breaking changes
@@ -169,14 +237,17 @@ Breaking changes are always listed first in each release section.
169
237
  ### Changed
170
238
 
171
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.
172
- - 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.
173
241
  - `setIfVersion()` is documented as optimistic (no backend-level atomicity); CAS guarantees are covered by race tests.
174
242
 
175
243
  ## [0.7.0] - 2026-07-30
176
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
+
177
249
  ### Changes
178
250
 
179
- - **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.
180
251
  - Upgrade the validated package baseline to Expo SDK 57, React Native 0.86.2, and Nitro Modules/Nitrogen 0.36.4.
181
252
  - Preserve each item/value relationship in heterogeneous `setBatch()` calls so TypeScript rejects values assigned to the wrong storage item.
182
253
  - Serialize native key-index hydration with concurrent mutations so `has`, `size`, and key queries cannot remain stale after a racing write.
@@ -185,6 +256,15 @@ Breaking changes are always listed first in each release section.
185
256
 
186
257
  ## [0.6.0] - 2026-06-15
187
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
+
188
268
  ### Added
189
269
 
190
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.
@@ -200,16 +280,7 @@ Breaking changes are always listed first in each release section.
200
280
 
201
281
  ### Changed
202
282
 
203
- - 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).
204
-
205
- ### Breaking Changes
206
-
207
- All new APIs are additive — existing code keeps working. These behavior and
208
- type changes can affect advanced consumers:
209
-
210
- - 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()`).
211
- - `StorageChangeOperation` gained the `"expire"` and `"clearGroup"` members. Exhaustive `switch` statements over a change event's `operation` need cases for the new members.
212
- - `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).
213
284
 
214
285
  ## [0.5.9] - 2026-06-11
215
286
 
@@ -242,7 +313,7 @@ type changes can affect advanced consumers:
242
313
 
243
314
  ### Changed
244
315
 
245
- - 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.
246
317
  - Refactor iOS Secure set/get/delete helpers so single-item and batch paths share Keychain status handling and cache updates.
247
318
  - Strengthen TypeScript inference parity on web by exporting `StorageSetter` and preserving tuple value types from `getBatch()`.
248
319
 
@@ -428,7 +499,7 @@ type changes can affect advanced consumers:
428
499
  - Group secure raw batch writes by per-item access control so secure batch paths stay fast even with mixed access-control settings.
429
500
  - Optimize C++ batch listener dispatch by copying scoped listeners once per batch operation.
430
501
  - Avoid duplicate secure biometric clearing calls by relying on secure clear paths that already include biometric cleanup.
431
- - 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`).
432
503
  - Improve iOS secure key union performance by deduplicating with an `unordered_set`.
433
504
  - Extract shared React hooks into `src/storage-hooks.ts` to reduce native/web entrypoint duplication.
434
505
  - Expand benchmark coverage to include Disk and Secure scope throughput checks and tighten regression thresholds.
package/README.md CHANGED
@@ -35,6 +35,7 @@ pagination, conflict resolution, or remote synchronization.
35
35
  - [Legacy Key Migration And Secure Resilience](#legacy-key-migration-and-secure-resilience)
36
36
  - [React Hooks](#react-hooks)
37
37
  - [Storage Scopes](#storage-scopes)
38
+ - [Prefix Queries](#prefix-queries)
38
39
  - [Secure Storage](#secure-storage)
39
40
  - [Batch Operations](#batch-operations)
40
41
  - [Events And Observability](#events-and-observability)
@@ -60,15 +61,28 @@ Peer dependencies:
60
61
  | Package | Version |
61
62
  | ---------------------------- | ------------------ |
62
63
  | `react` | `>=18.2.0` |
63
- | `react-native` | `>=0.75.0` |
64
+ | `react-native` | `>=0.77.0` |
64
65
  | `react-native-nitro-modules` | `>=0.37.0 <0.38.0` |
65
66
 
66
67
  Nitro peer requirement: `react-native-nitro-modules >=0.37.0 <0.38.0`.
67
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
+
68
82
  The package gate uses React Native `0.86.3` and the Strict TypeScript API.
69
83
  `check:ci` also compiles the public source against React Native `0.87.0`'s
70
84
  Strict TypeScript API; this does not change the runtime baseline. The Expo
71
- example uses Expo SDK `57.0.24`, React Native
85
+ example uses Expo SDK `57.0.26`, React Native
72
86
  `0.86.3`, React `19.2.3`, and Nitro Modules `0.37.1`, which is the React Native
73
87
  version supported by that Expo SDK. Do not override Expo's React Native version.
74
88
 
@@ -77,7 +91,7 @@ before installing this package, then rebuild the native app so the generated
77
91
  Nitro bindings and native runtime use the same major-minor version:
78
92
 
79
93
  ```sh
80
- bun add react-native-nitro-modules@0.37.1 react-native-nitro-storage@0.10.3
94
+ bun add react-native-nitro-modules@0.37.1 react-native-nitro-storage@0.11.0
81
95
  bunx expo prebuild
82
96
  ```
83
97
 
@@ -124,11 +138,15 @@ Add the config plugin before prebuilding native iOS and Android projects:
124
138
  }
125
139
  ```
126
140
 
127
- | Option | Default | What it does |
128
- | ------------------------- | ------------------------ | -------------------------------------------------------------- |
129
- | `faceIDPermission` | Built-in Face ID message | Sets `NSFaceIDUsageDescription`. |
130
- | `addBiometricPermissions` | `false` | Adds Android biometric and fingerprint permissions. |
131
- | `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.
132
150
 
133
151
  Android adapter initialization is owned by the package through an Android
134
152
  manifest initializer, so apps should not edit `MainApplication` to call
@@ -164,7 +182,10 @@ application state.
164
182
  Native storage calls are synchronous JSI operations. Keep values and batches
165
183
  small enough for the JavaScript event loop; native and configured web-backend
166
184
  failures throw errors. Secure cache fallback is opt-in through
167
- `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.
168
189
 
169
190
  ## Auth Tokens
170
191
 
@@ -193,9 +214,9 @@ boundary. `createSecureAuthStorage` already namespaces keys, notifies
193
214
  subscribers, and migrates legacy keys.
194
215
 
195
216
  Do not enable `fallbackToCacheOnReadError` for access or refresh tokens unless
196
- the application explicitly accepts stale or revoked credentials. Handle
197
- temporary secure-storage errors and retry from application lifecycle state
198
- 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.
199
220
 
200
221
  ## Typed Storage Items
201
222
 
@@ -309,8 +330,11 @@ storage.clear(StorageScope.Disk, {
309
330
  ## Legacy Key Migration And Secure Resilience
310
331
 
311
332
  `renameFrom` migrates an old key to a new one on first read and deletes the
312
- legacy entry. Secure items can fall back to the last cached value when the
313
- 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.
314
338
 
315
339
  ```ts
316
340
  import {
@@ -318,11 +342,11 @@ import {
318
342
  createSecureAuthStorage,
319
343
  } from "react-native-nitro-storage";
320
344
 
321
- const accessToken = secureItem<string>({
322
- key: "accessToken",
323
- namespace: "auth",
324
- defaultValue: "",
325
- 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
326
350
  fallbackToCacheOnReadError: true,
327
351
  onReadError: (error) => reportSecureReadError(error),
328
352
  });
@@ -332,7 +356,7 @@ const auth = createSecureAuthStorage(
332
356
  accessToken: { renameFrom: "authToken" },
333
357
  refreshToken: { renameFrom: "refreshToken" },
334
358
  },
335
- { namespace: "auth", group: "session", fallbackToCacheOnReadError: true },
359
+ { namespace: "auth", group: "session" },
336
360
  );
337
361
  ```
338
362
 
@@ -393,6 +417,26 @@ const tokenActions = useStorageActions(tokenItem); // { set, merge, reset, remov
393
417
  | `StorageScope.Disk` | SQLite WAL on iOS/Android (imports UserDefaults / SharedPreferences once); configured web backend | Preferences, feature flags, onboarding state, and non-secret persisted data. |
394
418
  | `StorageScope.Secure` | Keychain on iOS, Android Keystore-backed preferences | Refresh tokens, credentials, API tokens, and biometric-protected values. |
395
419
 
420
+ ## Prefix Queries
421
+
422
+ Prefix queries use literal, case-sensitive matching on every backend. `User::`
423
+ and `user::` are separate namespaces; `%`, `_`, and `\\` are literal characters.
424
+ Raw enumeration returns the original strings and preserves arbitrary keys as own
425
+ properties on an ordinary object, including `__proto__`.
426
+
427
+ Disk and Secure strings preserve embedded NUL characters in keys and values,
428
+ including batch operations. Earlier versions could truncate strings at Android
429
+ JNI or iOS Foundation boundaries; this fix cannot reconstruct previously lost
430
+ suffixes. iOS legacy Disk migration also preserves full keys without aliasing
431
+ shorter host-app defaults keys.
432
+
433
+ ```ts
434
+ storage.setString("User::theme", "dark", StorageScope.Disk);
435
+ storage.setString("user::theme", "light", StorageScope.Disk);
436
+ const settings = storage.getByPrefix("User::", StorageScope.Disk);
437
+ // settings["User::theme"] === "dark"; no lowercase namespace entries.
438
+ ```
439
+
396
440
  ## Secure Storage
397
441
 
398
442
  ```ts
@@ -467,6 +511,33 @@ can also throw when a protected store is locked or its key is invalidated. Use
467
511
  `authentication_required`, and rebuild the affected credential for
468
512
  `key_invalidated`.
469
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
+
470
541
  ## Batch Operations
471
542
 
472
543
  `getBatch()` preserves tuple value types, so IDEs infer each result from the
@@ -506,9 +577,17 @@ removeBatch([themeItem, localeItem], StorageScope.Disk);
506
577
  ## Events And Observability
507
578
 
508
579
  ```ts
509
- const unsubscribe = storage.subscribeNamespace("settings", (event) => {
510
- console.log(event.key, event.operation, event.source);
511
- });
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
+ );
512
591
 
513
592
  storage.setEventObserver((event) => {
514
593
  console.log(event.type, event.scope);
@@ -528,9 +607,10 @@ unsubscribe();
528
607
  `getMetricsSnapshot()` aggregates each operation across scopes for backward
529
608
  compatibility. `getScopedMetricsSnapshot()` adds the numeric scope suffix for
530
609
  per-scope analysis, for example `item:set:1`. `getCacheMetrics()` reports live
531
- Disk/Secure raw-cache hits, misses, entries, and estimated bytes. The cache is
532
- unbounded; `resetMetrics()` zeros the hit/miss counters and leaves entries in
533
- 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.
534
614
 
535
615
  The example app includes hidden integrity, keychain, and Disk/Secure stress
536
616
  labs at `nitrostorage://e2e-integrity`, `nitrostorage://e2e-keychain`, and
@@ -600,12 +680,19 @@ import {
600
680
  } from "react-native-nitro-storage";
601
681
  import { createIndexedDBBackend } from "react-native-nitro-storage/indexeddb-backend";
602
682
 
603
- const backend = await createIndexedDBBackend("app-storage", "kv");
683
+ const diskBackend = await createIndexedDBBackend("app-storage", "disk");
684
+ const secureBackend = await createIndexedDBBackend("app-storage", "secure");
604
685
 
605
- setWebDiskStorageBackend(backend);
606
- setWebSecureStorageBackend(backend);
686
+ setWebDiskStorageBackend(diskBackend);
687
+ setWebSecureStorageBackend(secureBackend);
607
688
  ```
608
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
+
609
696
  Web reads and mutations stay synchronous against the backend's in-memory
610
697
  contract; use `flushWebStorageBackends()` for asynchronous persistence
611
698
  boundaries. The native entry keeps the web backend setters, getters, and flush
@@ -614,11 +701,19 @@ function as typed no-ops for cross-platform code.
614
701
  Browser storage cannot provide iOS Keychain or Android Keystore guarantees. Web
615
702
  Secure scope is only as strong as the backend you configure.
616
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
+
617
708
  ## Testing
618
709
 
619
- The `react-native-nitro-storage/testing` entrypoint is a faithful in-memory
620
- implementation of the full public surface, so unit tests and Storybook run
621
- 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.
622
717
 
623
718
  ```ts
624
719
  import {
@@ -653,13 +748,23 @@ backends. The full reference lives in
653
748
  ## Error Contract
654
749
 
655
750
  Native and web adapters tag classified failures with stable error codes. Use
656
- `getStorageErrorCode(error)` or `isStorageError(error, code)` to branch on them:
657
- `keychain_locked` reports a locked Keychain that a retry can recover after
658
- authentication, secure-scope write or biometric failures carry their own
659
- codes, and invalid inputs (bad scope, malformed keys, numeric guard
660
- violations) are rejected before reaching native storage. Errors never swallow
661
- the underlying cause silently: the original platform message is preserved on
662
- 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.
663
768
 
664
769
  ## Platform Support
665
770
 
@@ -695,8 +800,10 @@ the error for diagnostics.
695
800
  upgrading the package so the Android manifest initializer is merged.
696
801
  - **Secure values fail after Android restore:** keep `configureAndroidBackup:
697
802
  true` or provide equivalent backup exclusions.
698
- - **Biometric prompt does not appear:** set `biometric: true` on the item and
699
- 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.
700
807
  - **Web secure storage is unavailable:** configure a secure backend before using
701
808
  Secure scope on web.
702
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>;