@umutcansu/react-native-pinvault 2.4.1 → 2.4.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,27 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.4.2 — 2026-10-10
4
+
5
+ ### Added
6
+
7
+ - **Expo config plugin** (`app.plugin.js`): at `expo prebuild` it sets the
8
+ iOS deployment target to 16.0, writes the Info.plist keys
9
+ (`faceIDPermission`, `backgroundUpdates`), adds
10
+ `PinVaultBridge.registerBackgroundTask()` to a Swift `AppDelegate`, copies
11
+ the native security file into Android's assets and the iOS app bundle
12
+ (`nativeSecurityFile`), and writes the two native opt-outs only when asked
13
+ (`allowNoNativeSecurityFile`, `pinReactNativeNetworking: false`). Built and
14
+ run with Expo SDK 54 (React Native 0.81), Android and iOS release builds.
15
+
16
+ ### Changed
17
+
18
+ - **Native libraries 2.4.2:** release builds refuse `allowUnsigned()` /
19
+ `allowUnpinnedConfigApi()` unless the block also calls
20
+ `allowRelaxationsInRelease()`. The bridge calls it only where the native
21
+ security file itself allows the relaxation (`allowUnsigned` /
22
+ `allowUnpinned`), so the native file stays the only place that decides;
23
+ JS cannot turn it on.
24
+
3
25
  ## 2.4.1 — 2026-10-10
4
26
 
5
27
  ### Security
package/README.md CHANGED
@@ -29,9 +29,9 @@ npm install @umutcansu/react-native-pinvault
29
29
  cd ios && pod install
30
30
  ```
31
31
 
32
- The native libraries come with it, at the package's own version: npm 2.4.0
33
- uses `io.github.umutcansu:pinvault:2.4.0` from Maven Central and the Swift
34
- package at tag `v2.4.0`. The New Architecture must stay on (React Native's
32
+ The native libraries come with it, at the package's own version: npm 2.4.2
33
+ uses `io.github.umutcansu:pinvault:2.4.2` from Maven Central and the Swift
34
+ package at tag `v2.4.2`. The New Architecture must stay on (React Native's
35
35
  default since 0.76).
36
36
 
37
37
  **iOS.** The pod depends on the PinVault Swift package through React Native's
@@ -65,6 +65,43 @@ hooks before `Application.onCreate` runs (see [Networking](#networking)).
65
65
  **Release builds.** Put the trust anchors into the app itself, not only into
66
66
  the JS bundle: [Native security file](#native-security-file).
67
67
 
68
+ ### Expo
69
+
70
+ A native module: it runs in a development build or `expo prebuild`, not in
71
+ Expo Go. The package's config plugin does the steps above at prebuild — iOS
72
+ deployment target 16.0 (`Podfile.properties.json` and the Xcode project),
73
+ the Info.plist keys, the background task in `AppDelegate.swift`, and the
74
+ native security file copied into Android's assets and the iOS app bundle:
75
+
76
+ ```bash
77
+ npx expo install @umutcansu/react-native-pinvault
78
+ ```
79
+
80
+ ```json
81
+ {
82
+ "expo": {
83
+ "plugins": [
84
+ ["@umutcansu/react-native-pinvault", {
85
+ "nativeSecurityFile": "./pinvault_security.json",
86
+ "faceIDPermission": "Unlock your protected files",
87
+ "backgroundUpdates": true
88
+ }]
89
+ ]
90
+ }
91
+ }
92
+ ```
93
+
94
+ | Option | Does |
95
+ |---|---|
96
+ | `nativeSecurityFile` | path (from the project root) of your `pinvault_security.json`; checked to be a JSON object at prebuild |
97
+ | `faceIDPermission` | `NSFaceIDUsageDescription`, for vault files behind the screen lock (`userAuth`) |
98
+ | `backgroundUpdates` | `BGTaskSchedulerPermittedIdentifiers`, `UIBackgroundModes` = `fetch` and `PinVaultBridge.registerBackgroundTask()` in a Swift `AppDelegate` (Expo SDK 53+), for `schedulePeriodicUpdates` |
99
+ | `allowNoNativeSecurityFile` | `true` writes the opt-out of the file (Android meta-data, Info.plist): release builds then take anchors from JS — not recommended |
100
+ | `pinReactNativeNetworking` | `false` removes the networking provider and writes `PinVaultPinReactNativeNetworking` = NO: RN's own fetch / WebSocket stay unpinned |
101
+
102
+ Unknown options and wrong types stop prebuild. Tested with Expo SDK 54
103
+ (React Native 0.81) on Android and iOS, release builds.
104
+
68
105
  ## Quick start
69
106
 
70
107
  ```ts
