react-native-nitro-storage 0.3.2 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. package/README.md +192 -30
  2. package/android/src/main/cpp/AndroidStorageAdapterCpp.cpp +22 -2
  3. package/android/src/main/cpp/AndroidStorageAdapterCpp.hpp +3 -0
  4. package/android/src/main/cpp/cpp-adapter.cpp +3 -1
  5. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +54 -5
  6. package/cpp/bindings/HybridStorage.cpp +167 -22
  7. package/cpp/bindings/HybridStorage.hpp +12 -1
  8. package/cpp/core/NativeStorageAdapter.hpp +3 -0
  9. package/ios/IOSStorageAdapterCpp.hpp +16 -0
  10. package/ios/IOSStorageAdapterCpp.mm +135 -11
  11. package/lib/commonjs/index.js +522 -275
  12. package/lib/commonjs/index.js.map +1 -1
  13. package/lib/commonjs/index.web.js +614 -270
  14. package/lib/commonjs/index.web.js.map +1 -1
  15. package/lib/commonjs/indexeddb-backend.js +130 -0
  16. package/lib/commonjs/indexeddb-backend.js.map +1 -0
  17. package/lib/commonjs/internal.js +25 -0
  18. package/lib/commonjs/internal.js.map +1 -1
  19. package/lib/module/index.js +516 -277
  20. package/lib/module/index.js.map +1 -1
  21. package/lib/module/index.web.js +608 -272
  22. package/lib/module/index.web.js.map +1 -1
  23. package/lib/module/indexeddb-backend.js +126 -0
  24. package/lib/module/indexeddb-backend.js.map +1 -0
  25. package/lib/module/internal.js +24 -0
  26. package/lib/module/internal.js.map +1 -1
  27. package/lib/typescript/Storage.nitro.d.ts +2 -0
  28. package/lib/typescript/Storage.nitro.d.ts.map +1 -1
  29. package/lib/typescript/index.d.ts +40 -1
  30. package/lib/typescript/index.d.ts.map +1 -1
  31. package/lib/typescript/index.web.d.ts +42 -1
  32. package/lib/typescript/index.web.d.ts.map +1 -1
  33. package/lib/typescript/indexeddb-backend.d.ts +29 -0
  34. package/lib/typescript/indexeddb-backend.d.ts.map +1 -0
  35. package/lib/typescript/internal.d.ts +1 -0
  36. package/lib/typescript/internal.d.ts.map +1 -1
  37. package/nitrogen/generated/android/NitroStorageOnLoad.cpp +22 -17
  38. package/nitrogen/generated/android/NitroStorageOnLoad.hpp +13 -4
  39. package/nitrogen/generated/shared/c++/HybridStorageSpec.cpp +2 -0
  40. package/nitrogen/generated/shared/c++/HybridStorageSpec.hpp +2 -0
  41. package/package.json +7 -3
  42. package/src/Storage.nitro.ts +2 -0
  43. package/src/index.ts +671 -296
  44. package/src/index.web.ts +776 -288
  45. package/src/indexeddb-backend.ts +143 -0
  46. package/src/internal.ts +28 -0
