react-native-nitro-storage 0.5.4 → 0.5.5

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 (35) hide show
  1. package/README.md +45 -5
  2. package/android/build.gradle +5 -5
  3. package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +12 -25
  4. package/app.plugin.js +114 -9
  5. package/docs/api-reference.md +39 -36
  6. package/docs/batch-transactions-migrations.md +1 -1
  7. package/docs/recipes.md +1 -1
  8. package/docs/secure-storage.md +15 -4
  9. package/docs/web-backends.md +5 -0
  10. package/lib/commonjs/index.js +49 -9
  11. package/lib/commonjs/index.js.map +1 -1
  12. package/lib/commonjs/index.web.js +71 -11
  13. package/lib/commonjs/index.web.js.map +1 -1
  14. package/lib/commonjs/indexeddb-backend.js +28 -0
  15. package/lib/commonjs/indexeddb-backend.js.map +1 -1
  16. package/lib/commonjs/web-storage-backend.js.map +1 -1
  17. package/lib/module/index.js +49 -9
  18. package/lib/module/index.js.map +1 -1
  19. package/lib/module/index.web.js +71 -11
  20. package/lib/module/index.web.js.map +1 -1
  21. package/lib/module/indexeddb-backend.js +28 -0
  22. package/lib/module/indexeddb-backend.js.map +1 -1
  23. package/lib/module/web-storage-backend.js.map +1 -1
  24. package/lib/typescript/index.d.ts +10 -3
  25. package/lib/typescript/index.d.ts.map +1 -1
  26. package/lib/typescript/index.web.d.ts +10 -3
  27. package/lib/typescript/index.web.d.ts.map +1 -1
  28. package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
  29. package/lib/typescript/web-storage-backend.d.ts +1 -0
  30. package/lib/typescript/web-storage-backend.d.ts.map +1 -1
  31. package/package.json +3 -3
  32. package/src/index.ts +80 -9
  33. package/src/index.web.ts +107 -11
  34. package/src/indexeddb-backend.ts +30 -0
  35. package/src/web-storage-backend.ts +1 -0
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/v/react-native-nitro-storage)](https://www.npmjs.com/package/react-native-nitro-storage)
4
4
  [![MIT license](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
5
5
  [![React Native](https://img.shields.io/badge/react--native-%3E%3D0.75-61dafb)](https://reactnative.dev/)
6
- [![Nitro Modules](https://img.shields.io/badge/nitro--modules-%3E%3D0.35.5-black)](https://nitro.margelo.com/)
6
+ [![Nitro Modules](https://img.shields.io/badge/nitro--modules-%3E%3D0.35.6-black)](https://nitro.margelo.com/)
7
7
 
8
8
  One storage layer for render-time state, persisted app state, and native secrets.
9
9
 
@@ -20,6 +20,7 @@ Use it when you want one storage API for React Native and web, with fast synchro
20
20
  - [Quick Start](#quick-start)
21
21
  - [Storage Scopes](#storage-scopes)
22
22
  - [Docs](#docs)
23
+ - [TypeScript And IDE Safety](#typescript-and-ide-safety)
23
24
  - [Platform Support](#platform-support)
24
25
  - [Security Model](#security-model)
25
26
  - [Migration Paths](#migration-paths)
@@ -83,7 +84,8 @@ bunx expo install react-native-nitro-storage react-native-nitro-modules
83
84
  "react-native-nitro-storage",
84
85
  {
85
86
  "faceIDPermission": "Allow $(PRODUCT_NAME) to protect your secure data with Face ID",
86
- "addBiometricPermissions": true
87
+ "addBiometricPermissions": true,
88
+ "configureAndroidBackup": true
87
89
  }
88
90
  ]
89
91
  ]
@@ -95,7 +97,7 @@ bunx expo install react-native-nitro-storage react-native-nitro-modules
95
97
  bunx expo prebuild
96
98
  ```
97
99
 
98
- The Expo plugin sets `NSFaceIDUsageDescription`, can opt into Android biometric permissions, and initializes the Android storage adapter in `MainApplication`.
100
+ The Expo plugin sets `NSFaceIDUsageDescription`, can opt into Android biometric permissions, initializes the Android storage adapter in `MainApplication`, and writes Android backup rules that exclude Nitro Storage secure preference files from cloud backup and device transfer. Set `configureAndroidBackup: false` only when you maintain equivalent backup rules yourself.
99
101
 
100
102
  Bare React Native projects should install pods after adding the package:
101
103
 
@@ -199,7 +201,7 @@ const snapshot = storage.export(StorageScope.Disk);
199
201
  storage.import(snapshot, StorageScope.Disk);
200
202
  ```
201
203
 
202
- `storage.export(StorageScope.Secure)` returns raw secret values. Do not log Secure exports or include them in diagnostics, analytics, crash reports, or support bundles.
204
+ `storage.export(StorageScope.Secure)` throws unless you pass `{ includeSecureValues: true }`. Prefer `storage.exportSecureUnsafe()` only for short-lived in-memory secure migration workflows. Do not log Secure exports or include them in diagnostics, analytics, crash reports, or support bundles.
203
205
 
204
206
  Subscribe to storage changes outside React:
205
207
 
@@ -284,6 +286,44 @@ The main building blocks are:
284
286
 
285
287
  See the full [API reference](docs/api-reference.md).
286
288
 
289
+ ## TypeScript And IDE Safety
290
+
291
+ The public API is designed around typed storage items. Define the value type, default value, serializer, parser, and validator in one place so reads, writes, React hooks, batch APIs, migrations, and transactions all share the same contract.
292
+
293
+ ```ts
294
+ type Preferences = {
295
+ theme: "system" | "light" | "dark";
296
+ compactMode: boolean;
297
+ };
298
+
299
+ const preferencesItem = createStorageItem<Preferences>({
300
+ key: "preferences",
301
+ scope: StorageScope.Disk,
302
+ defaultValue: { theme: "system", compactMode: false },
303
+ validate: (value): value is Preferences =>
304
+ typeof value === "object" &&
305
+ value !== null &&
306
+ ["system", "light", "dark"].includes(
307
+ (value as Partial<Preferences>).theme ?? "",
308
+ ) &&
309
+ typeof (value as Partial<Preferences>).compactMode === "boolean",
310
+ });
311
+
312
+ preferencesItem.set((current) => ({
313
+ ...current,
314
+ compactMode: !current.compactMode,
315
+ }));
316
+ ```
317
+
318
+ For web backend overrides, import the backend contracts instead of using loose object shapes:
319
+
320
+ ```ts
321
+ import type {
322
+ WebDiskStorageBackend,
323
+ WebSecureStorageBackend,
324
+ } from "react-native-nitro-storage";
325
+ ```
326
+
287
327
  ## Platform Support
288
328
 
289
329
  | Platform | Status | Notes |
@@ -297,7 +337,7 @@ Peer dependencies:
297
337
 
298
338
  - `react >=18.2.0`
299
339
  - `react-native >=0.75.0`
300
- - `react-native-nitro-modules >=0.35.5`
340
+ - `react-native-nitro-modules >=0.35.6`
301
341
 
302
342
  ## Security Model
303
343
 
@@ -28,10 +28,10 @@ def getExtOrIntegerDefault(name) {
28
28
  }
29
29
 
30
30
  android {
31
- namespace "com.nitrostorage"
31
+ namespace = "com.nitrostorage"
32
32
 
33
33
  // Use values from root project or defaults
34
- ndkVersion getExtOrDefault("ndkVersion")
34
+ ndkVersion = getExtOrDefault("ndkVersion")
35
35
  compileSdkVersion getExtOrIntegerDefault("compileSdkVersion")
36
36
 
37
37
  defaultConfig {
@@ -55,8 +55,8 @@ android {
55
55
  }
56
56
 
57
57
  buildFeatures {
58
- buildConfig true
59
- prefab true
58
+ buildConfig = true
59
+ prefab = true
60
60
  }
61
61
 
62
62
  compileOptions {
@@ -79,7 +79,7 @@ repositories {
79
79
  dependencies {
80
80
  //noinspection GradleDynamicVersion
81
81
  implementation "com.facebook.react:react-native:+"
82
- implementation "androidx.security:security-crypto:1.1.0-alpha06"
82
+ implementation "androidx.security:security-crypto:1.1.0"
83
83
  implementation "org.jetbrains.kotlin:kotlin-stdlib:1.9.0"
84
84
  implementation project(":react-native-nitro-modules")
85
85
  }
@@ -1,10 +1,11 @@
1
+ @file:Suppress("DEPRECATION")
2
+
1
3
  package com.nitrostorage
2
4
 
3
5
  import android.content.Context
4
6
  import android.security.keystore.KeyPermanentlyInvalidatedException
5
7
  import android.security.keystore.UserNotAuthenticatedException
6
8
  import android.content.SharedPreferences
7
- import android.util.Log
8
9
  import androidx.security.crypto.EncryptedSharedPreferences
9
10
  import androidx.security.crypto.MasterKey
10
11
  import java.security.InvalidKeyException
@@ -71,7 +72,6 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
71
72
  .build()
72
73
  initializeEncryptedPreferences("NitroStorageBiometric", bioKey)
73
74
  } catch (e: Exception) {
74
- Log.e("NitroStorage", "Biometric storage unavailable: ${e.message}")
75
75
  throw e.wrapStorageException(
76
76
  "NitroStorage: Biometric storage is not available on this device. " +
77
77
  "Ensure biometric hardware is present and credentials are enrolled.",
@@ -96,7 +96,6 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
96
96
  } catch (e: Exception) {
97
97
  when {
98
98
  e.hasCause(AEADBadTagException::class.java) -> {
99
- Log.w("NitroStorage", "Corrupted encryption keys for $name, attempting recovery...")
100
99
  clearCorruptedStorage(name, key)
101
100
  val freshAlias = if (name == "NitroStorageBiometric") biometricMasterKeyAlias else masterKeyAlias
102
101
  val freshKey = MasterKey.Builder(context, freshAlias)
@@ -142,9 +141,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
142
141
  else -> masterKeyAlias
143
142
  }
144
143
  keyStore.deleteEntry(alias)
145
- Log.i("NitroStorage", "Cleared corrupted storage: $name")
146
- } catch (e: Exception) {
147
- Log.e("NitroStorage", "Failed to clear corrupted storage $name: ${e.message}", e)
144
+ } catch (_: Exception) {
148
145
  }
149
146
  }
150
147
 
@@ -153,7 +150,6 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
153
150
  prefs.getString(key, null)
154
151
  } catch (e: Exception) {
155
152
  if (e.hasCause(AEADBadTagException::class.java)) {
156
- Log.w("NitroStorage", "Corrupt entry for key '$key', removing")
157
153
  prefs.edit().remove(key).commit()
158
154
  null
159
155
  } else {
@@ -198,8 +194,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
198
194
  keys.addAll(encryptedPreferences.all.keys.filter { !it.startsWith(INTERNAL_PREFIX) })
199
195
  try {
200
196
  keys.addAll(biometricPreferences.all.keys.filter { !it.startsWith(INTERNAL_PREFIX) })
201
- } catch (e: Exception) {
202
- Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
197
+ } catch (_: Exception) {
203
198
  }
204
199
  val built = keys.toTypedArray()
205
200
  secureKeysCache = built
@@ -369,8 +364,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
369
364
  inst.applySecureEditor(inst.encryptedPreferences.edit().remove(key))
370
365
  try {
371
366
  inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
372
- } catch (e: Exception) {
373
- Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
367
+ } catch (_: Exception) {
374
368
  }
375
369
  inst.invalidateSecureKeysCache()
376
370
  }
@@ -391,8 +385,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
391
385
  biometricEditor.remove(key)
392
386
  }
393
387
  inst.applySecureEditor(biometricEditor)
394
- } catch (e: Exception) {
395
- Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
388
+ } catch (_: Exception) {
396
389
  }
397
390
  inst.invalidateSecureKeysCache()
398
391
  }
@@ -404,8 +397,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
404
397
  val hasInEncrypted = inst.encryptedPreferences.contains(key)
405
398
  val hasInBiometric = try {
406
399
  inst.biometricPreferences.contains(key)
407
- } catch (e: Exception) {
408
- Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
400
+ } catch (_: Exception) {
409
401
  false
410
402
  }
411
403
  return hasInEncrypted || hasInBiometric
@@ -433,8 +425,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
433
425
  inst.applySecureEditor(inst.encryptedPreferences.edit().clear())
434
426
  try {
435
427
  inst.applySecureEditor(inst.biometricPreferences.edit().clear())
436
- } catch (e: Exception) {
437
- Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
428
+ } catch (_: Exception) {
438
429
  }
439
430
  inst.invalidateSecureKeysCache()
440
431
  }
@@ -466,8 +457,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
466
457
  val inst = getInstanceOrThrow()
467
458
  return try {
468
459
  inst.getSecureSafe(inst.biometricPreferences, key)
469
- } catch (e: Exception) {
470
- Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
460
+ } catch (_: Exception) {
471
461
  null
472
462
  }
473
463
  }
@@ -478,8 +468,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
478
468
  try {
479
469
  inst.applySecureEditor(inst.biometricPreferences.edit().remove(key))
480
470
  inst.invalidateSecureKeysCache()
481
- } catch (e: Exception) {
482
- Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
471
+ } catch (_: Exception) {
483
472
  }
484
473
  }
485
474
 
@@ -487,8 +476,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
487
476
  fun hasSecureBiometric(key: String): Boolean {
488
477
  return try {
489
478
  getInstanceOrThrow().biometricPreferences.contains(key)
490
- } catch (e: Exception) {
491
- Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
479
+ } catch (_: Exception) {
492
480
  false
493
481
  }
494
482
  }
@@ -499,8 +487,7 @@ class AndroidStorageAdapter private constructor(private val context: Context) {
499
487
  try {
500
488
  inst.applySecureEditor(inst.biometricPreferences.edit().clear())
501
489
  inst.invalidateSecureKeysCache()
502
- } catch (e: Exception) {
503
- Log.d("NitroStorage", "Biometric storage unavailable: ${e.message}")
490
+ } catch (_: Exception) {
504
491
  }
505
492
  }
506
493
  }
package/app.plugin.js CHANGED
@@ -2,16 +2,100 @@ const {
2
2
  withInfoPlist,
3
3
  withAndroidManifest,
4
4
  withMainApplication,
5
+ withDangerousMod,
5
6
  createRunOncePlugin,
6
7
  } = require("@expo/config-plugins");
8
+ const fs = require("fs");
9
+ const path = require("path");
10
+
11
+ const DATA_EXTRACTION_RULES_RESOURCE =
12
+ "@xml/nitro_storage_data_extraction_rules";
13
+ const FULL_BACKUP_CONTENT_RESOURCE = "@xml/nitro_storage_full_backup_content";
14
+
15
+ const secureSharedPrefs = [
16
+ "NitroStorageSecure.xml",
17
+ "NitroStorageBiometric.xml",
18
+ ];
19
+
20
+ function sharedPrefsExcludes(indent = " ") {
21
+ return secureSharedPrefs
22
+ .map((file) => `${indent}<exclude domain="sharedpref" path="${file}" />`)
23
+ .join("\n");
24
+ }
25
+
26
+ function dataExtractionRulesXml() {
27
+ const excludes = sharedPrefsExcludes(" ");
28
+ return `<?xml version="1.0" encoding="utf-8"?>
29
+ <data-extraction-rules>
30
+ <cloud-backup>
31
+ ${excludes}
32
+ </cloud-backup>
33
+ <device-transfer>
34
+ ${excludes}
35
+ </device-transfer>
36
+ </data-extraction-rules>
37
+ `;
38
+ }
39
+
40
+ function fullBackupContentXml() {
41
+ return `<?xml version="1.0" encoding="utf-8"?>
42
+ <full-backup-content>
43
+ ${sharedPrefsExcludes(" ")}
44
+ </full-backup-content>
45
+ `;
46
+ }
47
+
48
+ function ensureBackupAttributes(androidManifest) {
49
+ const application = androidManifest.manifest.application?.[0];
50
+ if (!application) {
51
+ return;
52
+ }
53
+
54
+ application.$ = application.$ || {};
55
+ if (!application.$["android:dataExtractionRules"]) {
56
+ application.$["android:dataExtractionRules"] =
57
+ DATA_EXTRACTION_RULES_RESOURCE;
58
+ }
59
+ if (!application.$["android:fullBackupContent"]) {
60
+ application.$["android:fullBackupContent"] = FULL_BACKUP_CONTENT_RESOURCE;
61
+ }
62
+ }
63
+
64
+ function writeAndroidBackupFiles(projectRoot) {
65
+ const xmlDir = path.join(
66
+ projectRoot,
67
+ "android",
68
+ "app",
69
+ "src",
70
+ "main",
71
+ "res",
72
+ "xml",
73
+ );
74
+ fs.mkdirSync(xmlDir, { recursive: true });
75
+ fs.writeFileSync(
76
+ path.join(xmlDir, "nitro_storage_data_extraction_rules.xml"),
77
+ dataExtractionRulesXml(),
78
+ );
79
+ fs.writeFileSync(
80
+ path.join(xmlDir, "nitro_storage_full_backup_content.xml"),
81
+ fullBackupContentXml(),
82
+ );
83
+ }
7
84
 
8
85
  const withNitroStorage = (config, props = {}) => {
9
86
  const defaultFaceIDPermission =
10
87
  "Allow $(PRODUCT_NAME) to use Face ID for secure authentication";
11
- const { faceIDPermission, addBiometricPermissions = false } = props;
88
+ const {
89
+ faceIDPermission,
90
+ addBiometricPermissions = false,
91
+ configureAndroidBackup = true,
92
+ } = props;
12
93
 
13
94
  config = withInfoPlist(config, (config) => {
14
- if (typeof faceIDPermission === "string" && faceIDPermission.trim() !== "") {
95
+ if (
96
+ typeof faceIDPermission === "string" &&
97
+ faceIDPermission.trim() !== ""
98
+ ) {
15
99
  config.modResults.NSFaceIDUsageDescription = faceIDPermission;
16
100
  } else if (!config.modResults.NSFaceIDUsageDescription) {
17
101
  config.modResults.NSFaceIDUsageDescription = defaultFaceIDPermission;
@@ -20,6 +104,10 @@ const withNitroStorage = (config, props = {}) => {
20
104
  });
21
105
 
22
106
  config = withAndroidManifest(config, (config) => {
107
+ if (configureAndroidBackup) {
108
+ ensureBackupAttributes(config.modResults);
109
+ }
110
+
23
111
  if (!addBiometricPermissions) {
24
112
  return config;
25
113
  }
@@ -38,10 +126,10 @@ const withNitroStorage = (config, props = {}) => {
38
126
  };
39
127
 
40
128
  const hasBiometric = permissions.some(
41
- (p) => p.$?.["android:name"] === "android.permission.USE_BIOMETRIC"
129
+ (p) => p.$?.["android:name"] === "android.permission.USE_BIOMETRIC",
42
130
  );
43
131
  const hasFingerprint = permissions.some(
44
- (p) => p.$?.["android:name"] === "android.permission.USE_FINGERPRINT"
132
+ (p) => p.$?.["android:name"] === "android.permission.USE_FINGERPRINT",
45
133
  );
46
134
 
47
135
  if (!hasBiometric) {
@@ -54,6 +142,16 @@ const withNitroStorage = (config, props = {}) => {
54
142
  return config;
55
143
  });
56
144
 
145
+ if (configureAndroidBackup) {
146
+ config = withDangerousMod(config, [
147
+ "android",
148
+ async (config) => {
149
+ writeAndroidBackupFiles(config.modRequest.projectRoot);
150
+ return config;
151
+ },
152
+ ]);
153
+ }
154
+
57
155
  config = withMainApplication(config, (config) => {
58
156
  const { modResults } = config;
59
157
  const { language, contents } = modResults;
@@ -67,13 +165,13 @@ const withNitroStorage = (config, props = {}) => {
67
165
  if (!contents.includes(importStatement)) {
68
166
  modResults.contents = contents.replace(
69
167
  /(package .*;\n)/,
70
- `$1\n${importStatement}\n`
168
+ `$1\n${importStatement}\n`,
71
169
  );
72
170
  }
73
171
 
74
172
  modResults.contents = modResults.contents.replace(
75
173
  /(super\.onCreate\(\);)/,
76
- `$1\n${initStatement}`
174
+ `$1\n${initStatement}`,
77
175
  );
78
176
  }
79
177
  } else if (language === "kt") {
@@ -84,13 +182,13 @@ const withNitroStorage = (config, props = {}) => {
84
182
  if (!contents.includes(importStatement)) {
85
183
  modResults.contents = contents.replace(
86
184
  /(package .*\n)/,
87
- `$1\n${importStatement}\n`
185
+ `$1\n${importStatement}\n`,
88
186
  );
89
187
  }
90
188
 
91
189
  modResults.contents = modResults.contents.replace(
92
190
  /(super\.onCreate\(\))/,
93
- `$1\n${initStatement}`
191
+ `$1\n${initStatement}`,
94
192
  );
95
193
  }
96
194
  }
@@ -104,5 +202,12 @@ const withNitroStorage = (config, props = {}) => {
104
202
  module.exports = createRunOncePlugin(
105
203
  withNitroStorage,
106
204
  "react-native-nitro-storage",
107
- "1.0.0"
205
+ "1.0.0",
108
206
  );
207
+ module.exports.withNitroStorage = withNitroStorage;
208
+ module.exports._internal = {
209
+ dataExtractionRulesXml,
210
+ fullBackupContentXml,
211
+ ensureBackupAttributes,
212
+ writeAndroidBackupFiles,
213
+ };
@@ -72,41 +72,42 @@ See [react-hooks.md](react-hooks.md).
72
72
 
73
73
  `storage` exposes raw and cross-item utilities:
74
74
 
75
- | Method | Purpose |
76
- | ------------------------------------------------ | ------------------------------------------------------------- |
77
- | `clear(scope)` | Clear one scope. |
78
- | `clearAll()` | Clear Memory, Disk, and Secure scopes. |
79
- | `clearNamespace(namespace, scope)` | Remove keys under `namespace:`. |
80
- | `subscribe(scope, listener)` | Subscribe to raw scope-level change events. |
81
- | `subscribeKey(scope, key, listener)` | Subscribe to raw events for one key. |
82
- | `subscribePrefix(scope, prefix, listener)` | Subscribe to raw events for matching key prefixes. |
83
- | `subscribeNamespace(namespace, scope, listener)` | Subscribe to raw events for `namespace:` keys. |
84
- | `setEventObserver(observer)` | Receive all change events for devtools or logging. |
85
- | `clearBiometric()` | Clear biometric Secure entries. |
86
- | `has(key, scope)` | Check for a raw key. |
87
- | `getAllKeys(scope)` | List raw keys. |
88
- | `getKeysByPrefix(prefix, scope)` | List raw keys with a prefix. |
89
- | `getByPrefix(prefix, scope)` | Read raw string values by prefix. |
90
- | `getAll(scope)` | Read all raw string values in a scope. |
91
- | `size(scope)` | Return approximate scope entry count. |
92
- | `setAccessControl(accessControl)` | Set the default Secure access control level. |
93
- | `setSecureWritesAsync(enabled)` | Toggle Android secure writes between sync and async modes. |
94
- | `setDiskWritesAsync(enabled)` | Toggle coalesced Disk write behavior. |
95
- | `flushDiskWrites()` | Flush pending Disk writes. |
96
- | `flushSecureWrites()` | Flush pending Secure writes. |
97
- | `setKeychainAccessGroup(group)` | Configure iOS Keychain access group. |
98
- | `setMetricsObserver(observer)` | Receive operation timing events. |
99
- | `getMetricsSnapshot()` | Read aggregated metrics. |
100
- | `resetMetrics()` | Clear metrics counters. |
101
- | `getCapabilities()` | Read runtime storage capabilities. |
102
- | `getSecurityCapabilities()` | Read secure backend capability metadata. |
103
- | `getSecureMetadata(key)` | Read secure metadata for one key without returning its value. |
104
- | `getAllSecureMetadata()` | Read secure metadata for all secure keys without values. |
105
- | `getString(key, scope)` | Read a raw string. |
106
- | `setString(key, value, scope)` | Write a raw string. |
107
- | `deleteString(key, scope)` | Remove a raw key. |
108
- | `export(scope)` | Snapshot raw strings from one scope. |
109
- | `import(data, scope)` | Bulk import raw strings. |
75
+ | Method | Purpose |
76
+ | ------------------------------------------------ | ----------------------------------------------------------------------------------------- |
77
+ | `clear(scope)` | Clear one scope. |
78
+ | `clearAll()` | Clear Memory, Disk, and Secure scopes. |
79
+ | `clearNamespace(namespace, scope)` | Remove keys under `namespace:`. |
80
+ | `subscribe(scope, listener)` | Subscribe to raw scope-level change events. |
81
+ | `subscribeKey(scope, key, listener)` | Subscribe to raw events for one key. |
82
+ | `subscribePrefix(scope, prefix, listener)` | Subscribe to raw events for matching key prefixes. |
83
+ | `subscribeNamespace(namespace, scope, listener)` | Subscribe to raw events for `namespace:` keys. |
84
+ | `setEventObserver(observer, options?)` | Receive all change events for devtools or logging. Secure values are redacted by default. |
85
+ | `clearBiometric()` | Clear biometric Secure entries. |
86
+ | `has(key, scope)` | Check for a raw key. |
87
+ | `getAllKeys(scope)` | List raw keys. |
88
+ | `getKeysByPrefix(prefix, scope)` | List raw keys with a prefix. |
89
+ | `getByPrefix(prefix, scope)` | Read raw string values by prefix. |
90
+ | `getAll(scope)` | Read all raw string values in a scope. |
91
+ | `size(scope)` | Return approximate scope entry count. |
92
+ | `setAccessControl(accessControl)` | Set the default Secure access control level. |
93
+ | `setSecureWritesAsync(enabled)` | Toggle Android secure writes between sync and async modes. |
94
+ | `setDiskWritesAsync(enabled)` | Toggle coalesced Disk write behavior. |
95
+ | `flushDiskWrites()` | Flush pending Disk writes. |
96
+ | `flushSecureWrites()` | Flush pending Secure writes. |
97
+ | `setKeychainAccessGroup(group)` | Configure iOS Keychain access group. |
98
+ | `setMetricsObserver(observer)` | Receive operation timing events. |
99
+ | `getMetricsSnapshot()` | Read aggregated metrics. |
100
+ | `resetMetrics()` | Clear metrics counters. |
101
+ | `getCapabilities()` | Read runtime storage capabilities. |
102
+ | `getSecurityCapabilities()` | Read secure backend capability metadata. |
103
+ | `getSecureMetadata(key)` | Read secure metadata for one key without returning its value. |
104
+ | `getAllSecureMetadata()` | Read secure metadata for all secure keys without values. |
105
+ | `getString(key, scope)` | Read a raw string. |
106
+ | `setString(key, value, scope)` | Write a raw string. |
107
+ | `deleteString(key, scope)` | Remove a raw key. |
108
+ | `export(scope, options?)` | Snapshot raw strings from one scope. Secure scope requires explicit unsafe opt-in. |
109
+ | `exportSecureUnsafe()` | Snapshot raw Secure strings for short-lived migration workflows. |
110
+ | `import(data, scope)` | Bulk import raw strings. |
110
111
 
111
112
  Raw string APIs bypass item serialization and validation. Prefer `StorageItem<T>` unless you are migrating, exporting/importing, or writing a custom integration.
112
113
 
@@ -115,7 +116,7 @@ const diskSnapshot = storage.export(StorageScope.Disk);
115
116
  storage.import(diskSnapshot, StorageScope.Disk);
116
117
  ```
117
118
 
118
- Secure exports contain raw secret values. Do not log `storage.export(StorageScope.Secure)` output or attach it to diagnostics, analytics, crash reports, or support bundles.
119
+ Secure exports contain raw secret values. `storage.export(StorageScope.Secure)` throws unless called with `{ includeSecureValues: true }`; `storage.exportSecureUnsafe()` is the explicit equivalent. Do not log Secure exports or attach them to diagnostics, analytics, crash reports, or support bundles.
119
120
 
120
121
  ## Event Subscriptions
121
122
 
@@ -149,6 +150,8 @@ storage.setEventObserver((event) => {
149
150
  });
150
151
  ```
151
152
 
153
+ `setEventObserver()` redacts Secure `oldValue` and `newValue` fields by default. Pass `{ redactSecureValues: false }` only for in-memory debugging paths that never persist logs. Raw `subscribe*()` APIs preserve values for state integrations.
154
+
152
155
  Local batch APIs emit one `type: "batch"` envelope to scope and prefix/namespace listeners. Key subscribers receive the matching per-key change so direct key integrations do not need to unpack batch envelopes. Secure events can include raw secret values; do not log Secure event payloads in production.
153
156
 
154
157
  ## Batch Operations
@@ -82,7 +82,7 @@ storage.import(snapshot, StorageScope.Disk);
82
82
 
83
83
  For Memory scope, import is atomic: all keys are written before listeners fire. For Disk and Secure, import delegates to native or web batch paths.
84
84
 
85
- Secure exports contain raw secret values. Do not log them or include them in diagnostics, analytics, crash reports, or support bundles.
85
+ Secure exports contain raw secret values. `storage.export(StorageScope.Secure)` requires `{ includeSecureValues: true }`; `storage.exportSecureUnsafe()` is the explicit equivalent. Do not log Secure exports or include them in diagnostics, analytics, crash reports, or support bundles.
86
86
 
87
87
  ## Transactions
88
88
 
package/docs/recipes.md CHANGED
@@ -278,7 +278,7 @@ storage.setEventObserver((event) => {
278
278
  });
279
279
  ```
280
280
 
281
- Use `subscribePrefix()` or `subscribeNamespace()` for targeted integrations. Use `setEventObserver()` for devtools-style logging. Secure events can include raw secret values, so filter them out of logs.
281
+ Use `subscribePrefix()` or `subscribeNamespace()` for targeted integrations. Use `setEventObserver()` for devtools-style logging; it redacts Secure values by default. Raw Secure subscriptions can include secret values, so filter them out of logs.
282
282
 
283
283
  ## Capability Checks
284
284
 
@@ -4,6 +4,8 @@ Secure scope is for secrets: refresh tokens, credentials, API tokens, and device
4
4
 
5
5
  Use Disk scope for non-secret persisted state. Secure storage has stronger boundaries but more platform rules, especially around biometric prompts, device lock state, and backup/restore behavior.
6
6
 
7
+ Keep Secure values small. Platform secure stores are optimized for credentials and keys, not large payloads or support bundles.
8
+
7
9
  ## Store a Secure Token
8
10
 
9
11
  ```ts
@@ -116,12 +118,12 @@ const allKeys = storage.getAllSecureMetadata();
116
118
 
117
119
  ## Secure Export Warning
118
120
 
119
- `storage.export(StorageScope.Secure)` returns raw secret values so it can round-trip with `storage.import(data, StorageScope.Secure)`.
121
+ `storage.export(StorageScope.Secure)` throws unless you explicitly opt into exposing raw secret values. Use `storage.exportSecureUnsafe()` or `storage.export(StorageScope.Secure, { includeSecureValues: true })` only when you need to round-trip with `storage.import(data, StorageScope.Secure)`.
120
122
 
121
123
  ```ts
122
124
  import { storage, StorageScope } from "react-native-nitro-storage";
123
125
 
124
- const secureSnapshot = storage.export(StorageScope.Secure);
126
+ const secureSnapshot = storage.exportSecureUnsafe();
125
127
  storage.import(secureSnapshot, StorageScope.Secure);
126
128
  ```
127
129
 
@@ -129,9 +131,18 @@ Only keep Secure exports in memory for the shortest possible workflow. Do not lo
129
131
 
130
132
  ## Secure Event Warning
131
133
 
132
- Secure scope event subscriptions and `storage.setEventObserver()` can receive raw secret values in `oldValue`, `newValue`, or batch `changes`.
134
+ Secure scope event subscriptions can receive raw secret values in `oldValue`, `newValue`, or batch `changes`. `storage.setEventObserver()` redacts Secure values by default because observer callbacks are commonly used for logging and devtools.
135
+
136
+ Use Secure events for in-memory coordination only. Do not log Secure event payloads or send them to analytics, crash reporting, support bundles, or devtools sessions that persist outside the device. Pass `{ redactSecureValues: false }` to `setEventObserver()` only for local, non-persistent debugging.
137
+
138
+ ## Android Backup Rules
139
+
140
+ Android secure storage uses encrypted SharedPreferences. Restored encrypted preference files can become unreadable when the app's Keystore keys are not restored with them. The Expo plugin configures backup exclusions for Nitro Storage secure files by default:
141
+
142
+ - `NitroStorageSecure.xml`
143
+ - `NitroStorageBiometric.xml`
133
144
 
134
- Use Secure events for in-memory coordination only. Do not log Secure event payloads or send them to analytics, crash reporting, support bundles, or devtools sessions that persist outside the device.
145
+ If you disable `configureAndroidBackup` or maintain custom Android backup XML, add equivalent exclusions for both cloud backup and device transfer.
135
146
 
136
147
  ## Locked Keychain Errors
137
148
 
@@ -33,10 +33,13 @@ Optional methods improve performance and observability:
33
33
  - `size()`
34
34
  - `subscribe(listener)`
35
35
  - `flush()`
36
+ - `close()`
36
37
  - `name`
37
38
 
38
39
  `subscribe(listener)` should report `{ key, newValue }` changes. Use `key: null` when the whole backend is cleared.
39
40
 
41
+ `close()` should release backend-owned resources such as database handles or broadcast channels. Nitro Storage calls it when a configured Disk or Secure backend is replaced.
42
+
40
43
  ## Disk Backend
41
44
 
42
45
  ```ts
@@ -95,6 +98,8 @@ setWebSecureStorageBackend(backend);
95
98
 
96
99
  Reads are synchronous because they are served from memory after initial load. Writes update memory first and persist to IndexedDB in the background.
97
100
 
101
+ The IndexedDB backend exposes `close()` and rejects later synchronous operations after it is closed.
102
+
98
103
  ## Cross-tab Updates
99
104
 
100
105
  The IndexedDB backend uses `BroadcastChannel` when available. Other tabs receive cache invalidation events and update their in-memory copy.