react-native-nitro-storage 0.5.3 → 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.
- package/.watchmanconfig +6 -0
- package/README.md +45 -5
- package/android/build.gradle +5 -5
- package/android/src/main/java/com/nitrostorage/AndroidStorageAdapter.kt +12 -25
- package/app.plugin.js +114 -9
- package/docs/api-reference.md +39 -36
- package/docs/batch-transactions-migrations.md +1 -1
- package/docs/recipes.md +1 -1
- package/docs/secure-storage.md +15 -4
- package/docs/web-backends.md +5 -0
- package/lib/commonjs/index.js +129 -27
- package/lib/commonjs/index.js.map +1 -1
- package/lib/commonjs/index.web.js +169 -32
- package/lib/commonjs/index.web.js.map +1 -1
- package/lib/commonjs/indexeddb-backend.js +28 -0
- package/lib/commonjs/indexeddb-backend.js.map +1 -1
- package/lib/commonjs/web-storage-backend.js.map +1 -1
- package/lib/module/index.js +129 -27
- package/lib/module/index.js.map +1 -1
- package/lib/module/index.web.js +169 -32
- package/lib/module/index.web.js.map +1 -1
- package/lib/module/indexeddb-backend.js +28 -0
- package/lib/module/indexeddb-backend.js.map +1 -1
- package/lib/module/web-storage-backend.js.map +1 -1
- package/lib/typescript/index.d.ts +10 -3
- package/lib/typescript/index.d.ts.map +1 -1
- package/lib/typescript/index.web.d.ts +10 -3
- package/lib/typescript/index.web.d.ts.map +1 -1
- package/lib/typescript/indexeddb-backend.d.ts.map +1 -1
- package/lib/typescript/web-storage-backend.d.ts +1 -0
- package/lib/typescript/web-storage-backend.d.ts.map +1 -1
- package/package.json +5 -3
- package/src/index.ts +197 -32
- package/src/index.web.ts +250 -37
- package/src/indexeddb-backend.ts +30 -0
- package/src/web-storage-backend.ts +1 -0
package/.watchmanconfig
ADDED
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
[](https://www.npmjs.com/package/react-native-nitro-storage)
|
|
4
4
|
[](LICENSE)
|
|
5
5
|
[](https://reactnative.dev/)
|
|
6
|
-
[](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,
|
|
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)`
|
|
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.
|
|
340
|
+
- `react-native-nitro-modules >=0.35.6`
|
|
301
341
|
|
|
302
342
|
## Security Model
|
|
303
343
|
|
package/android/build.gradle
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 (
|
|
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 {
|
|
88
|
+
const {
|
|
89
|
+
faceIDPermission,
|
|
90
|
+
addBiometricPermissions = false,
|
|
91
|
+
configureAndroidBackup = true,
|
|
92
|
+
} = props;
|
|
12
93
|
|
|
13
94
|
config = withInfoPlist(config, (config) => {
|
|
14
|
-
if (
|
|
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
|
+
};
|
package/docs/api-reference.md
CHANGED
|
@@ -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)`
|
|
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)`
|
|
109
|
-
| `
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
package/docs/secure-storage.md
CHANGED
|
@@ -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)`
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
package/docs/web-backends.md
CHANGED
|
@@ -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.
|