package/README.md CHANGED
@@ -14,6 +14,12 @@ Synchronous Memory, Disk, and Secure storage in one unified API — powered by [
14
14
  - **Biometric storage** — hardware-backed biometric protection on iOS & Android
15
15
  - **Auth storage factory** — `createSecureAuthStorage` for multi-token auth flows
16
16
  - **Batch operations** — atomic multi-key get/set/remove via native batch APIs
17
+ - **Prefix queries** — fast key/value scans with `storage.getKeysByPrefix` and `storage.getByPrefix`
18
+ - **Versioned writes** — optimistic concurrency with `item.getWithVersion()` and `item.setIfVersion(...)`
19
+ - **Performance metrics** — observe operation timings and aggregate snapshots
20
+ - **Web secure backend override** — plug custom secure storage backend on web
21
+ - **IndexedDB backend** — drop-in `createIndexedDBBackend` factory for persistent web Secure storage with large payloads
22
+ - **Bulk import** — load a raw `Record<string, string>` into any scope atomically with `storage.import`
17
23
  - **Transactions** — grouped writes with automatic rollback on error
18
24
  - **Migrations** — versioned data migrations with `registerMigration` / `migrateToLatest`
19
25
  - **MMKV migration** — drop-in `migrateFromMMKV` for painless migration from MMKV
@@ -31,6 +37,12 @@ Every feature in this package is documented with at least one runnable example i
31
37
  - Validation + recovery — see Feature Flags with Validation
32
38
  - Biometric + access control — see Biometric-protected Secrets
33
39
  - Global storage utilities (`clear*`, `has`, `getAll*`, `size`, secure write settings) — see Global utility examples and Storage Snapshots and Cleanup
40
+ - Prefix utilities (`getKeysByPrefix`, `getByPrefix`) — see Prefix Queries and Namespace Inspection
41
+ - Versioned item API (`getWithVersion`, `setIfVersion`) — see Optimistic Versioned Writes
42
+ - Metrics API (`setMetricsObserver`, `getMetricsSnapshot`, `resetMetrics`) — see Storage Metrics Instrumentation
43
+ - Web secure backend override (`setWebSecureStorageBackend`, `getWebSecureStorageBackend`) — see Custom Web Secure Backend
44
+ - IndexedDB backend factory (`createIndexedDBBackend`) — see IndexedDB Backend for Web
45
+ - Bulk import (`storage.import`) — see Bulk Data Import
34
46
  - Batch APIs (`getBatch`, `setBatch`, `removeBatch`) — see Batch Operations and Bulk Bootstrap with Batch APIs
35
47
  - Transactions — see Transactions and Atomic Balance Transfer
36
48
  - Migrations (`registerMigration`, `migrateToLatest`) — see Migrations
@@ -185,21 +197,24 @@ function createStorageItem<T = undefined>(
185
197
  | `coalesceSecureWrites` | `boolean` | `false` | Batch same-tick Secure writes per key |
186
198
  | `namespace` | `string` | — | Prefix key as `namespace:key` for isolation |
187
199
  | `biometric` | `boolean` | `false` | Require biometric auth (Secure scope only) |
200
+ | `biometricLevel` | `BiometricLevel` | `None` | Biometric policy (`BiometryOrPasscode` / `BiometryOnly`) |
188
201
  | `accessControl` | `AccessControl` | — | Keychain access control level (native only) |
189
202
 
190
203
  **Returned `StorageItem<T>`:**
191
204
 
192
- | Method / Property | Type | Description |
193
- | ----------------- | ---------------------------------------- | -------------------------------------------------- |
194
- | `get()` | `() => T` | Read current value (synchronous) |
195
- | `set(value)` | `(value: T \| ((prev: T) => T)) => void` | Write a value or updater function |
196
- | `delete()` | `() => void` | Remove the stored value (resets to `defaultValue`) |
197
- | `has()` | `() => boolean` | Check if a value exists in storage |
198
- | `subscribe(cb)` | `(cb: () => void) => () => void` | Listen for changes, returns unsubscribe |
199
- | `serialize` | `(v: T) => string` | The item's serializer |
200
- | `deserialize` | `(v: string) => T` | The item's deserializer |
201
- | `scope` | `StorageScope` | The item's scope |
202
- | `key` | `string` | The resolved key (includes namespace prefix) |
205
+ | Method / Property | Type | Description |
206
+ | ------------------- | -------------------------------------------------------------------- | ------------------------------------------------------ |
207
+ | `get()` | `() => T` | Read current value (synchronous) |
208
+ | `getWithVersion()` | `() => { value: T; version: StorageVersion }` | Read value plus current storage version token |
209
+ | `set(value)` | `(value: T \| ((prev: T) => T)) => void` | Write a value or updater function |
210
+ | `setIfVersion(...)` | `(version: StorageVersion, value: T \| ((prev: T) => T)) => boolean` | Write only if version matches (optimistic concurrency) |
211
+ | `delete()` | `() => void` | Remove the stored value (resets to `defaultValue`) |
212
+ | `has()` | `() => boolean` | Check if a value exists in storage |
213
+ | `subscribe(cb)` | `(cb: () => void) => () => void` | Listen for changes, returns unsubscribe |
214
+ | `serialize` | `(v: T) => string` | The item's serializer |
215
+ | `deserialize` | `(v: string) => T` | The item's deserializer |
216
+ | `scope` | `StorageScope` | The item's scope |
217
+ | `key` | `string` | The resolved key (includes namespace prefix) |
203
218
 
204
219
  **Non-React subscription example:**
205
220
 
@@ -249,30 +264,42 @@ setToken("new-token");
249
264
  import { storage, StorageScope } from "react-native-nitro-storage";
250
265
  ```
251
266
 
252
- | Method | Description |
253
- | --------------------------------------- | ---------------------------------------------------------------------------- |
254
- | `storage.clear(scope)` | Clear all keys in a scope (`Secure` also clears biometric entries) |
255
- | `storage.clearAll()` | Clear Memory + Disk + Secure |
256
- | `storage.clearNamespace(ns, scope)` | Remove only keys matching a namespace |
257
- | `storage.clearBiometric()` | Remove all biometric-prefixed keys |
258
- | `storage.has(key, scope)` | Check if a key exists |
259
- | `storage.getAllKeys(scope)` | Get all key names |
260
- | `storage.getAll(scope)` | Get all key-value pairs as `Record<string, string>` |
261
- | `storage.size(scope)` | Number of stored keys |
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 |
265
- | `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
267
+ | Method | Description |
268
+ | ---------------------------------------- | ---------------------------------------------------------------------------- |
269
+ | `storage.clear(scope)` | Clear all keys in a scope (`Secure` also clears biometric entries) |
270
+ | `storage.clearAll()` | Clear Memory + Disk + Secure |
271
+ | `storage.clearNamespace(ns, scope)` | Remove only keys matching a namespace |
272
+ | `storage.clearBiometric()` | Remove all biometric-prefixed keys |
273
+ | `storage.has(key, scope)` | Check if a key exists |
274
+ | `storage.getAllKeys(scope)` | Get all key names |
275
+ | `storage.getKeysByPrefix(prefix, scope)` | Get keys that start with a prefix |
276
+ | `storage.getByPrefix(prefix, scope)` | Get raw key-value pairs for keys matching a prefix |
277
+ | `storage.getAll(scope)` | Get all key-value pairs as `Record<string, string>` |
278
+ | `storage.size(scope)` | Number of stored keys |
279
+ | `storage.setAccessControl(level)` | Set default secure access control for subsequent secure writes (native only) |
280
+ | `storage.setSecureWritesAsync(enabled)` | Toggle async secure writes on Android (`false` by default) |
281
+ | `storage.flushSecureWrites()` | Force flush of queued secure writes when coalescing is enabled |
282
+ | `storage.setKeychainAccessGroup(group)` | Set keychain access group for app sharing (native only) |
283
+ | `storage.import(data, scope)` | Bulk-load a `Record<string, string>` of raw key/value pairs into a scope |
284
+ | `storage.setMetricsObserver(observer?)` | Subscribe to per-operation timing events |
285
+ | `storage.getMetricsSnapshot()` | Get aggregate counters/latency stats keyed by operation |
286
+ | `storage.resetMetrics()` | Reset in-memory metrics counters |
266
287
 
267
288
  > `storage.getAll(StorageScope.Secure)` returns regular secure entries. Biometric-protected values are not included in this snapshot API.
268
289
 
269
290
  #### Global utility examples
270
291
 
271
292
  ```ts
272
- import { AccessControl, storage, StorageScope } from "react-native-nitro-storage";
293
+ import {
294
+ AccessControl,
295
+ storage,
296
+ StorageScope,
297
+ } from "react-native-nitro-storage";
273
298
 
274
299
  storage.has("session", StorageScope.Disk);
275
300
  storage.getAllKeys(StorageScope.Disk);
301
+ storage.getKeysByPrefix("user-42:", StorageScope.Disk);
302
+ storage.getByPrefix("user-42:", StorageScope.Disk);
276
303
  storage.getAll(StorageScope.Disk);
277
304
  storage.size(StorageScope.Disk);
278
305
 
@@ -303,6 +330,55 @@ storage.setSecureWritesAsync(true);
303
330
  storage.flushSecureWrites(); // deterministic durability boundary
304
331
  ```
305
332
 
333
+ #### Custom web secure backend
334
+
335
+ By default, web Secure scope uses `localStorage` with `__secure_` key prefixing. You can replace it with a custom backend (for example encrypted IndexedDB adapter).
336
+
337
+ ```ts
338
+ import {
339
+ getWebSecureStorageBackend,
340
+ setWebSecureStorageBackend,
341
+ } from "react-native-nitro-storage";
342
+
343
+ setWebSecureStorageBackend({
344
+ getItem: (key) => encryptedStore.get(key) ?? null,
345
+ setItem: (key, value) => encryptedStore.set(key, value),
346
+ removeItem: (key) => encryptedStore.delete(key),
347
+ clear: () => encryptedStore.clear(),
348
+ getAllKeys: () => encryptedStore.keys(),
349
+ });
350
+
351
+ const backend = getWebSecureStorageBackend();
352
+ console.log("custom backend active:", backend !== undefined);
353
+ ```
354
+
355
+ ---
356
+
357
+ ### IndexedDB Backend for Web
358
+
359
+ The default web Secure backend uses `localStorage`, which is synchronous and size-limited. For large payloads or when you need true persistence across tab reloads, use the built-in IndexedDB-backed factory.
360
+
361
+ ```ts
362
+ import { setWebSecureStorageBackend } from "react-native-nitro-storage";
363
+ import { createIndexedDBBackend } from "react-native-nitro-storage/indexeddb-backend";
364
+
365
+ // call once at app startup, before rendering any components that read secure items
366
+ const backend = await createIndexedDBBackend();
367
+ setWebSecureStorageBackend(backend);
368
+ ```
369
+
370
+ **How it works:**
371
+
372
+ - **Async init**: `createIndexedDBBackend()` opens (or creates) the IndexedDB database and hydrates an in-memory cache from all stored entries before resolving.
373
+ - **Synchronous reads**: all `getItem` calls are served from the in-memory cache — no async overhead after init.
374
+ - **Fire-and-forget writes**: `setItem`, `removeItem`, and `clear` update the cache synchronously, then persist to IndexedDB in the background. The cache is always the authoritative source.
375
+ - **Custom database/store**: optionally pass `dbName` and `storeName` to isolate databases per environment or tenant.
376
+
377
+ ```ts
378
+ const backend = await createIndexedDBBackend("my-app-db", "secure-kv");
379
+ setWebSecureStorageBackend(backend);
380
+ ```
381
+
306
382
  ---
307
383
 
308
384
  ### `createSecureAuthStorage<K>(config, options?)`
@@ -318,7 +394,7 @@ function createSecureAuthStorage<K extends string>(
318
394
 
319
395
  - Default namespace: `"auth"`
320
396
  - Each key is a separate `StorageItem<string>` with `StorageScope.Secure`
321
- - Supports per-key TTL, biometric, and access control
397
+ - Supports per-key TTL, biometric level policy, and access control
322
398
 
323
399
  ---
324
400
 
@@ -550,13 +626,18 @@ const flagsItem = createStorageItem<FeatureFlags>({
550
626
  ### Biometric-protected Secrets
551
627
 
552
628
  ```ts
553
- import { AccessControl, createStorageItem, StorageScope } from "react-native-nitro-storage";
629
+ import {
630
+ AccessControl,
631
+ BiometricLevel,
632
+ createStorageItem,
633
+ StorageScope,
634
+ } from "react-native-nitro-storage";
554
635
 
555
636
  const paymentPin = createStorageItem<string>({
556
637
  key: "payment-pin",
557
638
  scope: StorageScope.Secure,
558
639
  defaultValue: "",
559
- biometric: true,
640
+ biometricLevel: BiometricLevel.BiometryOnly,
560
641
  accessControl: AccessControl.WhenPasscodeSetThisDeviceOnly,
561
642
  });
562
643
 
@@ -686,7 +767,11 @@ const compactItem = createStorageItem<{ id: number; active: boolean }>({
686
767
  ### Coalesced Secure Writes with Deterministic Flush
687
768
 
688
769
  ```ts
689
- import { createStorageItem, storage, StorageScope } from "react-native-nitro-storage";
770
+ import {
771
+ createStorageItem,
772
+ storage,
773
+ StorageScope,
774
+ } from "react-native-nitro-storage";
690
775
 
691
776
  const sessionToken = createStorageItem<string>({
692
777
  key: "session-token",
@@ -702,6 +787,25 @@ sessionToken.set("token-v2");
702
787
  storage.flushSecureWrites();
703
788
  ```
704
789
 
790
+ ### Bulk Data Import
791
+
792
+ Load server-fetched data into storage in one atomic call. All keys become visible simultaneously before any listener fires.
793
+
794
+ ```ts
795
+ import { storage, StorageScope } from "react-native-nitro-storage";
796
+
797
+ // seed local cache from a server response
798
+ const serverData = await fetchInitialData(); // Record<string, string>
799
+ storage.import(serverData, StorageScope.Disk);
800
+
801
+ // all imported keys are immediately readable
802
+ const value = storage.has("remote-config", StorageScope.Disk);
803
+ ```
804
+
805
+ > `storage.import` writes raw string values directly — serialization is bypassed. Use it for bootstrapping data that was already serialized by the server or exported via `storage.getAll`.
806
+
807
+ ---
808
+
705
809
  ### Storage Snapshots and Cleanup
706
810
 
707
811
  ```ts
@@ -718,6 +822,58 @@ if (storage.has("legacy-flag", StorageScope.Disk)) {
718
822
  storage.clearBiometric();
719
823
  ```
720
824
 
825
+ ### Prefix Queries and Namespace Inspection
826
+
827
+ ```ts
828
+ import { storage, StorageScope } from "react-native-nitro-storage";
829
+
830
+ const userKeys = storage.getKeysByPrefix("user-42:", StorageScope.Disk);
831
+ const userRawEntries = storage.getByPrefix("user-42:", StorageScope.Disk);
832
+
833
+ console.log(userKeys);
834
+ console.log(userRawEntries);
835
+ ```
836
+
837
+ ### Optimistic Versioned Writes
838
+
839
+ ```ts
840
+ import { createStorageItem, StorageScope } from "react-native-nitro-storage";
841
+
842
+ const profileItem = createStorageItem({
843
+ key: "profile",
844
+ scope: StorageScope.Disk,
845
+ defaultValue: { name: "Guest" },
846
+ });
847
+
848
+ const snapshot = profileItem.getWithVersion();
849
+ const didWrite = profileItem.setIfVersion(snapshot.version, {
850
+ ...snapshot.value,
851
+ name: "Ada",
852
+ });
853
+
854
+ if (!didWrite) {
855
+ // value changed since snapshot; reload and retry
856
+ }
857
+ ```
858
+
859
+ ### Storage Metrics Instrumentation
860
+
861
+ ```ts
862
+ import { storage } from "react-native-nitro-storage";
863
+
864
+ storage.setMetricsObserver((event) => {
865
+ console.log(
866
+ `[nitro-storage] ${event.operation} scope=${event.scope} duration=${event.durationMs}ms keys=${event.keysCount}`,
867
+ );
868
+ });
869
+
870
+ const metrics = storage.getMetricsSnapshot();
871
+ console.log(metrics["item:get"]?.avgDurationMs);
872
+
873
+ storage.resetMetrics();
874
+ storage.setMetricsObserver(undefined);
875
+ ```
876
+
721
877
  ### Low-level Subscription (outside React)
722
878
 
723
879
  ```ts
@@ -769,6 +925,12 @@ import type {
769
925
  MigrationContext,
770
926
  Migration,
771
927
  TransactionContext,
928
+ StorageVersion,
929
+ VersionedValue,
930
+ StorageMetricsEvent,
931
+ StorageMetricsObserver,
932
+ StorageMetricSummary,
933
+ WebSecureStorageBackend,
772
934
  MMKVLike,
773
935
  SecureAuthStorageConfig,
774
936
  } from "react-native-nitro-storage";
@@ -90,6 +90,14 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysDisk() {
90
90
  return fromJavaStringArray(keys);
91
91
  }
92
92
 
93
+ std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixDisk(const std::string& prefix) {
94
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
95
+ local_ref<JavaStringArray>(std::string)
96
+ >("getKeysByPrefixDisk");
97
+ auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
98
+ return fromJavaStringArray(keys);
99
+ }
100
+
93
101
  size_t AndroidStorageAdapterCpp::sizeDisk() {
94
102
  static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jint()>("sizeDisk");
95
103
  return static_cast<size_t>(method(AndroidStorageAdapterJava::javaClassStatic()));
@@ -166,6 +174,14 @@ std::vector<std::string> AndroidStorageAdapterCpp::getAllKeysSecure() {
166
174
  return fromJavaStringArray(keys);
167
175
  }
168
176
 
177
+ std::vector<std::string> AndroidStorageAdapterCpp::getKeysByPrefixSecure(const std::string& prefix) {
178
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<
179
+ local_ref<JavaStringArray>(std::string)
180
+ >("getKeysByPrefixSecure");
181
+ auto keys = method(AndroidStorageAdapterJava::javaClassStatic(), prefix);
182
+ return fromJavaStringArray(keys);
183
+ }
184
+
169
185
  size_t AndroidStorageAdapterCpp::sizeSecure() {
170
186
  static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<jint()>("sizeSecure");
171
187
  return static_cast<size_t>(method(AndroidStorageAdapterJava::javaClassStatic()));
@@ -222,8 +238,12 @@ void AndroidStorageAdapterCpp::setKeychainAccessGroup(const std::string& /*group
222
238
  // --- Biometric ---
223
239
 
224
240
  void AndroidStorageAdapterCpp::setSecureBiometric(const std::string& key, const std::string& value) {
225
- static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(std::string, std::string)>("setSecureBiometric");
226
- method(AndroidStorageAdapterJava::javaClassStatic(), key, value);
241
+ setSecureBiometricWithLevel(key, value, 2);
242
+ }
243
+
244
+ void AndroidStorageAdapterCpp::setSecureBiometricWithLevel(const std::string& key, const std::string& value, int level) {
245
+ static auto method = AndroidStorageAdapterJava::javaClassStatic()->getStaticMethod<void(std::string, std::string, jint)>("setSecureBiometricWithLevel");
246
+ method(AndroidStorageAdapterJava::javaClassStatic(), key, value, level);
227
247
  }
228
248
 
229
249
  std::optional<std::string> AndroidStorageAdapterCpp::getSecureBiometric(const std::string& key) {
@@ -30,6 +30,7 @@ public:
30
30
  void deleteDisk(const std::string& key) override;
31
31
  bool hasDisk(const std::string& key) override;
32
32
  std::vector<std::string> getAllKeysDisk() override;
33
+ std::vector<std::string> getKeysByPrefixDisk(const std::string& prefix) override;
33
34
  size_t sizeDisk() override;
34
35
  void setDiskBatch(const std::vector<std::string>& keys, const std::vector<std::string>& values) override;
35
36
  std::vector<std::optional<std::string>> getDiskBatch(const std::vector<std::string>& keys) override;
@@ -40,6 +41,7 @@ public:
40
41
  void deleteSecure(const std::string& key) override;
41
42
  bool hasSecure(const std::string& key) override;
42
43
  std::vector<std::string> getAllKeysSecure() override;
44
+ std::vector<std::string> getKeysByPrefixSecure(const std::string& prefix) override;
43
45
  size_t sizeSecure() override;
44
46
  void setSecureBatch(const std::vector<std::string>& keys, const std::vector<std::string>& values) override;
45
47
  std::vector<std::optional<std::string>> getSecureBatch(const std::vector<std::string>& keys) override;
@@ -53,6 +55,7 @@ public:
53
55
  void setKeychainAccessGroup(const std::string& group) override;
54
56
 
55
57
  void setSecureBiometric(const std::string& key, const std::string& value) override;
58
+ void setSecureBiometricWithLevel(const std::string& key, const std::string& value, int level) override;
56
59
  std::optional<std::string> getSecureBiometric(const std::string& key) override;
57
60
  void deleteSecureBiometric(const std::string& key) override;
58
61
  bool hasSecureBiometric(const std::string& key) override;
@@ -3,5 +3,7 @@
3
3
  #include "../../../nitrogen/generated/android/NitroStorageOnLoad.hpp"
4
4
 
5
5
  JNIEXPORT jint JNICALL JNI_OnLoad(JavaVM* vm, void*) {
6
- return margelo::nitro::NitroStorage::initialize(vm);
6
+ return facebook::jni::initialize(vm, []() {
7
+ margelo::nitro::NitroStorage::registerAllNatives();
8
+ });
7
9
  }
@@ -36,6 +36,9 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
36
36
 
37
37
  @Volatile
38
38
  private var secureWritesAsync = false
39
+
40
+ @Volatile
41
+ private var secureKeysCache: Array<String>? = null
39
42
 
40
43
  private fun initializeEncryptedPreferences(name: String, key: MasterKey): SharedPreferences {
41
44
  return try {
@@ -101,6 +104,30 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
101
104
  editor.commit()
102
105
  }
103
106
  }
107
+
108
+ private fun invalidateSecureKeysCache() {
109
+ secureKeysCache = null
110
+ }
111
+
112
+ private fun getSecureKeysCached(): Array<String> {
113
+ val cached = secureKeysCache
114
+ if (cached != null) {
115
+ return cached
116
+ }
117
+
118
+ synchronized(this) {
119
+ val existing = secureKeysCache
120
+ if (existing != null) {
121
+ return existing
122
+ }
123
+ val keys = linkedSetOf<String>()
124
+ keys.addAll(encryptedPreferences.all.keys)
125
+ keys.addAll(biometricPreferences.all.keys)
126
+ val built = keys.toTypedArray()
127
+ secureKeysCache = built
128
+ return built
129
+ }
130
+ }
104
131
 
105
132
  companion object {
106
133
  @Volatile
@@ -188,6 +215,13 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
188
215
  return getInstanceOrThrow().sharedPreferences.all.keys.toTypedArray()
189
216
  }
190
217
 
218
+ @JvmStatic
219
+ fun getKeysByPrefixDisk(prefix: String): Array<String> {
220
+ return getInstanceOrThrow().sharedPreferences.all.keys
221
+ .filter { it.startsWith(prefix) }
222
+ .toTypedArray()
223
+ }
224
+
191
225
  @JvmStatic
192
226
  fun sizeDisk(): Int {
193
227
  return getInstanceOrThrow().sharedPreferences.all.size
@@ -205,6 +239,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
205
239
  val inst = getInstanceOrThrow()
206
240
  val editor = inst.encryptedPreferences.edit().putString(key, value)
207
241
  inst.applySecureEditor(editor)
242
+ inst.invalidateSecureKeysCache()
208
243
  }
209
244
 
210
245
  @JvmStatic
@@ -216,6 +251,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
216
251
  editor.putString(keys[index], values[index])
217
252
  }
218
253
  inst.applySecureEditor(editor)
254
+ inst.invalidateSecureKeysCache()
219
255
  }
220
256
 
221
257
  @JvmStatic
@@ -236,6 +272,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
236
272
  val inst = getInstanceOrThrow()
237
273
  inst.applySecureEditor(inst.encryptedPreferences.edit().remove(key))
238
274
  inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
275
+ inst.invalidateSecureKeysCache()
239
276
  }
240
277
 
241
278
  @JvmStatic
@@ -249,6 +286,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
249
286
  }
250
287
  inst.applySecureEditor(secureEditor)
251
288
  inst.applySecureEditor(biometricEditor)
289
+ inst.invalidateSecureKeysCache()
252
290
  }
253
291
 
254
292
  @JvmStatic
@@ -260,15 +298,17 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
260
298
  @JvmStatic
261
299
  fun getAllKeysSecure(): Array<String> {
262
300
  val inst = getInstanceOrThrow()
263
- val keys = linkedSetOf<String>()
264
- keys.addAll(inst.encryptedPreferences.all.keys)
265
- keys.addAll(inst.biometricPreferences.all.keys)
266
- return keys.toTypedArray()
301
+ return inst.getSecureKeysCached()
302
+ }
303
+
304
+ @JvmStatic
305
+ fun getKeysByPrefixSecure(prefix: String): Array<String> {
306
+ return getAllKeysSecure().filter { it.startsWith(prefix) }.toTypedArray()
267
307
  }
268
308
 
269
309
  @JvmStatic
270
310
  fun sizeSecure(): Int {
271
- return getAllKeysSecure().size
311
+ return getInstanceOrThrow().getSecureKeysCached().size
272
312
  }
273
313
 
274
314
  @JvmStatic
@@ -276,15 +316,22 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
276
316
  val inst = getInstanceOrThrow()
277
317
  inst.applySecureEditor(inst.encryptedPreferences.edit().clear())
278
318
  inst.applySecureEditor(inst.biometricPreferences.edit().clear())
319
+ inst.invalidateSecureKeysCache()
279
320
  }
280
321
 
281
322
  // --- Biometric (separate encrypted store, requires recent biometric auth on Android) ---
282
323
 
283
324
  @JvmStatic
284
325
  fun setSecureBiometric(key: String, value: String) {
326
+ setSecureBiometricWithLevel(key, value, 2)
327
+ }
328
+
329
+ @JvmStatic
330
+ fun setSecureBiometricWithLevel(key: String, value: String, @Suppress("UNUSED_PARAMETER") level: Int) {
285
331
  val inst = getInstanceOrThrow()
286
332
  val editor = inst.biometricPreferences.edit().putString(key, value)
287
333
  inst.applySecureEditor(editor)
334
+ inst.invalidateSecureKeysCache()
288
335
  }
289
336
 
290
337
  @JvmStatic
@@ -296,6 +343,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
296
343
  fun deleteSecureBiometric(key: String) {
297
344
  val inst = getInstanceOrThrow()
298
345
  inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
346
+ inst.invalidateSecureKeysCache()
299
347
  }
300
348
 
301
349
  @JvmStatic
@@ -307,6 +355,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
307
355
  fun clearSecureBiometric() {
308
356
  val inst = getInstanceOrThrow()
309
357
  inst.applySecureEditor(inst.biometricPreferences.edit().clear())
358
+ inst.invalidateSecureKeysCache()
310
359
  }
311
360
  }
312
361
  }