@@ -216,7 +253,10 @@ parsed as strictly; a broken file rejects `start` with `E_INVALID_CONFIG`
216
253
  a block that only names itself rejects `start` — otherwise the bundle would
217
254
  supply the pins and keys;
218
255
  - `allowUnsigned`, `allowUnpinnedConfigApi` and `allowServerGeneratedKey` from
219
- JS are refused unless the file allows them for that block;
256
+ JS are refused unless the file allows them for that block; a release build
257
+ of the native libraries (2.4.2+) refuses the first two unless the block also
258
+ says `allowRelaxationsInRelease()`, which the bridge adds only where the file
259
+ allows the relaxation;
220
260
  - a block's `url`, `enrollmentUrl`, `renewalUrl`, `tokenHosts` and
221
261
  `clientCertHosts` are fixed where the file gives them (where the block talks to, and who gets its token and identity),
222
262
  and `attestation: true` / `proofOfPossession: true` there cannot be turned off from JS;
@@ -4,7 +4,7 @@ package = JSON.parse(File.read(File.join(__dir__, "package.json")))
4
4
 
5
5
  # PinVault for iOS is a Swift package (Package.swift at the repository root,
6
6
  # product "PinVault"). React Native's spm_dependency adds it to the Pods project:
7
- # - consumers: the git URL at the tag of this package's version (v2.4.1);
7
+ # - consumers: the git URL at the tag of this package's version (v2.4.2);
8
8
  # - development: PINVAULT_IOS_PACKAGE_PATH = a PinVault checkout (the sample's
9
9
  # Podfile sets it to this repository), the counterpart of pinvault.localPath.
10
10
  pinvault_url = "https://github.com/umutcansu/PinVault.git"
@@ -9,7 +9,7 @@ buildscript {
9
9
  kotlinVersion: "2.2.0",
10
10
  minSdkVersion: 24,
11
11
  compileSdkVersion: 36,
12
- pinvaultVersion: "2.4.1",
12
+ pinvaultVersion: "2.4.2",
13
13
  ]
