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.
- package/CHANGELOG.md +85 -14
- package/README.md +148 -41
- package/SECURITY.md +16 -6
- package/android/build.gradle +0 -1
- package/android/consumer-rules.pro +0 -3
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +48 -33
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -3
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +73 -23
- package/android/src/main/java/com/nitrostorage/DiskSqliteStore.kt +22 -4
- package/app.plugin.js +51 -51
- package/cpp/bindings/HybridStorage.cpp +25 -6
- package/cpp/core/NativeStorageAdapter.hpp +14 -1
- package/cpp/core/SqliteDiskStore.cpp +94 -17
- package/cpp/core/SqliteDiskStore.hpp +10 -0
- package/docs/api-reference.md +73 -28
- package/docs/benchmarks.md +41 -12
- package/docs/mmkv-migration.md +3 -1
- package/docs/native-libraries.md +10 -4
- package/docs/secure-storage.md +26 -12
- package/docs/web-backends.md +6 -2
- package/indexeddb-backend/package.json +6 -0
- package/ios/IOSStorageAdapterCpp.hpp +4 -0
- package/ios/IOSStorageAdapterCpp.mm +149 -57
- package/lib/commonjs/capabilities.js +3 -8
- package/lib/commonjs/capabilities.js.map +1 -1
- package/lib/commonjs/index.js +23 -8
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +61 -19
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +21 -37
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/internal.js +6 -0
- package/lib/commonjs/internal.js.map +1 -1
- package/lib/commonjs/storage-core.js +61 -22
- package/lib/commonjs/storage-core.js.map +1 -1
- package/lib/commonjs/storage-runtime.js +1 -1
- package/lib/commonjs/storage-runtime.js.map +1 -1
- package/lib/commonjs/testing.js +68 -23
- package/lib/commonjs/testing.js.map +1 -1
- package/lib/commonjs/web-backend-contract.js +6 -2
- package/lib/commonjs/web-backend-contract.js.map +1 -1
- package/lib/module/capabilities.js +3 -8
- package/lib/module/capabilities.js.map +1 -1
- package/lib/module/index.js +14 -5
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +61 -12
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +21 -37
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/internal.js +5 -0
- package/lib/module/internal.js.map +1 -1
- package/lib/module/storage-core.js +62 -23
- package/lib/module/storage-core.js.map +1 -1
- package/lib/module/storage-runtime.js +1 -1
- package/lib/module/storage-runtime.js.map +1 -1
- package/lib/module/testing.js +59 -16
- package/lib/module/testing.js.map +1 -1
- package/lib/module/web-backend-contract.js +5 -2
- package/lib/module/web-backend-contract.js.map +1 -1
- package/lib/typescript/capabilities.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +8 -1
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +7 -1
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/internal.d.ts +1 -0
- package/lib/typescript/internal.d.ts.map +1 -1
- package/lib/typescript/storage-core.d.ts +1 -1
- package/lib/typescript/storage-core.d.ts.map +1 -1
- package/lib/typescript/storage-runtime.d.ts +1 -1
- package/lib/typescript/storage-runtime.d.ts.map +1 -1
- package/lib/typescript/testing.d.ts +15 -2
- package/lib/typescript/testing.d.ts.map +1 -1
- package/lib/typescript/web-backend-contract.d.ts +1 -0
- package/lib/typescript/web-backend-contract.d.ts.map +1 -1
- package/package.json +14 -5
- package/react-native-nitro-storage.podspec +4 -2
- package/src/capabilities.ts +4 -9
- package/src/index.ts +19 -5
- package/src/index.web.ts +86 -19
- package/src/indexeddb-backend.ts +20 -42
- package/src/internal.ts +8 -0
- package/src/storage-core.ts +84 -33
- package/src/storage-runtime.ts +3 -1
- package/src/testing.ts +91 -18
- package/src/web-backend-contract.ts +6 -2
- 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
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
197
|
-
temporary secure-storage errors and retry from application
|
|
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
|
|
313
|
-
|
|
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
|
|
322
|
-
key: "
|
|
323
|
-
namespace: "
|
|
324
|
-
defaultValue:
|
|
325
|
-
renameFrom: "
|
|
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"
|
|
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(
|
|
510
|
-
|
|
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.
|
|
532
|
-
|
|
533
|
-
|
|
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
|
|
683
|
+
const diskBackend = await createIndexedDBBackend("app-storage", "disk");
|
|
684
|
+
const secureBackend = await createIndexedDBBackend("app-storage", "secure");
|
|
604
685
|
|
|
605
|
-
setWebDiskStorageBackend(
|
|
606
|
-
setWebSecureStorageBackend(
|
|
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
|
|
620
|
-
implementation
|
|
621
|
-
without native modules.
|
|
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
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
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
|
|
699
|
-
|
|
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
|
|
8
|
-
|
|
|
9
|
-
| `0.
|
|
10
|
-
| `<0.
|
|
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
|
|
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
|
-
|
|
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.
|
package/android/build.gradle
CHANGED
|
@@ -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>;
|