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/docs/api-reference.md
CHANGED
|
@@ -14,28 +14,28 @@ const item = createStorageItem<T>({
|
|
|
14
14
|
|
|
15
15
|
`StorageItemConfig<T>`:
|
|
16
16
|
|
|
17
|
-
| Field | Type | Purpose
|
|
18
|
-
| ---------------------------- | -------------------------------- |
|
|
19
|
-
| `key` | `string` | Storage key. Combined with `namespace` when provided.
|
|
20
|
-
| `scope` | `StorageScope` | Memory, Disk, or Secure.
|
|
21
|
-
| `defaultValue` | `T` | Value returned when no stored value exists.
|
|
22
|
-
| `serialize` | `(value: T) => string` | Custom string encoder. Defaults to primitive/JSON serialization.
|
|
23
|
-
| `deserialize` | `(value: string) => T` | Custom string decoder.
|
|
24
|
-
| `validate` | `(value: unknown) => value is T` | Runtime guard for stored data.
|
|
25
|
-
| `onValidationError` | `(invalidValue: unknown) => T` | Replacement value when validation fails.
|
|
26
|
-
| `expiration` | `{ ttlMs: number }` | Time-to-live for the value.
|
|
27
|
-
| `onExpired` | `(key: string) => void` | Called when a read detects TTL expiry.
|
|
28
|
-
| `readCache` | `boolean` | Reuse raw cache entries for reads, including cached missing values. |
|
|
29
|
-
| `coalesceDiskWrites` | `boolean` | Buffer Disk writes until the next flush.
|
|
30
|
-
| `coalesceSecureWrites` | `boolean` | Buffer Secure writes until the next flush.
|
|
31
|
-
| `namespace` | `string` | Prefix keys as `namespace:key`.
|
|
32
|
-
| `biometric` | `boolean` | Store through biometric secure storage.
|
|
33
|
-
| `biometricLevel` | `BiometricLevel` | Require biometric/passcode or biometric-only access.
|
|
34
|
-
| `accessControl` | `AccessControl` | Platform secure accessibility setting.
|
|
35
|
-
| `group` | `string` | Register the item for group cleanup and inspection.
|
|
36
|
-
| `renameFrom` | `string \| readonly string[]` | Copy a legacy key on first read, then remove it.
|
|
37
|
-
| `fallbackToCacheOnReadError` | `boolean` | Return the last cached value when a backend read fails.
|
|
38
|
-
| `onReadError` | `(error: unknown) => void` | Observe a backend read failure before fallback or rethrow.
|
|
17
|
+
| Field | Type | Purpose |
|
|
18
|
+
| ---------------------------- | -------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
19
|
+
| `key` | `string` | Storage key. Combined with `namespace` when provided. Must not be empty (`invalid_key`). |
|
|
20
|
+
| `scope` | `StorageScope` | Memory, Disk, or Secure. |
|
|
21
|
+
| `defaultValue` | `T` | Value returned when no stored value exists. |
|
|
22
|
+
| `serialize` | `(value: T) => string` | Custom string encoder. Defaults to primitive/JSON serialization. |
|
|
23
|
+
| `deserialize` | `(value: string) => T` | Custom string decoder. |
|
|
24
|
+
| `validate` | `(value: unknown) => value is T` | Runtime guard for stored data. |
|
|
25
|
+
| `onValidationError` | `(invalidValue: unknown) => T` | Replacement value when validation fails. |
|
|
26
|
+
| `expiration` | `{ ttlMs: number }` | Time-to-live for the value. |
|
|
27
|
+
| `onExpired` | `(key: string) => void` | Called when a read detects TTL expiry. |
|
|
28
|
+
| `readCache` | `boolean` | Reuse raw cache entries for reads, including cached missing values. Items without `readCache` or `fallbackToCacheOnReadError` do not fill the cache on read. |
|
|
29
|
+
| `coalesceDiskWrites` | `boolean` | Buffer Disk writes until the next flush. |
|
|
30
|
+
| `coalesceSecureWrites` | `boolean` | Buffer Secure writes until the next flush. |
|
|
31
|
+
| `namespace` | `string` | Prefix keys as `namespace:key`. |
|
|
32
|
+
| `biometric` | `boolean` | Store through biometric secure storage. |
|
|
33
|
+
| `biometricLevel` | `BiometricLevel` | Require biometric/passcode or biometric-only access. |
|
|
34
|
+
| `accessControl` | `AccessControl` | Platform secure accessibility setting. |
|
|
35
|
+
| `group` | `string` | Register the item for group cleanup and inspection. |
|
|
36
|
+
| `renameFrom` | `string \| readonly string[]` | Copy a legacy key on first read, then remove it. |
|
|
37
|
+
| `fallbackToCacheOnReadError` | `boolean` | Return the last cached value when a backend read fails with `keychain_locked`. Other errors still throw. Only iOS reports `keychain_locked`, so this has no effect on Android or web. |
|
|
38
|
+
| `onReadError` | `(error: unknown) => void` | Observe a backend read failure before fallback or rethrow. |
|
|
39
39
|
|
|
40
40
|
`StorageItem<T>`:
|
|
41
41
|
|
|
@@ -65,6 +65,20 @@ const unsubscribe = profileItem.subscribeSelector(
|
|
|
65
65
|
);
|
|
66
66
|
```
|
|
67
67
|
|
|
68
|
+
## Scoped Item Factories
|
|
69
|
+
|
|
70
|
+
`memoryItem(config)`, `diskItem(config)`, and `secureItem(config)` take the same
|
|
71
|
+
config as `createStorageItem` without the `scope` field.
|
|
72
|
+
|
|
73
|
+
```ts
|
|
74
|
+
const draft = memoryItem<string>({ key: "draft", defaultValue: "" });
|
|
75
|
+
const theme = diskItem<"light" | "dark">({
|
|
76
|
+
key: "theme",
|
|
77
|
+
defaultValue: "light",
|
|
78
|
+
});
|
|
79
|
+
const token = secureItem<string>({ key: "token", defaultValue: "" });
|
|
80
|
+
```
|
|
81
|
+
|
|
68
82
|
## createSetItem
|
|
69
83
|
|
|
70
84
|
```ts
|
|
@@ -85,9 +99,11 @@ flags.getTyped();
|
|
|
85
99
|
## React Hooks
|
|
86
100
|
|
|
87
101
|
```ts
|
|
88
|
-
const [value, setValue] = useStorage(item);
|
|
102
|
+
const [value, setValue, actions] = useStorage(item);
|
|
89
103
|
const [selected, setItem] = useStorageSelector(item, selector, isEqual);
|
|
90
104
|
const setOnly = useSetStorage(item);
|
|
105
|
+
const readOnly = useStorageValue(item);
|
|
106
|
+
const writeOnly = useStorageActions(item);
|
|
91
107
|
```
|
|
92
108
|
|
|
93
109
|
See [react-hooks.md](react-hooks.md).
|
|
@@ -128,7 +144,8 @@ See [react-hooks.md](react-hooks.md).
|
|
|
128
144
|
| `getMetricsSnapshot()` | Read aggregated metrics. |
|
|
129
145
|
| `getScopedMetricsSnapshot()` | Read metrics grouped by storage scope. |
|
|
130
146
|
| `resetMetrics()` | Clear metrics counters. |
|
|
131
|
-
| `
|
|
147
|
+
| `getCacheMetrics()` | Read raw-cache hits, misses, live entries, and estimated bytes. |
|
|
148
|
+
| `getCapabilities()` | Read runtime storage capabilities. Native `backend.disk` is `"sqlite"`. |
|
|
132
149
|
| `getSecurityCapabilities()` | Read secure backend capability metadata. |
|
|
133
150
|
| `getSecureMetadata(key)` | Read secure metadata for one key without returning its value. |
|
|
134
151
|
| `getAllSecureMetadata()` | Read secure metadata for all secure keys without values. |
|
|
@@ -259,6 +276,21 @@ the native or web adapter. `isStorageError(error, code)` matches one exact code
|
|
|
259
276
|
without parsing platform message text. See [secure-storage.md](secure-storage.md)
|
|
260
277
|
for recovery semantics.
|
|
261
278
|
|
|
279
|
+
| Code | Raised when |
|
|
280
|
+
| ----------------------------- | ----------------------------------------------------------------------------------------------------------------------- |
|
|
281
|
+
| `keychain_locked` | The protected store is locked. |
|
|
282
|
+
| `authentication_required` | The item needs user authentication, or the user cancelled the prompt. |
|
|
283
|
+
| `key_invalidated` | The protecting key was invalidated, for example by a biometric enrolment change. |
|
|
284
|
+
| `biometric_unavailable` | The requested biometric level is not available on this device or OS version. |
|
|
285
|
+
| `storage_corruption` | Stored secure data could not be decrypted, or (Android) the Secure master key or store cannot be created. Do not retry. |
|
|
286
|
+
| `storage_compensation_failed` | A multi-step write failed and restoring the previous state also failed. |
|
|
287
|
+
| `unsupported` | The operation is not available on this platform or environment. |
|
|
288
|
+
| `invalid_key` | A storage key is empty. |
|
|
289
|
+
|
|
290
|
+
`StorageCompositeError` and `StorageCompensationError` describe errors that
|
|
291
|
+
carry a primary failure plus secondary failures from reconciliation or
|
|
292
|
+
compensation steps.
|
|
293
|
+
|
|
262
294
|
`isKeychainLockedError(error)` is deprecated. It remains available for
|
|
263
295
|
compatibility and returns `true` for `keychain_locked`,
|
|
264
296
|
`authentication_required`, and `key_invalidated`.
|
|
@@ -273,9 +305,10 @@ getWebSecureStorageBackend();
|
|
|
273
305
|
await flushWebStorageBackends();
|
|
274
306
|
```
|
|
275
307
|
|
|
276
|
-
|
|
277
|
-
`
|
|
278
|
-
|
|
308
|
+
Every entry, including native and `/testing`, exports
|
|
309
|
+
`describeWebBackendCapabilities(backend)` and `isIndexedDBWebBackend(backend)`.
|
|
310
|
+
The native and testing entries keep the web backend setters, getters, and flush
|
|
311
|
+
function as typed no-ops for shared code.
|
|
279
312
|
|
|
280
313
|
See [web-backends.md](web-backends.md).
|
|
281
314
|
|
|
@@ -339,6 +372,18 @@ Common public types:
|
|
|
339
372
|
- `PlatformStorage`
|
|
340
373
|
- `PlatformScope`
|
|
341
374
|
- `WebBackendCapabilities`
|
|
375
|
+
- `StorageCapabilities`
|
|
376
|
+
- `StorageCacheMetrics`
|
|
377
|
+
- `StorageExportOptions`
|
|
378
|
+
- `StorageEventObserverOptions`
|
|
379
|
+
- `StorageCompositeError`
|
|
380
|
+
- `StorageCompensationError`
|
|
381
|
+
- `SetStorageItem<T>`
|
|
382
|
+
- `SetItemConfig<T>`
|
|
383
|
+
- `StorageClearOptions`
|
|
384
|
+
- `StorageKeyRef`
|
|
385
|
+
- `StorageActions<T>`
|
|
386
|
+
- `StorageSetter<T>`
|
|
342
387
|
|
|
343
388
|
`getCapabilities().writeBuffering` describes real per-mode durability:
|
|
344
389
|
|
|
@@ -350,4 +395,4 @@ Common public types:
|
|
|
350
395
|
|
|
351
396
|
`describeWebBackendCapabilities(backend)` reports a backend's `buffered`, `flushable`, `closable`, and `subscribable` capabilities from the same typed contract used by the built-in backends.
|
|
352
397
|
|
|
353
|
-
The IndexedDB subpath exports `createIndexedDBBackend()` and `IndexedDBBackendOptions`.
|
|
398
|
+
The IndexedDB subpath exports `createIndexedDBBackend()` and `IndexedDBBackendOptions`. The root export of `createIndexedDBBackend` is deprecated; import it from `react-native-nitro-storage/indexeddb-backend`.
|
package/docs/benchmarks.md
CHANGED
|
@@ -28,22 +28,14 @@ Native Disk/Secure baselines require a device or simulator run and are not part
|
|
|
28
28
|
|
|
29
29
|
## Release Checklist
|
|
30
30
|
|
|
31
|
-
Before publishing:
|
|
31
|
+
Before publishing, run the full release gate:
|
|
32
32
|
|
|
33
33
|
```sh
|
|
34
|
-
bun run
|
|
35
|
-
bun run lint:check
|
|
36
|
-
bun run format:check
|
|
37
|
-
bun run typecheck
|
|
38
|
-
bun run test:types
|
|
39
|
-
bun run test
|
|
40
|
-
bun run test:cpp
|
|
41
|
-
bun run build
|
|
42
|
-
bun run benchmark
|
|
43
|
-
bun run --cwd packages/react-native-nitro-storage check:pack
|
|
34
|
+
bun run release:preflight
|
|
44
35
|
```
|
|
45
36
|
|
|
46
|
-
|
|
37
|
+
It runs `check:ci` (including the benchmark), the example checks, the package
|
|
38
|
+
audit, and the publish dry run.
|
|
47
39
|
|
|
48
40
|
## 2026-09-27 Memory enumeration experiment
|
|
49
41
|
|
package/docs/mmkv-migration.md
CHANGED
|
@@ -4,12 +4,14 @@ Use `migrateFromMMKV(mmkv, item, deleteAfterMigration?)` when an app already sto
|
|
|
4
4
|
|
|
5
5
|
The helper reads in this order:
|
|
6
6
|
|
|
7
|
-
1. `mmkv.getString(key)`
|
|
7
|
+
1. `mmkv.getString(key)`, then `JSON.parse` on the string. If parsing succeeds, the parsed value is written; otherwise the raw string is written.
|
|
8
8
|
2. `mmkv.getNumber(key)`
|
|
9
9
|
3. `mmkv.getBoolean(key)`
|
|
10
10
|
|
|
11
11
|
It writes through `item.set()`, so custom serialization, validation, TTL behavior, and listeners remain active.
|
|
12
12
|
|
|
13
|
+
Because of the `JSON.parse` step, a string item can receive a non-string value: MMKV strings such as `"12345"`, `"true"`, or `"null"` are written as a number, a boolean, or `null`, while the item type still says `string`. For string items, add `validate` with an `onValidationError` fallback, or copy the raw value with `storage.setString(key, mmkv.getString(key), scope)` instead.
|
|
14
|
+
|
|
13
15
|
## Basic Migration
|
|
14
16
|
|
|
15
17
|
```ts
|
package/docs/native-libraries.md
CHANGED
|
@@ -6,12 +6,18 @@ EncryptedSharedPreferences. Web Disk stays on the configured web backend
|
|
|
6
6
|
|
|
7
7
|
## Kept
|
|
8
8
|
|
|
9
|
-
| Library | Where | Why
|
|
10
|
-
| ---------- | ------------------------------------------------ |
|
|
11
|
-
| SQLite WAL | iOS `SqliteDiskStore`, Android `DiskSqliteStore` | Transactional batch writes, prefix queries
|
|
9
|
+
| Library | Where | Why |
|
|
10
|
+
| ---------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
11
|
+
| SQLite WAL | iOS `SqliteDiskStore`, Android `DiskSqliteStore` | Transactional batch writes, literal prefix queries in SQL, and crash-safe persistence. Closest Disk engine to a dedicated mmap KV without adding MMKV. |
|
|
12
12
|
|
|
13
13
|
Existing UserDefaults suite keys and Android `NitroStorage` preferences are
|
|
14
|
-
copied into SQLite on first open.
|
|
14
|
+
copied into SQLite once, on first open. A marker in the SQLite `meta` table
|
|
15
|
+
(`suite_v1` on iOS, `prefs_v1` on Android) records completion, so later launches
|
|
16
|
+
skip the import. On iOS the suite domain is kept for downgrade safety, and Disk
|
|
17
|
+
key enumeration still merges its keys. On Android the legacy `NitroStorage`
|
|
18
|
+
preferences are kept after the import for downgrade safety, like the iOS suite;
|
|
19
|
+
Disk deletes remove the key from them and `clear(Disk)` clears them, so a logout
|
|
20
|
+
wipe leaves no pre-SQLite copy behind. Later Disk reads and writes use SQLite.
|
|
15
21
|
|
|
16
22
|
## Evaluated and not shipped
|
|
17
23
|
|
package/docs/secure-storage.md
CHANGED
|
@@ -47,7 +47,14 @@ export const recoveryCodeItem = createStorageItem<string>({
|
|
|
47
47
|
|
|
48
48
|
`BiometricLevel.BiometryOnly` does not allow passcode fallback. Use `BiometricLevel.BiometryOrPasscode` when passcode fallback is acceptable.
|
|
49
49
|
|
|
50
|
-
On Android 11 and newer, the two levels use separate Android Keystore keys with distinct allowed authenticators. Android 10 and older support `BiometryOrPasscode`; `BiometryOnly` reports `biometric_unavailable` because the older Keystore API cannot enforce that distinction safely for this storage backend.
|
|
50
|
+
On Android 11 and newer, the two levels use separate Android Keystore keys with distinct allowed authenticators. Android 10 and older support `BiometryOrPasscode`; `BiometryOnly` reports `biometric_unavailable` because the older Keystore API cannot enforce that distinction safely for this storage backend.
|
|
51
|
+
|
|
52
|
+
### Platform prompt behaviour
|
|
53
|
+
|
|
54
|
+
- **iOS:** reading a biometric item runs `SecItemCopyMatching` with user interaction allowed. The system shows the Face ID, Touch ID, or passcode sheet, and the synchronous JSI call blocks the JavaScript thread until the user answers. JavaScript timers, JavaScript-driven animations, and other JSI calls wait during that time. Read biometric items from a user action, not during render. Deleting a biometric item does not read it first unless an event listener or an unredacted event observer needs the previous value, so a delete does not show a prompt.
|
|
55
|
+
- **Android errors:** if the default Secure master key or store cannot be created (for example a Keystore key that exists but is unusable), Secure calls throw `storage_corruption`. This is not a temporary state: do not retry in a loop. Secure scope stays unavailable until the app data is cleared or the app is reinstalled; Memory and Disk keep working.
|
|
56
|
+
- **Android:** Nitro Storage never shows a prompt. Each biometric level is an `EncryptedSharedPreferences` file whose Tink keyset is wrapped by an Android Keystore key that requires user authentication within the last 30 seconds. That key is used only when the store is first opened in a process: `EncryptedSharedPreferences` decrypts the keyset once and keeps it in memory, and later reads and writes in the same process do not check authentication again. After the first successful open, biometric values stay readable without authentication until the process ends.
|
|
57
|
+
- **What Android apps must do:** run `androidx.biometric.BiometricPrompt` in the app before every read that must be gated, and treat the storage check as a one-time guard per process. Enable the config plugin option `addBiometricPermissions` so the app can declare `USE_BIOMETRIC` and `USE_FINGERPRINT` for its own prompt. Reading a `BiometryOrPasscode` item opens only the `BiometryOrPasscode` store, so a device-credential authentication is enough for that level.
|
|
51
58
|
|
|
52
59
|
## Access Control
|
|
53
60
|
|
|
@@ -61,6 +68,8 @@ On Android 11 and newer, the two levels use separate Android Keystore keys with
|
|
|
61
68
|
| `AccessControl.WhenUnlockedThisDeviceOnly` | The secret should not migrate through backup/restore. |
|
|
62
69
|
| `AccessControl.AfterFirstUnlockThisDeviceOnly` | Background refresh is needed, but migration is not allowed. |
|
|
63
70
|
|
|
71
|
+
On iOS the level applies on every write, including updates of an item that already exists, so changing `accessControl` or `storage.setAccessControl()` moves existing items to the new level on their next write. On iOS, `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, instead of returning `false`. A biometric item that the Keychain reports as needing authentication counts as present, so `has()` on a biometric item returns `true` without a prompt; while the device is locked the same status also returns `true`. Listing, counting, and prefix queries on Secure keys throw a Keychain status error for unexpected statuses instead of returning an empty result.
|
|
72
|
+
|
|
64
73
|
## Secure Auth Item Map
|
|
65
74
|
|
|
66
75
|
`createSecureAuthStorage()` creates a namespaced map of secure string items.
|
|
@@ -172,13 +181,16 @@ Recovery depends on the exact stable code:
|
|
|
172
181
|
|
|
173
182
|
`isKeychainLockedError()` remains available for compatibility but is
|
|
174
183
|
deprecated. It groups all three codes and must not be used to select retry
|
|
175
|
-
behavior. The package does not
|
|
176
|
-
|
|
184
|
+
behavior. The package does not sleep or retry internally; the application owns
|
|
185
|
+
lifecycle scheduling and cancellation. Biometric reads on iOS block the
|
|
186
|
+
synchronous JSI call while the system prompt is visible.
|
|
177
187
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
188
|
+
`fallbackToCacheOnReadError` returns the last value read in this process only
|
|
189
|
+
when a read fails with `keychain_locked`. Every other error still throws. Only
|
|
190
|
+
iOS reports `keychain_locked`; Android and web never do, so the fallback has no
|
|
191
|
+
effect there. Do not
|
|
192
|
+
enable it for access or refresh tokens unless the application explicitly
|
|
193
|
+
accepts stale credentials while the device is locked.
|
|
182
194
|
|
|
183
195
|
## Android Secure Write Mode
|
|
184
196
|
|
|
@@ -204,13 +216,13 @@ entries, and surfaces native clear failures.
|
|
|
204
216
|
## iOS Legacy Disk Migration
|
|
205
217
|
|
|
206
218
|
Older releases tracked observed Disk keys in `standardUserDefaults`. On iOS,
|
|
207
|
-
|
|
219
|
+
the first Disk operation copies a valid registry into the Nitro suite domain and
|
|
208
220
|
removes each legacy source only after a target readback and synchronization
|
|
209
221
|
check. `NSUserDefaults` is not transactional, so the migration stops on any
|
|
210
222
|
failed synchronization or readback without deleting the source or registry.
|
|
211
223
|
Malformed registries, same-domain or fallback stores, conflicting target
|
|
212
224
|
values, and failed persistence therefore remain available for recovery. The
|
|
213
|
-
migration is retryable on a later
|
|
225
|
+
migration is retryable on a later launch; do not delete the registry
|
|
214
226
|
manually while an upgrade is in progress.
|
|
215
227
|
|
|
216
228
|
After that cutover, suite string keys are imported into the SQLite WAL Disk
|
|
@@ -237,10 +249,12 @@ See [web-backends.md](web-backends.md) for backend contracts and IndexedDB setup
|
|
|
237
249
|
Before releasing secure-storage changes, run:
|
|
238
250
|
|
|
239
251
|
```sh
|
|
240
|
-
bun run test
|
|
241
|
-
bun run test:cpp
|
|
242
|
-
|
|
252
|
+
bun run test
|
|
253
|
+
bun run test:cpp
|
|
254
|
+
bun run audit:package
|
|
243
255
|
```
|
|
244
256
|
|
|
257
|
+
`bun run release:preflight` runs these checks together with the full release gate.
|
|
258
|
+
|
|
245
259
|
Also run the [physical-device Keychain lifecycle protocol](keychain-lifecycle-testing.md)
|
|
246
260
|
when changing biometric, Keychain, or error-classification behavior.
|
package/docs/web-backends.md
CHANGED
|
@@ -4,6 +4,8 @@ Nitro Storage runs on web through synchronous backend contracts. Disk and Secure
|
|
|
4
4
|
|
|
5
5
|
The default web backend is localStorage-style. Configure custom backends when you need IndexedDB persistence, tests with isolated storage, cross-tab sync, or a platform-specific secret wrapper.
|
|
6
6
|
|
|
7
|
+
Register a separate backend instance for each scope. If one instance is registered for both scopes, Disk enumeration skips the `__secure_` and `__bio_` keys that Secure scope writes, and each scope's `clear()` removes only its own keys, but separate stores keep the scopes fully isolated.
|
|
8
|
+
|
|
7
9
|
## Backend Contract
|
|
8
10
|
|
|
9
11
|
```ts
|
|
@@ -102,7 +104,7 @@ The IndexedDB backend exposes `close()` and rejects later synchronous operations
|
|
|
102
104
|
|
|
103
105
|
### Persistence Lifecycle
|
|
104
106
|
|
|
105
|
-
IndexedDB transactions cannot block a page unload, so in-flight writes may be aborted when the page closes. The backend
|
|
107
|
+
IndexedDB transactions cannot block a page unload, so in-flight writes may be aborted when the page closes. The backend does not install `pagehide` or `visibilitychange` handlers, because there is no synchronous way to finish a pending transaction there. `flush()` awaits every pending transaction. When persistence fails, `flush()` throws an error that names the affected keys, and `onError` receives each individual failure.
|
|
106
108
|
|
|
107
109
|
```ts
|
|
108
110
|
const backend = await createIndexedDBBackend("app-secure", "keyvalue", {
|
|
@@ -119,7 +121,9 @@ Treat IndexedDB persistence as best-effort under abrupt termination: keep a dura
|
|
|
119
121
|
|
|
120
122
|
## Cross-tab Updates
|
|
121
123
|
|
|
122
|
-
The IndexedDB backend uses `BroadcastChannel` when available. Other tabs receive cache invalidation events and update their in-memory copy.
|
|
124
|
+
The IndexedDB backend uses `BroadcastChannel` when available. Other tabs receive cache invalidation events and update their in-memory copy. Messages that arrive while a new backend is still loading its snapshot are applied after the snapshot, so they are not overwritten.
|
|
125
|
+
|
|
126
|
+
The window `storage` event updates a scope only while that scope uses the default `localStorage` backend, and only for events from `localStorage`. Custom backends must sync through `subscribe(listener)`.
|
|
123
127
|
|
|
124
128
|
If you provide your own backend, implement `subscribe(listener)` to keep Nitro Storage caches aligned with external writes.
|
|
125
129
|
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#pragma once
|
|
2
2
|
|
|
3
3
|
#include "../core/NativeStorageAdapter.hpp"
|
|
4
|
+
#include <atomic>
|
|
4
5
|
#include <mutex>
|
|
5
6
|
#include <unordered_set>
|
|
6
7
|
|
|
@@ -55,7 +56,10 @@ private:
|
|
|
55
56
|
std::unordered_set<std::string> secureKeysCache_;
|
|
56
57
|
std::unordered_set<std::string> biometricKeysCache_;
|
|
57
58
|
bool secureKeyCacheHydrated_{false};
|
|
59
|
+
std::mutex diskMigrationMutex_;
|
|
60
|
+
std::atomic<bool> diskMigrated_{false};
|
|
58
61
|
|
|
62
|
+
void ensureDiskMigrated();
|
|
59
63
|
void ensureSecureKeyCacheHydrated();
|
|
60
64
|
void markSecureKeySet(const std::string& key);
|
|
61
65
|
void markSecureKeyRemoved(const std::string& key);
|