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.
- package/CHANGELOG.md +71 -14
- package/README.md +127 -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 +1 -4
- 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 +15 -0
- 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 +84 -11
- package/cpp/core/SqliteDiskStore.hpp +10 -0
- package/docs/api-reference.md +73 -28
- package/docs/benchmarks.md +4 -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 +96 -18
- 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 +35 -11
- 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 +36 -12
- 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 +52 -19
- 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,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
|
|
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
|
-
-
|
|
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
|
-
-
|
|
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 (
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
198
|
-
temporary secure-storage errors and retry from application
|
|
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
|
|
314
|
-
|
|
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
|
|
323
|
-
key: "
|
|
324
|
-
namespace: "
|
|
325
|
-
defaultValue:
|
|
326
|
-
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
|
|
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"
|
|
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(
|
|
531
|
-
|
|
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.
|
|
553
|
-
|
|
554
|
-
|
|
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
|
|
683
|
+
const diskBackend = await createIndexedDBBackend("app-storage", "disk");
|
|
684
|
+
const secureBackend = await createIndexedDBBackend("app-storage", "secure");
|
|
625
685
|
|
|
626
|
-
setWebDiskStorageBackend(
|
|
627
|
-
setWebSecureStorageBackend(
|
|
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
|
|
641
|
-
implementation
|
|
642
|
-
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.
|
|
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
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
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
|
|
720
|
-
|
|
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
|
|
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>;
|
|
@@ -69,10 +69,7 @@ std::vector<std::string> fromJavaStringArray(alias_ref<JavaStringArray> values)
|
|
|
69
69
|
|
|
70
70
|
} // namespace
|
|
71
71
|
|
|
72
|
-
AndroidStorageAdapterCpp::AndroidStorageAdapterCpp(
|
|
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
|
|
12
|
+
static void ensureInitialized() {
|
|
13
13
|
static auto method = javaClassStatic()->getStaticMethod<facebook::jni::JObject()>("getContext", "()Landroid/content/Context;");
|
|
14
|
-
|
|
14
|
+
method(javaClassStatic());
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
};
|
|
18
18
|
|
|
19
19
|
class AndroidStorageAdapterCpp : public NativeStorageAdapter {
|
|
20
20
|
public:
|
|
21
|
-
|
|
21
|
+
AndroidStorageAdapterCpp();
|
|
22
22
|
~AndroidStorageAdapterCpp() override;
|
|
23
23
|
|
|
24
24
|
void setDisk(const std::string& key, const std::string& value) override;
|