react-native-nitro-storage 0.3.1 → 0.3.2
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/README.md +199 -10
- package/android/CMakeLists.txt +2 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +4 -0
- package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +1 -0
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +36 -13
- package/cpp/bindings/HybridStorage.cpp +55 -9
- package/cpp/bindings/HybridStorage.hpp +19 -2
- package/cpp/core/NativeStorageAdapter.hpp +1 -0
- package/ios/IOSStorageAdapterCpp.hpp +1 -0
- package/ios/IOSStorageAdapterCpp.mm +7 -1
- package/lib/commonjs/index.js +139 -63
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +236 -89
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/storage-hooks.js +36 -0
- package/lib/commonjs/storage-hooks.js.map +1 -0
- package/lib/module/index.js +121 -60
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +219 -87
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/storage-hooks.js +30 -0
- package/lib/module/storage-hooks.js.map +1 -0
- package/lib/typescript/Storage.nitro.d.ts +2 -0
- package/lib/typescript/Storage.nitro.d.ts.map +1 -1
- package/lib/typescript/index.d.ts +3 -3
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +5 -3
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/storage-hooks.d.ts +10 -0
- package/lib/typescript/storage-hooks.d.ts.map +1 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
- package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
- package/package.json +5 -3
- package/src/Storage.nitro.ts +2 -0
- package/src/index.ts +143 -83
- package/src/index.web.ts +255 -112
- package/src/storage-hooks.ts +48 -0
package/README.md
CHANGED
|
@@ -19,6 +19,24 @@ Synchronous Memory, Disk, and Secure storage in one unified API — powered by [
|
|
|
19
19
|
- **MMKV migration** — drop-in `migrateFromMMKV` for painless migration from MMKV
|
|
20
20
|
- **Cross-platform** — iOS, Android, and web (`localStorage` fallback)
|
|
21
21
|
|
|
22
|
+
## Feature Coverage
|
|
23
|
+
|
|
24
|
+
Every feature in this package is documented with at least one runnable example in this README:
|
|
25
|
+
|
|
26
|
+
- Core item API (`createStorageItem`, `get/set/delete/has/subscribe`) — see Quick Start and Low-level subscription use case
|
|
27
|
+
- Hooks (`useStorage`, `useStorageSelector`, `useSetStorage`) — see Quick Start and Persisted User Preferences
|
|
28
|
+
- Scopes (`Memory`, `Disk`, `Secure`) — see Storage Scopes and multiple use cases
|
|
29
|
+
- Namespaces — see Multi-Tenant / Namespaced Storage
|
|
30
|
+
- TTL expiration + callbacks — see OTP / Temporary Codes
|
|
31
|
+
- Validation + recovery — see Feature Flags with Validation
|
|
32
|
+
- Biometric + access control — see Biometric-protected Secrets
|
|
33
|
+
- Global storage utilities (`clear*`, `has`, `getAll*`, `size`, secure write settings) — see Global utility examples and Storage Snapshots and Cleanup
|
|
34
|
+
- Batch APIs (`getBatch`, `setBatch`, `removeBatch`) — see Batch Operations and Bulk Bootstrap with Batch APIs
|
|
35
|
+
- Transactions — see Transactions and Atomic Balance Transfer
|
|
36
|
+
- Migrations (`registerMigration`, `migrateToLatest`) — see Migrations
|
|
37
|
+
- MMKV migration (`migrateFromMMKV`) — see MMKV Migration and Migrating From MMKV
|
|
38
|
+
- Auth storage factory (`createSecureAuthStorage`) — see Auth Token Management
|
|
39
|
+
|
|
22
40
|
## Requirements
|
|
23
41
|
|
|
24
42
|
| Dependency | Version |
|
|
@@ -183,6 +201,17 @@ function createStorageItem<T = undefined>(
|
|
|
183
201
|
| `scope` | `StorageScope` | The item's scope |
|
|
184
202
|
| `key` | `string` | The resolved key (includes namespace prefix) |
|
|
185
203
|
|
|
204
|
+
**Non-React subscription example:**
|
|
205
|
+
|
|
206
|
+
```ts
|
|
207
|
+
const unsubscribe = sessionItem.subscribe(() => {
|
|
208
|
+
console.log("session changed:", sessionItem.get());
|
|
209
|
+
});
|
|
210
|
+
|
|
211
|
+
sessionItem.set("next-session");
|
|
212
|
+
unsubscribe();
|
|
213
|
+
```
|
|
214
|
+
|
|
186
215
|
---
|
|
187
216
|
|
|
188
217
|
### React Hooks
|
|
@@ -231,10 +260,49 @@ import { storage, StorageScope } from "react-native-nitro-storage";
|
|
|
231
260
|
| `storage.getAll(scope)` | Get all key-value pairs as `Record<string, string>` |
|
|
232
261
|
| `storage.size(scope)` | Number of stored keys |
|
|
233
262
|
| `storage.setAccessControl(level)` | Set default secure access control for subsequent secure writes (native only) |
|
|
263
|
+
| `storage.setSecureWritesAsync(enabled)` | Toggle async secure writes on Android (`false` by default) |
|
|
264
|
+
| `storage.flushSecureWrites()` | Force flush of queued secure writes when coalescing is enabled |
|
|
234
265
|
| `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
|
|
235
266
|
|
|
236
267
|
> `storage.getAll(StorageScope.Secure)` returns regular secure entries. Biometric-protected values are not included in this snapshot API.
|
|
237
268
|
|
|
269
|
+
#### Global utility examples
|
|
270
|
+
|
|
271
|
+
```ts
|
|
272
|
+
import { AccessControl, storage, StorageScope } from "react-native-nitro-storage";
|
|
273
|
+
|
|
274
|
+
storage.has("session", StorageScope.Disk);
|
|
275
|
+
storage.getAllKeys(StorageScope.Disk);
|
|
276
|
+
storage.getAll(StorageScope.Disk);
|
|
277
|
+
storage.size(StorageScope.Disk);
|
|
278
|
+
|
|
279
|
+
storage.clearNamespace("user-42", StorageScope.Disk);
|
|
280
|
+
storage.clearBiometric();
|
|
281
|
+
|
|
282
|
+
storage.setAccessControl(AccessControl.WhenUnlockedThisDeviceOnly);
|
|
283
|
+
storage.setKeychainAccessGroup("group.com.example.shared");
|
|
284
|
+
|
|
285
|
+
storage.clear(StorageScope.Memory);
|
|
286
|
+
storage.clearAll();
|
|
287
|
+
```
|
|
288
|
+
|
|
289
|
+
#### Android secure write mode
|
|
290
|
+
|
|
291
|
+
`storage.setSecureWritesAsync(true)` switches secure writes from synchronous `commit()` to asynchronous `apply()` on Android.
|
|
292
|
+
Use this for non-critical secure writes when lower latency matters more than immediate durability.
|
|
293
|
+
|
|
294
|
+
Call `storage.flushSecureWrites()` when you need deterministic persistence boundaries (for example before namespace clears, process handoff, or strict test assertions).
|
|
295
|
+
|
|
296
|
+
```ts
|
|
297
|
+
import { storage } from "react-native-nitro-storage";
|
|
298
|
+
|
|
299
|
+
storage.setSecureWritesAsync(true);
|
|
300
|
+
|
|
301
|
+
// ...multiple secure writes happen (including coalesced item writes)
|
|
302
|
+
|
|
303
|
+
storage.flushSecureWrites(); // deterministic durability boundary
|
|
304
|
+
```
|
|
305
|
+
|
|
238
306
|
---
|
|
239
307
|
|
|
240
308
|
### `createSecureAuthStorage<K>(config, options?)`
|
|
@@ -398,11 +466,11 @@ enum BiometricLevel {
|
|
|
398
466
|
### Persisted User Preferences
|
|
399
467
|
|
|
400
468
|
```ts
|
|
401
|
-
|
|
469
|
+
type UserPreferences = {
|
|
402
470
|
theme: "light" | "dark" | "system";
|
|
403
471
|
language: string;
|
|
404
472
|
notifications: boolean;
|
|
405
|
-
}
|
|
473
|
+
};
|
|
406
474
|
|
|
407
475
|
const prefsItem = createStorageItem<UserPreferences>({
|
|
408
476
|
key: "prefs",
|
|
@@ -446,21 +514,29 @@ storage.clearNamespace("myapp-auth", StorageScope.Secure);
|
|
|
446
514
|
### Feature Flags with Validation
|
|
447
515
|
|
|
448
516
|
```ts
|
|
449
|
-
|
|
517
|
+
type FeatureFlags = {
|
|
450
518
|
darkMode: boolean;
|
|
451
519
|
betaFeature: boolean;
|
|
452
520
|
maxUploadMb: number;
|
|
453
|
-
}
|
|
521
|
+
};
|
|
522
|
+
|
|
523
|
+
const isRecord = (value: unknown): value is Record<string, unknown> =>
|
|
524
|
+
typeof value === "object" && value !== null;
|
|
525
|
+
|
|
526
|
+
const isFeatureFlags = (value: unknown): value is FeatureFlags => {
|
|
527
|
+
if (!isRecord(value)) return false;
|
|
528
|
+
return (
|
|
529
|
+
typeof value.darkMode === "boolean" &&
|
|
530
|
+
typeof value.betaFeature === "boolean" &&
|
|
531
|
+
typeof value.maxUploadMb === "number"
|
|
532
|
+
);
|
|
533
|
+
};
|
|
454
534
|
|
|
455
535
|
const flagsItem = createStorageItem<FeatureFlags>({
|
|
456
536
|
key: "feature-flags",
|
|
457
537
|
scope: StorageScope.Disk,
|
|
458
538
|
defaultValue: { darkMode: false, betaFeature: false, maxUploadMb: 10 },
|
|
459
|
-
validate:
|
|
460
|
-
typeof v === "object" &&
|
|
461
|
-
v !== null &&
|
|
462
|
-
typeof (v as any).darkMode === "boolean" &&
|
|
463
|
-
typeof (v as any).maxUploadMb === "number",
|
|
539
|
+
validate: isFeatureFlags,
|
|
464
540
|
onValidationError: () => ({
|
|
465
541
|
darkMode: false,
|
|
466
542
|
betaFeature: false,
|
|
@@ -471,6 +547,24 @@ const flagsItem = createStorageItem<FeatureFlags>({
|
|
|
471
547
|
});
|
|
472
548
|
```
|
|
473
549
|
|
|
550
|
+
### Biometric-protected Secrets
|
|
551
|
+
|
|
552
|
+
```ts
|
|
553
|
+
import { AccessControl, createStorageItem, StorageScope } from "react-native-nitro-storage";
|
|
554
|
+
|
|
555
|
+
const paymentPin = createStorageItem<string>({
|
|
556
|
+
key: "payment-pin",
|
|
557
|
+
scope: StorageScope.Secure,
|
|
558
|
+
defaultValue: "",
|
|
559
|
+
biometric: true,
|
|
560
|
+
accessControl: AccessControl.WhenPasscodeSetThisDeviceOnly,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
paymentPin.set("4829");
|
|
564
|
+
const pin = paymentPin.get();
|
|
565
|
+
paymentPin.delete();
|
|
566
|
+
```
|
|
567
|
+
|
|
474
568
|
### Multi-Tenant / Namespaced Storage
|
|
475
569
|
|
|
476
570
|
```ts
|
|
@@ -515,6 +609,40 @@ otpItem.set("482917");
|
|
|
515
609
|
const code = otpItem.get();
|
|
516
610
|
```
|
|
517
611
|
|
|
612
|
+
### Bulk Bootstrap with Batch APIs
|
|
613
|
+
|
|
614
|
+
```ts
|
|
615
|
+
import {
|
|
616
|
+
createStorageItem,
|
|
617
|
+
getBatch,
|
|
618
|
+
removeBatch,
|
|
619
|
+
setBatch,
|
|
620
|
+
StorageScope,
|
|
621
|
+
} from "react-native-nitro-storage";
|
|
622
|
+
|
|
623
|
+
const firstName = createStorageItem({
|
|
624
|
+
key: "first-name",
|
|
625
|
+
scope: StorageScope.Disk,
|
|
626
|
+
defaultValue: "",
|
|
627
|
+
});
|
|
628
|
+
const lastName = createStorageItem({
|
|
629
|
+
key: "last-name",
|
|
630
|
+
scope: StorageScope.Disk,
|
|
631
|
+
defaultValue: "",
|
|
632
|
+
});
|
|
633
|
+
|
|
634
|
+
setBatch(
|
|
635
|
+
[
|
|
636
|
+
{ item: firstName, value: "Ada" },
|
|
637
|
+
{ item: lastName, value: "Lovelace" },
|
|
638
|
+
],
|
|
639
|
+
StorageScope.Disk,
|
|
640
|
+
);
|
|
641
|
+
|
|
642
|
+
const [first, last] = getBatch([firstName, lastName], StorageScope.Disk);
|
|
643
|
+
removeBatch([firstName, lastName], StorageScope.Disk);
|
|
644
|
+
```
|
|
645
|
+
|
|
518
646
|
### Atomic Balance Transfer
|
|
519
647
|
|
|
520
648
|
```ts
|
|
@@ -555,6 +683,60 @@ const compactItem = createStorageItem<{ id: number; active: boolean }>({
|
|
|
555
683
|
});
|
|
556
684
|
```
|
|
557
685
|
|
|
686
|
+
### Coalesced Secure Writes with Deterministic Flush
|
|
687
|
+
|
|
688
|
+
```ts
|
|
689
|
+
import { createStorageItem, storage, StorageScope } from "react-native-nitro-storage";
|
|
690
|
+
|
|
691
|
+
const sessionToken = createStorageItem<string>({
|
|
692
|
+
key: "session-token",
|
|
693
|
+
scope: StorageScope.Secure,
|
|
694
|
+
defaultValue: "",
|
|
695
|
+
coalesceSecureWrites: true,
|
|
696
|
+
});
|
|
697
|
+
|
|
698
|
+
sessionToken.set("token-v1");
|
|
699
|
+
sessionToken.set("token-v2");
|
|
700
|
+
|
|
701
|
+
// force pending secure writes to native persistence
|
|
702
|
+
storage.flushSecureWrites();
|
|
703
|
+
```
|
|
704
|
+
|
|
705
|
+
### Storage Snapshots and Cleanup
|
|
706
|
+
|
|
707
|
+
```ts
|
|
708
|
+
import { storage, StorageScope } from "react-native-nitro-storage";
|
|
709
|
+
|
|
710
|
+
const diskKeys = storage.getAllKeys(StorageScope.Disk);
|
|
711
|
+
const diskValues = storage.getAll(StorageScope.Disk);
|
|
712
|
+
const secureCount = storage.size(StorageScope.Secure);
|
|
713
|
+
|
|
714
|
+
if (storage.has("legacy-flag", StorageScope.Disk)) {
|
|
715
|
+
storage.clearNamespace("legacy", StorageScope.Disk);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
storage.clearBiometric();
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### Low-level Subscription (outside React)
|
|
722
|
+
|
|
723
|
+
```ts
|
|
724
|
+
import { createStorageItem, StorageScope } from "react-native-nitro-storage";
|
|
725
|
+
|
|
726
|
+
const notificationsItem = createStorageItem<boolean>({
|
|
727
|
+
key: "notifications-enabled",
|
|
728
|
+
scope: StorageScope.Disk,
|
|
729
|
+
defaultValue: true,
|
|
730
|
+
});
|
|
731
|
+
|
|
732
|
+
const unsubscribe = notificationsItem.subscribe(() => {
|
|
733
|
+
console.log("notifications changed:", notificationsItem.get());
|
|
734
|
+
});
|
|
735
|
+
|
|
736
|
+
notificationsItem.set(false);
|
|
737
|
+
unsubscribe();
|
|
738
|
+
```
|
|
739
|
+
|
|
558
740
|
### Migrating From MMKV
|
|
559
741
|
|
|
560
742
|
```ts
|
|
@@ -600,7 +782,11 @@ From repository root:
|
|
|
600
782
|
|
|
601
783
|
```bash
|
|
602
784
|
bun run test -- --filter=react-native-nitro-storage
|
|
785
|
+
bun run lint -- --filter=react-native-nitro-storage
|
|
786
|
+
bun run format:check -- --filter=react-native-nitro-storage
|
|
603
787
|
bun run typecheck -- --filter=react-native-nitro-storage
|
|
788
|
+
bun run test:types -- --filter=react-native-nitro-storage
|
|
789
|
+
bun run test:cpp -- --filter=react-native-nitro-storage
|
|
604
790
|
bun run build -- --filter=react-native-nitro-storage
|
|
605
791
|
```
|
|
606
792
|
|
|
@@ -612,7 +798,10 @@ bun run test:coverage # run tests with coverage
|
|
|
612
798
|
bun run lint # eslint (expo-magic rules)
|
|
613
799
|
bun run format:check # prettier check
|
|
614
800
|
bun run typecheck # tsc --noEmit
|
|
615
|
-
bun run
|
|
801
|
+
bun run test:types # public type-level API tests
|
|
802
|
+
bun run test:cpp # C++ binding/core tests
|
|
803
|
+
bun run check:pack # npm pack content guard
|
|
804
|
+
bun run build # bob build
|
|
616
805
|
bun run benchmark # performance benchmarks
|
|
617
806
|
```
|
|
618
807
|
|
package/android/CMakeLists.txt
CHANGED
|
@@ -11,6 +11,8 @@ file(GLOB SOURCES
|
|
|
11
11
|
"../cpp/core/*.cpp"
|
|
12
12
|
"./src/main/cpp/*.cpp"
|
|
13
13
|
)
|
|
14
|
+
# Unit/integration C++ tests define `main()` and must never be linked into the Android shared library.
|
|
15
|
+
list(FILTER SOURCES EXCLUDE REGEX ".*/[^/]*Test\\.cpp$")
|
|
14
16
|
|
|
15
17
|
# 2. Create the library target
|
|
16
18
|
add_library(
|
|
@@ -213,6 +213,10 @@ void AndroidStorageAdapterCpp::clearSecure() {
|
|
|
213
213
|
// --- Config (no-ops on Android; access control / groups are iOS-specific) ---
|
|
214
214
|
|
|
215
215
|
void AndroidStorageAdapterCpp::setSecureAccessControl(int /*level*/) {}
|
|
216
|
+
void AndroidStorageAdapterCpp::setSecureWritesAsync(bool enabled) {
|
|
217
|
+
static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(jboolean)>("setSecureWritesAsync");
|
|
218
|
+
method(AndroidStorageAdapterJava::javaClassStatic(), enabled);
|
|
219
|
+
}
|
|
216
220
|
void AndroidStorageAdapterCpp::setKeychainAccessGroup(const std::string& /*group*/) {}
|
|
217
221
|
|
|
218
222
|
// --- Biometric ---
|
|
@@ -49,6 +49,7 @@ public:
|
|
|
49
49
|
void clearSecure() override;
|
|
50
50
|
|
|
51
51
|
void setSecureAccessControl(int level) override;
|
|
52
|
+
void setSecureWritesAsync(bool enabled) override;
|
|
52
53
|
void setKeychainAccessGroup(const std::string& group) override;
|
|
53
54
|
|
|
54
55
|
void setSecureBiometric(const std::string& key, const std::string& value) override;
|
|
@@ -33,6 +33,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
33
33
|
encryptedPreferences
|
|
34
34
|
}
|
|
35
35
|
}
|
|
36
|
+
|
|
37
|
+
@Volatile
|
|
38
|
+
private var secureWritesAsync = false
|
|
36
39
|
|
|
37
40
|
private fun initializeEncryptedPreferences(name: String, key: MasterKey): SharedPreferences {
|
|
38
41
|
return try {
|
|
@@ -90,6 +93,14 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
}
|
|
96
|
+
|
|
97
|
+
private fun applySecureEditor(editor: SharedPreferences.Editor) {
|
|
98
|
+
if (secureWritesAsync) {
|
|
99
|
+
editor.apply()
|
|
100
|
+
} else {
|
|
101
|
+
editor.commit()
|
|
102
|
+
}
|
|
103
|
+
}
|
|
93
104
|
|
|
94
105
|
companion object {
|
|
95
106
|
@Volatile
|
|
@@ -118,6 +129,11 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
118
129
|
return getInstanceOrThrow().context
|
|
119
130
|
}
|
|
120
131
|
|
|
132
|
+
@JvmStatic
|
|
133
|
+
fun setSecureWritesAsync(enabled: Boolean) {
|
|
134
|
+
getInstanceOrThrow().secureWritesAsync = enabled
|
|
135
|
+
}
|
|
136
|
+
|
|
121
137
|
// --- Disk ---
|
|
122
138
|
|
|
123
139
|
@JvmStatic
|
|
@@ -182,21 +198,24 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
182
198
|
getInstanceOrThrow().sharedPreferences.edit().clear().apply()
|
|
183
199
|
}
|
|
184
200
|
|
|
185
|
-
// --- Secure (
|
|
201
|
+
// --- Secure (sync commit by default, async apply when enabled) ---
|
|
186
202
|
|
|
187
203
|
@JvmStatic
|
|
188
204
|
fun setSecure(key: String, value: String) {
|
|
189
|
-
getInstanceOrThrow()
|
|
205
|
+
val inst = getInstanceOrThrow()
|
|
206
|
+
val editor = inst.encryptedPreferences.edit().putString(key, value)
|
|
207
|
+
inst.applySecureEditor(editor)
|
|
190
208
|
}
|
|
191
209
|
|
|
192
210
|
@JvmStatic
|
|
193
211
|
fun setSecureBatch(keys: Array<String>, values: Array<String>) {
|
|
194
|
-
val
|
|
212
|
+
val inst = getInstanceOrThrow()
|
|
213
|
+
val editor = inst.encryptedPreferences.edit()
|
|
195
214
|
val count = minOf(keys.size, values.size)
|
|
196
215
|
for (index in 0 until count) {
|
|
197
216
|
editor.putString(keys[index], values[index])
|
|
198
217
|
}
|
|
199
|
-
|
|
218
|
+
inst.applySecureEditor(editor)
|
|
200
219
|
}
|
|
201
220
|
|
|
202
221
|
@JvmStatic
|
|
@@ -215,8 +234,8 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
215
234
|
@JvmStatic
|
|
216
235
|
fun deleteSecure(key: String) {
|
|
217
236
|
val inst = getInstanceOrThrow()
|
|
218
|
-
inst.encryptedPreferences.edit().remove(key)
|
|
219
|
-
inst.biometricPreferences.edit().remove(key)
|
|
237
|
+
inst.applySecureEditor(inst.encryptedPreferences.edit().remove(key))
|
|
238
|
+
inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
|
|
220
239
|
}
|
|
221
240
|
|
|
222
241
|
@JvmStatic
|
|
@@ -228,8 +247,8 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
228
247
|
secureEditor.remove(key)
|
|
229
248
|
biometricEditor.remove(key)
|
|
230
249
|
}
|
|
231
|
-
|
|
232
|
-
|
|
250
|
+
inst.applySecureEditor(secureEditor)
|
|
251
|
+
inst.applySecureEditor(biometricEditor)
|
|
233
252
|
}
|
|
234
253
|
|
|
235
254
|
@JvmStatic
|
|
@@ -255,15 +274,17 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
255
274
|
@JvmStatic
|
|
256
275
|
fun clearSecure() {
|
|
257
276
|
val inst = getInstanceOrThrow()
|
|
258
|
-
inst.encryptedPreferences.edit().clear()
|
|
259
|
-
inst.biometricPreferences.edit().clear()
|
|
277
|
+
inst.applySecureEditor(inst.encryptedPreferences.edit().clear())
|
|
278
|
+
inst.applySecureEditor(inst.biometricPreferences.edit().clear())
|
|
260
279
|
}
|
|
261
280
|
|
|
262
281
|
// --- Biometric (separate encrypted store, requires recent biometric auth on Android) ---
|
|
263
282
|
|
|
264
283
|
@JvmStatic
|
|
265
284
|
fun setSecureBiometric(key: String, value: String) {
|
|
266
|
-
getInstanceOrThrow()
|
|
285
|
+
val inst = getInstanceOrThrow()
|
|
286
|
+
val editor = inst.biometricPreferences.edit().putString(key, value)
|
|
287
|
+
inst.applySecureEditor(editor)
|
|
267
288
|
}
|
|
268
289
|
|
|
269
290
|
@JvmStatic
|
|
@@ -273,7 +294,8 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
273
294
|
|
|
274
295
|
@JvmStatic
|
|
275
296
|
fun deleteSecureBiometric(key: String) {
|
|
276
|
-
getInstanceOrThrow()
|
|
297
|
+
val inst = getInstanceOrThrow()
|
|
298
|
+
inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
|
|
277
299
|
}
|
|
278
300
|
|
|
279
301
|
@JvmStatic
|
|
@@ -283,7 +305,8 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
|
|
|
283
305
|
|
|
284
306
|
@JvmStatic
|
|
285
307
|
fun clearSecureBiometric() {
|
|
286
|
-
getInstanceOrThrow()
|
|
308
|
+
val inst = getInstanceOrThrow()
|
|
309
|
+
inst.applySecureEditor(inst.biometricPreferences.edit().clear())
|
|
287
310
|
}
|
|
288
311
|
}
|
|
289
312
|
}
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
#include "HybridStorage.hpp"
|
|
2
2
|
#include <stdexcept>
|
|
3
3
|
|
|
4
|
+
#ifndef NITRO_STORAGE_DISABLE_PLATFORM_ADAPTER
|
|
4
5
|
#if __APPLE__
|
|
5
6
|
#include "../../ios/IOSStorageAdapterCpp.hpp"
|
|
6
7
|
#elif __ANDROID__
|
|
7
8
|
#include "../../android/src/main/cpp/AndroidStorageAdapterCpp.hpp"
|
|
8
9
|
#include <fbjni/fbjni.h>
|
|
9
10
|
#endif
|
|
11
|
+
#endif
|
|
10
12
|
|
|
11
13
|
namespace margelo::nitro::NitroStorage {
|
|
12
14
|
|
|
@@ -16,12 +18,14 @@ constexpr auto kBatchMissingSentinel = "__nitro_storage_batch_missing__::v1";
|
|
|
16
18
|
|
|
17
19
|
HybridStorage::HybridStorage()
|
|
18
20
|
: HybridObject(TAG), HybridStorageSpec() {
|
|
21
|
+
#ifndef NITRO_STORAGE_DISABLE_PLATFORM_ADAPTER
|
|
19
22
|
#if __APPLE__
|
|
20
23
|
nativeAdapter_ = std::make_shared<::NitroStorage::IOSStorageAdapterCpp>();
|
|
21
24
|
#elif __ANDROID__
|
|
22
25
|
auto context = ::NitroStorage::AndroidStorageAdapterJava::getContext();
|
|
23
26
|
nativeAdapter_ = std::make_shared<::NitroStorage::AndroidStorageAdapterCpp>(context);
|
|
24
27
|
#endif
|
|
28
|
+
#endif
|
|
25
29
|
}
|
|
26
30
|
|
|
27
31
|
HybridStorage::HybridStorage(std::shared_ptr<::NitroStorage::NativeStorageAdapter> adapter)
|
|
@@ -298,8 +302,10 @@ void HybridStorage::setBatch(const std::vector<std::string>& keys, const std::ve
|
|
|
298
302
|
break;
|
|
299
303
|
}
|
|
300
304
|
|
|
305
|
+
const auto scopeValue = static_cast<int>(s);
|
|
306
|
+
const auto listeners = copyListenersForScope(scopeValue);
|
|
301
307
|
for (size_t i = 0; i < keys.size(); ++i) {
|
|
302
|
-
notifyListeners(
|
|
308
|
+
notifyListeners(listeners, keys[i], values[i]);
|
|
303
309
|
}
|
|
304
310
|
}
|
|
305
311
|
|
|
@@ -392,11 +398,34 @@ void HybridStorage::removeBatch(const std::vector<std::string>& keys, double sco
|
|
|
392
398
|
break;
|
|
393
399
|
}
|
|
394
400
|
|
|
401
|
+
const auto scopeValue = static_cast<int>(s);
|
|
402
|
+
const auto listeners = copyListenersForScope(scopeValue);
|
|
395
403
|
for (const auto& key : keys) {
|
|
396
|
-
notifyListeners(
|
|
404
|
+
notifyListeners(listeners, key, std::nullopt);
|
|
397
405
|
}
|
|
398
406
|
}
|
|
399
407
|
|
|
408
|
+
void HybridStorage::removeByPrefix(const std::string& prefix, double scope) {
|
|
409
|
+
if (prefix.empty()) {
|
|
410
|
+
return;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
const auto keys = getAllKeys(scope);
|
|
414
|
+
std::vector<std::string> prefixedKeys;
|
|
415
|
+
prefixedKeys.reserve(keys.size());
|
|
416
|
+
for (const auto& key : keys) {
|
|
417
|
+
if (key.rfind(prefix, 0) == 0) {
|
|
418
|
+
prefixedKeys.push_back(key);
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
if (prefixedKeys.empty()) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
removeBatch(prefixedKeys, scope);
|
|
427
|
+
}
|
|
428
|
+
|
|
400
429
|
// --- Configuration ---
|
|
401
430
|
|
|
402
431
|
void HybridStorage::setSecureAccessControl(double level) {
|
|
@@ -404,6 +433,11 @@ void HybridStorage::setSecureAccessControl(double level) {
|
|
|
404
433
|
nativeAdapter_->setSecureAccessControl(static_cast<int>(level));
|
|
405
434
|
}
|
|
406
435
|
|
|
436
|
+
void HybridStorage::setSecureWritesAsync(bool enabled) {
|
|
437
|
+
ensureAdapter();
|
|
438
|
+
nativeAdapter_->setSecureWritesAsync(enabled);
|
|
439
|
+
}
|
|
440
|
+
|
|
407
441
|
void HybridStorage::setKeychainAccessGroup(const std::string& group) {
|
|
408
442
|
ensureAdapter();
|
|
409
443
|
nativeAdapter_->setKeychainAccessGroup(group);
|
|
@@ -457,11 +491,7 @@ void HybridStorage::clearSecureBiometric() {
|
|
|
457
491
|
|
|
458
492
|
// --- Internal ---
|
|
459
493
|
|
|
460
|
-
|
|
461
|
-
int scope,
|
|
462
|
-
const std::string& key,
|
|
463
|
-
const std::optional<std::string>& value
|
|
464
|
-
) {
|
|
494
|
+
std::vector<HybridStorage::Listener> HybridStorage::copyListenersForScope(int scope) {
|
|
465
495
|
std::vector<Listener> listenersCopy;
|
|
466
496
|
{
|
|
467
497
|
std::lock_guard<std::mutex> lock(listenersMutex_);
|
|
@@ -471,8 +501,15 @@ void HybridStorage::notifyListeners(
|
|
|
471
501
|
listenersCopy = it->second;
|
|
472
502
|
}
|
|
473
503
|
}
|
|
474
|
-
|
|
475
|
-
|
|
504
|
+
return listenersCopy;
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
void HybridStorage::notifyListeners(
|
|
508
|
+
const std::vector<Listener>& listeners,
|
|
509
|
+
const std::string& key,
|
|
510
|
+
const std::optional<std::string>& value
|
|
511
|
+
) {
|
|
512
|
+
for (const auto& listener : listeners) {
|
|
476
513
|
try {
|
|
477
514
|
listener.callback(key, value);
|
|
478
515
|
} catch (...) {
|
|
@@ -481,6 +518,15 @@ void HybridStorage::notifyListeners(
|
|
|
481
518
|
}
|
|
482
519
|
}
|
|
483
520
|
|
|
521
|
+
void HybridStorage::notifyListeners(
|
|
522
|
+
int scope,
|
|
523
|
+
const std::string& key,
|
|
524
|
+
const std::optional<std::string>& value
|
|
525
|
+
) {
|
|
526
|
+
const auto listeners = copyListenersForScope(scope);
|
|
527
|
+
notifyListeners(listeners, key, value);
|
|
528
|
+
}
|
|
529
|
+
|
|
484
530
|
void HybridStorage::ensureAdapter() const {
|
|
485
531
|
if (!nativeAdapter_) {
|
|
486
532
|
throw std::runtime_error("NitroStorage: Native adapter not initialized");
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#include "HybridStorageSpec.hpp"
|
|
4
4
|
#include "../core/NativeStorageAdapter.hpp"
|
|
5
5
|
#include <unordered_map>
|
|
6
|
+
#include <map>
|
|
6
7
|
#include <mutex>
|
|
7
8
|
#include <functional>
|
|
8
9
|
#include <memory>
|
|
@@ -10,6 +11,14 @@
|
|
|
10
11
|
|
|
11
12
|
namespace margelo::nitro::NitroStorage {
|
|
12
13
|
|
|
14
|
+
#ifdef NITRO_STORAGE_USE_ORDERED_MAP_FOR_TESTS
|
|
15
|
+
template <typename Key, typename Value>
|
|
16
|
+
using HybridStorageMap = std::map<Key, Value>;
|
|
17
|
+
#else
|
|
18
|
+
template <typename Key, typename Value>
|
|
19
|
+
using HybridStorageMap = std::unordered_map<Key, Value>;
|
|
20
|
+
#endif
|
|
21
|
+
|
|
13
22
|
class HybridStorage : public HybridStorageSpec {
|
|
14
23
|
public:
|
|
15
24
|
HybridStorage();
|
|
@@ -26,11 +35,13 @@ public:
|
|
|
26
35
|
void setBatch(const std::vector<std::string>& keys, const std::vector<std::string>& values, double scope) override;
|
|
27
36
|
std::vector<std::string> getBatch(const std::vector<std::string>& keys, double scope) override;
|
|
28
37
|
void removeBatch(const std::vector<std::string>& keys, double scope) override;
|
|
38
|
+
void removeByPrefix(const std::string& prefix, double scope) override;
|
|
29
39
|
std::function<void()> addOnChange(
|
|
30
40
|
double scope,
|
|
31
41
|
const std::function<void(const std::string&, const std::optional<std::string>&)>& callback
|
|
32
42
|
) override;
|
|
33
43
|
void setSecureAccessControl(double level) override;
|
|
44
|
+
void setSecureWritesAsync(bool enabled) override;
|
|
34
45
|
void setKeychainAccessGroup(const std::string& group) override;
|
|
35
46
|
void setSecureBiometric(const std::string& key, const std::string& value) override;
|
|
36
47
|
std::optional<std::string> getSecureBiometric(const std::string& key) override;
|
|
@@ -50,15 +61,21 @@ private:
|
|
|
50
61
|
std::function<void(const std::string&, const std::optional<std::string>&)> callback;
|
|
51
62
|
};
|
|
52
63
|
|
|
53
|
-
|
|
64
|
+
HybridStorageMap<std::string, std::string> memoryStore_;
|
|
54
65
|
std::mutex memoryMutex_;
|
|
55
66
|
|
|
56
67
|
std::shared_ptr<::NitroStorage::NativeStorageAdapter> nativeAdapter_;
|
|
57
68
|
|
|
58
|
-
|
|
69
|
+
HybridStorageMap<int, std::vector<Listener>> listeners_;
|
|
59
70
|
std::mutex listenersMutex_;
|
|
60
71
|
size_t nextListenerId_ = 0;
|
|
61
72
|
|
|
73
|
+
std::vector<Listener> copyListenersForScope(int scope);
|
|
74
|
+
void notifyListeners(
|
|
75
|
+
const std::vector<Listener>& listeners,
|
|
76
|
+
const std::string& key,
|
|
77
|
+
const std::optional<std::string>& value
|
|
78
|
+
);
|
|
62
79
|
void notifyListeners(int scope, const std::string& key, const std::optional<std::string>& value);
|
|
63
80
|
void ensureAdapter() const;
|
|
64
81
|
Scope toScope(double scopeValue);
|
|
@@ -34,6 +34,7 @@ public:
|
|
|
34
34
|
virtual void clearSecure() = 0;
|
|
35
35
|
|
|
36
36
|
virtual void setSecureAccessControl(int level) = 0;
|
|
37
|
+
virtual void setSecureWritesAsync(bool enabled) = 0;
|
|
37
38
|
virtual void setKeychainAccessGroup(const std::string& group) = 0;
|
|
38
39
|
|
|
39
40
|
virtual void setSecureBiometric(const std::string& key, const std::string& value) = 0;
|
|
@@ -33,6 +33,7 @@ public:
|
|
|
33
33
|
void clearSecure() override;
|
|
34
34
|
|
|
35
35
|
void setSecureAccessControl(int level) override;
|
|
36
|
+
void setSecureWritesAsync(bool enabled) override;
|
|
36
37
|
void setKeychainAccessGroup(const std::string& group) override;
|
|
37
38
|
|
|
38
39
|
void setSecureBiometric(const std::string& key, const std::string& value) override;
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
#import <Security/Security.h>
|
|
4
4
|
#import <LocalAuthentication/LocalAuthentication.h>
|
|
5
5
|
#include <algorithm>
|
|
6
|
+
#include <unordered_set>
|
|
6
7
|
|
|
7
8
|
namespace NitroStorage {
|
|
8
9
|
|
|
@@ -222,9 +223,10 @@ bool IOSStorageAdapterCpp::hasSecure(const std::string& key) {
|
|
|
222
223
|
std::vector<std::string> IOSStorageAdapterCpp::getAllKeysSecure() {
|
|
223
224
|
NSString* group = keychainAccessGroup_.empty() ? nil : [NSString stringWithUTF8String:keychainAccessGroup_.c_str()];
|
|
224
225
|
std::vector<std::string> keys = keychainAccountsForService(kKeychainService, group);
|
|
226
|
+
std::unordered_set<std::string> seen(keys.begin(), keys.end());
|
|
225
227
|
const std::vector<std::string> biometricKeys = keychainAccountsForService(kBiometricKeychainService, group);
|
|
226
228
|
for (const auto& key : biometricKeys) {
|
|
227
|
-
if (
|
|
229
|
+
if (seen.insert(key).second) {
|
|
228
230
|
keys.push_back(key);
|
|
229
231
|
}
|
|
230
232
|
}
|
|
@@ -288,6 +290,10 @@ void IOSStorageAdapterCpp::setSecureAccessControl(int level) {
|
|
|
288
290
|
accessControlLevel_ = level;
|
|
289
291
|
}
|
|
290
292
|
|
|
293
|
+
void IOSStorageAdapterCpp::setSecureWritesAsync(bool /*enabled*/) {
|
|
294
|
+
// iOS writes are synchronous by design; keep behavior unchanged.
|
|
295
|
+
}
|
|
296
|
+
|
|
291
297
|
void IOSStorageAdapterCpp::setKeychainAccessGroup(const std::string& group) {
|
|
292
298
|
keychainAccessGroup_ = group;
|
|
293
299
|
}
|