14
14
  ext.pinvaultRnExt = { prop ->
15
15
  rootProject.ext.has(prop) ? rootProject.ext.get(prop) : PinVaultRN[prop]
@@ -321,6 +321,8 @@ internal object ConfigParser {
321
321
  if (sec.allowUnsigned) this.allowUnsigned()
322
322
  sec.serverScope?.let { this.serverScope(it) }
323
323
  if (sec.allowUnpinned) this.allowUnpinnedConfigApi()
324
+ // The native security file allowed them for this block: the library keeps them in a release build too.
325
+ if (native != null && (sec.allowUnsigned || sec.allowUnpinned)) this.allowRelaxationsInRelease()
324
326
  if (sec.allowServerKey) this.allowServerGeneratedKey()
325
327
  sec.clientCaPins?.let { this.clientCaPins(it) }
326
328
  maxLifetime?.let { this.maxClientCertLifetimeDays(it) }
package/app.plugin.js ADDED
@@ -0,0 +1,2 @@
1
+ // Expo config plugin: see plugin/withPinVault.js and the README's "Expo" section.
2
+ module.exports = require('./plugin/withPinVault');
@@ -346,6 +346,8 @@ public enum ConfigParser {
346
346
  if sec.allowUnsigned { api.allowUnsigned() }
347
347
  if let v = sec.serverScope { api.serverScope(v) }
348
348
  if sec.allowUnpinned { api.allowUnpinnedConfigApi() }
349
+ // The native security file allowed them for this block: the library keeps them in a release build too.
350
+ if native != nil, sec.allowUnsigned || sec.allowUnpinned { api.allowRelaxationsInRelease() }
349
351
  if sec.allowServerKey { api.allowServerGeneratedKey() }
350
352
  if let v = sec.clientCaPins { api.clientCaPins(v) }
351
353
  if let maxLifetime { api.maxClientCertLifetimeDays(maxLifetime) }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umutcansu/react-native-pinvault",
3
- "version": "2.4.1",
3
+ "version": "2.4.2",
4
4
  "description": "PinVault for React Native: dynamic certificate pinning with signed remote configs, mTLS enrollment and signed vault files, as a thin bridge over the native PinVault libraries",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -10,7 +10,8 @@
10
10
  "types": "./lib/typescript/src/index.d.ts",
11
11
  "default": "./lib/module/index.js"
12
12
  },
13
- "./package.json": "./package.json"
13
+ "./package.json": "./package.json",
14
+ "./app.plugin.js": "./app.plugin.js"
14
15
  },
15
16
  "files": [
16
17
  "src",
@@ -23,6 +24,8 @@
23
24
  "ios/*.swift",
24
25
  "ios/Core",
25
26
  "*.podspec",
27
+ "app.plugin.js",
28
+ "plugin",
26
29
  "README.md",
27
30
  "CHANGELOG.md",
28
31
  "LICENSE",
@@ -43,7 +46,8 @@
43
46
  "mtls",
44
47
  "security",
45
48
  "ios",
46
- "android"
49
+ "android",
50
+ "expo"
47
51
  ],
48
52
  "repository": {
49
53
  "type": "git",
@@ -65,6 +69,7 @@
65
69
  "react-native": ">=0.81.0"
66
70
  },
67
71
  "devDependencies": {
72
+ "@expo/config-plugins": "^54.0.5",
68
73
  "@jest/globals": "^29.7.0",
69
74
  "@react-native/babel-preset": "0.87.1",
70
75
  "@react-native/jest-preset": "0.87.1",
@@ -0,0 +1,232 @@
1
+ // Expo config plugin for @umutcansu/react-native-pinvault: does at
2
+ // `expo prebuild` what the README's Install and "Native security file"
3
+ // sections ask of a bare React Native app.
4
+ //
5
+ // plugins: [["@umutcansu/react-native-pinvault", {
6
+ // nativeSecurityFile: "./pinvault_security.json", // → Android assets + iOS app bundle
7
+ // faceIDPermission: "Unlock your protected files", // NSFaceIDUsageDescription (userAuth vault files)
8
+ // backgroundUpdates: true, // schedulePeriodicUpdates on iOS
9
+ // allowNoNativeSecurityFile: false, // release builds without the file (not recommended)
10
+ // pinReactNativeNetworking: true // false = RN's own networking stays unpinned
11
+ // }]]
12
+ //
13
+ // Every option is optional. Without `nativeSecurityFile` a release build
14
+ // refuses a config with Config APIs or static pins (README, "Native security
15
+ // file") unless `allowNoNativeSecurityFile` is true.
16
+ const fs = require('fs');
17
+ const path = require('path');
18
+
19
+ let configPlugins;
20
+ try {
21
+ configPlugins = require('expo/config-plugins');
22
+ } catch (e) {
23
+ configPlugins = require('@expo/config-plugins');
24
+ }
25
+ const {
26
+ AndroidConfig,
27
+ IOSConfig,
28
+ withAndroidManifest,
29
+ withAppDelegate,
30
+ withDangerousMod,
31
+ withInfoPlist,
32
+ withPodfileProperties,
33
+ withXcodeProject,
34
+ createRunOncePlugin,
35
+ } = configPlugins;
36
+
37
+ const pkg = require('../package.json');
38
+
39
+ const SECURITY_FILE = 'pinvault_security.json';
40
+ const BG_TASK_ID = 'io.github.umutcansu.pinvault.refresh';
41
+ const MIN_IOS = '16.0';
42
+ const ALLOW_NO_FILE_META = 'io.github.umutcansu.pinvault.ALLOW_NO_NATIVE_SECURITY_FILE';
43
+ const NETWORKING_PROVIDER = 'io.github.umutcansu.pinvault.reactnative.PinVaultNetworkingInitializer';
44
+ const KNOWN_OPTIONS = [
45
+ 'nativeSecurityFile',
46
+ 'faceIDPermission',
47
+ 'backgroundUpdates',
48
+ 'allowNoNativeSecurityFile',
49
+ 'pinReactNativeNetworking',
50
+ ];
51
+
52
+ function checkOptions(options) {
53
+ const props = options || {};
54
+ const unknown = Object.keys(props).filter((k) => !KNOWN_OPTIONS.includes(k));
55
+ if (unknown.length > 0) {
56
+ throw new Error(`[${pkg.name}] unknown plugin option(s): ${unknown.join(', ')} (known: ${KNOWN_OPTIONS.join(', ')})`);
57
+ }
58
+ for (const k of ['backgroundUpdates', 'allowNoNativeSecurityFile', 'pinReactNativeNetworking']) {
59
+ if (props[k] !== undefined && typeof props[k] !== 'boolean') throw new Error(`[${pkg.name}] ${k} must be true or false`);
60
+ }
61
+ for (const k of ['nativeSecurityFile', 'faceIDPermission']) {
62
+ if (props[k] !== undefined && (typeof props[k] !== 'string' || props[k].trim() === '')) {
63
+ throw new Error(`[${pkg.name}] ${k} must be a non-empty string`);
64
+ }
65
+ }
66
+ return props;
67
+ }
68
+
69
+ /** "15.1" < "16.0": dotted numeric versions. */
70
+ function versionLess(a, b) {
71
+ const x = String(a).split('.').map(Number);
72
+ const y = String(b).split('.').map(Number);
73
+ for (let i = 0; i < Math.max(x.length, y.length); i++) {
74
+ const d = (x[i] || 0) - (y[i] || 0);
75
+ if (d !== 0) return d < 0;
76
+ }
77
+ return false;
78
+ }
79
+
80
+ /** The project's security file, read and parsed (a JSON error stops prebuild, not the app at launch). */
81
+ function readSecurityFile(projectRoot, relative) {
82
+ const source = path.resolve(projectRoot, relative);
83
+ if (!fs.existsSync(source)) throw new Error(`[${pkg.name}] nativeSecurityFile not found: ${source}`);
84
+ const text = fs.readFileSync(source, 'utf8');
85
+ try {
86
+ const parsed = JSON.parse(text);
87
+ if (parsed === null || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('not a JSON object');
88
+ } catch (e) {
89
+ throw new Error(`[${pkg.name}] nativeSecurityFile ${source} is not a JSON object: ${e.message}`);
90
+ }
91
+ return text;
92
+ }
93
+
94
+ /**
95
+ * AppDelegate.swift: `import RNPinVault` and the background task registration
96
+ * as the first line of didFinishLaunchingWithOptions. Idempotent.
97
+ */
98
+ function addBackgroundTaskToAppDelegate(contents, language) {
99
+ if (language !== 'swift') {
100
+ throw new Error(`[${pkg.name}] backgroundUpdates needs a Swift AppDelegate (Expo SDK 53+); add the call by hand: README, "Install"`);
101
+ }
102
+ if (contents.includes('PinVaultBridge.registerBackgroundTask()')) return contents;
103
+ let out = contents;
104
+ if (!/^import RNPinVault$/m.test(out)) {
105
+ const imports = out.match(/^import .+$/gm);
106
+ if (!imports) throw new Error(`[${pkg.name}] AppDelegate.swift has no import lines`);
107
+ const last = imports[imports.length - 1];
108
+ out = out.replace(last, `${last}\nimport RNPinVault`);
109
+ }
110
+ const launch = /(didFinishLaunchingWithOptions launchOptions:[^{]*\{\n)/;
111
+ if (!launch.test(out)) {
112
+ throw new Error(`[${pkg.name}] application(_:didFinishLaunchingWithOptions:) not found in AppDelegate.swift`);
113
+ }
114
+ return out.replace(
115
+ launch,
116
+ '$1 // @umutcansu/react-native-pinvault: schedulePeriodicUpdates (before launch ends)\n' +
117
+ ' _ = PinVaultBridge.registerBackgroundTask()\n'
118
+ );
119
+ }
120
+
121
+ /** Info.plist entries the options ask for. */
122
+ function applyInfoPlist(plist, props) {
123
+ const out = { ...plist };
124
+ if (props.faceIDPermission) out.NSFaceIDUsageDescription = props.faceIDPermission;
125
+ if (props.backgroundUpdates) {
126
+ const ids = Array.isArray(out.BGTaskSchedulerPermittedIdentifiers) ? [...out.BGTaskSchedulerPermittedIdentifiers] : [];
127
+ if (!ids.includes(BG_TASK_ID)) ids.push(BG_TASK_ID);
128
+ out.BGTaskSchedulerPermittedIdentifiers = ids;
129
+ const modes = Array.isArray(out.UIBackgroundModes) ? [...out.UIBackgroundModes] : [];
130
+ if (!modes.includes('fetch')) modes.push('fetch');
131
+ out.UIBackgroundModes = modes;
132
+ }
133
+ if (props.allowNoNativeSecurityFile === true) out.PinVaultAllowNoNativeSecurityFile = true;
134
+ else delete out.PinVaultAllowNoNativeSecurityFile;
135
+ if (props.pinReactNativeNetworking === false) out.PinVaultPinReactNativeNetworking = false;
136
+ else delete out.PinVaultPinReactNativeNetworking;
137
+ return out;
138
+ }
139
+
140
+ /** AndroidManifest: the allow-no-file meta-data and the networking provider opt-out. */
141
+ function applyAndroidManifest(manifest, props) {
142
+ const app = AndroidConfig.Manifest.getMainApplicationOrThrow(manifest);
143
+ if (props.allowNoNativeSecurityFile === true) {
144
+ AndroidConfig.Manifest.addMetaDataItemToMainApplication(app, ALLOW_NO_FILE_META, 'true');
145
+ } else {
146
+ AndroidConfig.Manifest.removeMetaDataItemFromMainApplication(app, ALLOW_NO_FILE_META);
147
+ }
148
+ app.provider = (app.provider || []).filter((p) => p.$['android:name'] !== NETWORKING_PROVIDER);
149
+ if (props.pinReactNativeNetworking === false) {
150
+ manifest.manifest.$['xmlns:tools'] = manifest.manifest.$['xmlns:tools'] || 'http://schemas.android.com/tools';
151
+ app.provider.push({
152
+ $: {
153
+ 'android:name': NETWORKING_PROVIDER,
154
+ 'android:authorities': '${applicationId}.pinvault-networking',
155
+ 'tools:node': 'remove',
156
+ },
157
+ });
158
+ }
159
+ return manifest;
160
+ }
161
+
162
+ function withPinVault(config, options) {
163
+ const props = checkOptions(options);
164
+
165
+ // iOS 16: the PinVault Swift package's minimum.
166
+ config = withPodfileProperties(config, (c) => {
167
+ const current = c.modResults['ios.deploymentTarget'];
168
+ if (!current || versionLess(current, MIN_IOS)) c.modResults['ios.deploymentTarget'] = MIN_IOS;
169
+ return c;
170
+ });
171
+ config = withXcodeProject(config, (c) => {
172
+ const configurations = c.modResults.pbxXCBuildConfigurationSection();
173
+ for (const key of Object.keys(configurations)) {
174
+ const settings = configurations[key].buildSettings;
175
+ if (!settings || settings.IPHONEOS_DEPLOYMENT_TARGET === undefined) continue;
176
+ if (versionLess(String(settings.IPHONEOS_DEPLOYMENT_TARGET).replace(/"/g, ''), MIN_IOS)) {
177
+ settings.IPHONEOS_DEPLOYMENT_TARGET = MIN_IOS;
178
+ }
179
+ }
180
+ return c;
181
+ });
182
+
183
+ config = withInfoPlist(config, (c) => {
184
+ c.modResults = applyInfoPlist(c.modResults, props);
185
+ return c;
186
+ });
187
+ config = withAndroidManifest(config, (c) => {
188
+ c.modResults = applyAndroidManifest(c.modResults, props);
189
+ return c;
190
+ });
191
+
192
+ if (props.backgroundUpdates) {
193
+ config = withAppDelegate(config, (c) => {
194
+ c.modResults.contents = addBackgroundTaskToAppDelegate(c.modResults.contents, c.modResults.language);
195
+ return c;
196
+ });
197
+ }
198
+
199
+ if (props.nativeSecurityFile) {
200
+ // Android: assets (sealed by the APK signature; README, "Native security file").
201
+ config = withDangerousMod(config, [
202
+ 'android',
203
+ async (c) => {
204
+ const text = readSecurityFile(c.modRequest.projectRoot, props.nativeSecurityFile);
205
+ const assets = path.join(c.modRequest.platformProjectRoot, 'app', 'src', 'main', 'assets');
206
+ fs.mkdirSync(assets, { recursive: true });
207
+ fs.writeFileSync(path.join(assets, SECURITY_FILE), text);
208
+ return c;
209
+ },
210
+ ]);
211
+ // iOS: a resource of the app target (sealed by the code signature).
212
+ config = withXcodeProject(config, (c) => {
213
+ const text = readSecurityFile(c.modRequest.projectRoot, props.nativeSecurityFile);
214
+ const projectName = c.modRequest.projectName;
215
+ fs.writeFileSync(path.join(c.modRequest.platformProjectRoot, projectName, SECURITY_FILE), text);
216
+ const file = `${projectName}/${SECURITY_FILE}`;
217
+ if (!c.modResults.hasFile(file)) {
218
+ c.modResults = IOSConfig.XcodeUtils.addResourceFileToGroup({
219
+ filepath: file,
220
+ groupName: projectName,
221
+ project: c.modResults,
222
+ isBuildFile: true,
223
+ });
224
+ }
225
+ return c;
226
+ });
227
+ }
228
+ return config;
229
+ }
230
+
231
+ module.exports = createRunOncePlugin(withPinVault, pkg.name, pkg.version);
232
+ module.exports.internal = { addBackgroundTaskToAppDelegate, applyInfoPlist, applyAndroidManifest, checkOptions, versionLess };