@umutcansu/react-native-pinvault 2.7.0 → 2.8.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.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,31 @@
1
1
  # Changelog
2
2
 
3
+ ## 2.8.1 — 2026-10-15
4
+
5
+ ### Changed
6
+
7
+ - **iOS: a pinned session notices when its trust check was skipped.** The
8
+ generic certificate-validation killers answer URLSession's server-trust
9
+ challenge before the library's delegate; such a connection is now refused
10
+ (`fetch` rejects with an SSL handshake error), a `wss://` socket cancelled,
11
+ and the host reported as `hooking_framework` evidence `tls-unverified:<host>`.
12
+ Depends on PinVault 2.8.1 (Swift package tag `v2.8.1`).
13
+
14
+ ## 2.8.0 — 2026-10-15
15
+
16
+ ### Changed
17
+
18
+ - **Release builds take a block's addresses only from the native security
19
+ file**: a declared block names its `url` there, and `enrollmentUrl` /
20
+ `renewalUrl` from JS are refused where the file names none (the device's
21
+ identity goes to all three, its token to `url`). iOS `ios.resolve` follows the
22
+ same rule (top-level `"ios": {"resolve": {…}}` in the file; fixed wherever the
23
+ file gives it). Add them to `pinvault_security.json` if a release build now
24
+ refuses to start.
25
+ - `PinVault.fetch` refuses `Host`, `Connection`, `Keep-Alive`,
26
+ `Proxy-Connection`, `TE`, `Trailer`, `Transfer-Encoding`, `Upgrade` and
27
+ `Content-Length` from JS: the HTTP client sets them.
28
+
3
29
  ## 2.7.0 — 2026-10-15
4
30
 
5
31
  ### Changed
@@ -11,7 +37,7 @@
11
37
  library's default sends `PinVault-Token` to the block's own Config API only,
12
38
  so a `tokenHosts` from JS would add hosts that receive the device's token.
13
39
  Debug builds and files that name the hosts are unchanged. Move your
14
- `tokenHosts` into `pinvault-security.json` (Android: `res/raw`, iOS: the
40
+ `tokenHosts` into `pinvault_security.json` (Android: `assets`, iOS: the
15
41
  bundle) if a release build now refuses to start.
16
42
 
17
43
  ## 2.6.1 — 2026-10-15
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.7.0
33
- uses `io.github.umutcansu:pinvault:2.7.0` from Maven Central and the Swift
34
- package at tag `v2.7.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.8.1
33
+ uses `io.github.umutcansu:pinvault:2.8.1` from Maven Central and the Swift
34
+ package at tag `v2.8.1`. 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
@@ -165,7 +165,8 @@ names), enum values are the Kotlin constant names (`'TOKEN_MTLS'`,
165
165
  requirePinnedReactNativeNetworking, // see Networking
166
166
  android: { requireUnlockedDeviceAllowFallback, pinGlobalNetworking,
167
167
  keepReactNativeHttpCache, keepReactNativeCookies },
168
- ios: { resolve: { 'mock-tls.sample': '10.0.0.12' }, expectedBundleIds, expectedTeamIds, userAuthStrength,
168
+ ios: { resolve: { 'mock-tls.sample': '10.0.0.12' }, // release: only from the native security file
169
+ expectedBundleIds, expectedTeamIds, userAuthStrength,
169
170
  reactNativeMaxResponseBytes },
170
171
  }
171
172
  ```
@@ -236,6 +237,7 @@ signature. Ship them natively as well, in `pinvault_security.json`:
236
237
  "allowUnsigned": false, "allowUnpinnedConfigApi": false, "allowServerGeneratedKey": false
237
238
  }],
238
239
  "staticPins": { "pins": [ … ], "version": 1 },
240
+ "ios": { "resolve": { "api.example.com": "10.0.0.12" } },
239
241
  "require": {
240
242
  "requireUnlockedDevice": true, "requireHardwareBackedKeys": true,
241
243
  "managedTrustRoots": true, "wipeVaultFilesOnRevocation": true,
@@ -262,9 +264,9 @@ parsed as strictly; a broken file rejects `start` with `E_INVALID_CONFIG`
262
264
  applies) or repeat it (lists in any order); a different value rejects `start`
263
265
  with `E_INVALID_CONFIG` naming the field;
264
266
  - in a release build a declared block must carry its anchors: `bootstrapPins`
265
- (or `allowUnpinnedConfigApi`) and `signaturePublicKeys` (or `allowUnsigned`);
266
- a block that only names itself rejects `start` — otherwise the bundle would
267
- supply the pins and keys;
267
+ (or `allowUnpinnedConfigApi`), `signaturePublicKeys` (or `allowUnsigned`) and
268
+ its `url` (2.8.0+: the device's token and identity go there); a block that
269
+ only names itself rejects `start` — otherwise the bundle would supply them;
268
270
  - `allowUnsigned`, `allowUnpinnedConfigApi` and `allowServerGeneratedKey` from
269
271
  JS are refused unless the file allows them for that block; a release build
270
272
  of the native libraries (2.4.2+) refuses the first two unless the block also
@@ -273,10 +275,13 @@ parsed as strictly; a broken file rejects `start` with `E_INVALID_CONFIG`
273
275
  - a block's `url`, `enrollmentUrl`, `renewalUrl`, `tokenHosts` and
274
276
  `clientCertHosts` are fixed where the file gives them (where the block talks to, and who gets its token and identity),
275
277
  and `attestation: true` / `proofOfPossession: true` there cannot be turned off from JS;
276
- in a release build JS may give `clientCertHosts` and `tokenHosts` only where
277
- the file names them (the bundle cannot hand the device's identity or its
278
- `PinVault-Token` to another host; the library's default sends the token to the
279
- block's own Config API only);
278
+ in a release build JS may give `enrollmentUrl`, `renewalUrl` (2.8.0+),
279
+ `clientCertHosts` and `tokenHosts` only where the file names them (the bundle
280
+ cannot hand the device's identity or its `PinVault-Token` to another host; the
281
+ library presents the identity to the block's url, enrollment and renewal
282
+ addresses, and its default sends the token to the block's own Config API only);
283
+ - iOS `ios.resolve` (where a pinned host's connection goes) is fixed where the
284
+ file gives it, and a release build takes it only from the file (2.8.0+);
280
285
  - `require` only tightens: a protection it turns on stays on whatever JS says,
281
286
  its `requireCaTrust` hosts are added to JS's, the expected signer / bundle /
282
287
  team ids and iOS `userAuthStrength` are fixed, and `expiredConfigGraceSeconds`
@@ -457,7 +462,10 @@ as one more signal. The decisive check is server-side attestation
457
462
  - **NETWORK** — pinned native path only; no unpinned fallback; `https` → `http`
458
463
  redirects are not followed; RN's own fetch / XHR / images / WebSocket are pinned on
459
464
  both platforms, fail closed before `start`, and a replaced
460
- hook is detected.
465
+ hook is detected. On iOS (2.8.1+) a TLS connection whose certificate never
466
+ reached the library's check (an "SSL kill switch" tweak answered it first) is
467
+ refused and reported in the attestation report (`hooking_framework`,
468
+ `tls-unverified:<host>`).
461
469
  - **Trust anchors** — release builds take the relaxations only from the native
462
470
  security file; with the file, JS cannot change pins, keys or scopes.
463
471
  - **STORAGE** — the plugin stores nothing in JS (no AsyncStorage); tokens live
@@ -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.7.0);
7
+ # - consumers: the git URL at the tag of this package's version (v2.8.1);
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.7.0",
12
+ pinvaultVersion: "2.8.1",
13
13
  ]
14
14
  ext.pinvaultRnExt = { prop ->
15
15
  rootProject.ext.has(prop) ? rootProject.ext.get(prop) : PinVaultRN[prop]
@@ -278,6 +278,20 @@ internal object ConfigParser {
278
278
  val blockUrl = nativeBlock?.url?.also { if (it != url) fixed("url") } ?: url
279
279
  val fixedEnrollmentUrl = nativeBlock?.enrollmentUrl?.also { if (enrollmentUrl != null && it != enrollmentUrl) fixed("enrollmentUrl") } ?: enrollmentUrl
280
280
  val fixedRenewalUrl = nativeBlock?.renewalUrl?.also { if (renewalUrl != null && it != renewalUrl) fixed("renewalUrl") } ?: renewalUrl
281
+ // The device's identity goes to the enrollment and renewal addresses as well as to url:
282
+ // in a release build JS may give them only where the file names them (url itself must be
283
+ // in the file, SecurityPolicy.apply), like tokenHosts and clientCertHosts below.
284
+ if (release && nativeBlock != null) {
285
+ for ((key, given, declared) in listOf(
286
+ Triple("enrollmentUrl", enrollmentUrl, nativeBlock.enrollmentUrl),
287
+ Triple("renewalUrl", renewalUrl, nativeBlock.renewalUrl),
288
+ )) {
289
+ if (declared == null && given != null) {
290
+ throw BridgeInputException("${b.path}.$key: a release build takes it only from the app's native security file " +
291
+ "(${native?.source}); declare $key for Config API '$id' there")
292
+ }
293
+ }
294
+ }
281
295
  if (nativeBlock?.attestation == true && jsAttestation == false) {
282
296
  throw BridgeInputException("${b.path}.attestation: the app's native security file turns it on; JS cannot turn it off")
283
297
  }
@@ -48,11 +48,13 @@ import java.io.IOException
48
48
  * a different value is refused (`E_INVALID_CONFIG`);
49
49
  * - `allowUnsigned`, `allowUnpinnedConfigApi`, `allowServerGeneratedKey` from
50
50
  * JS are refused unless the file allows them for that block;
51
- * - `url`, `tokenHosts` and `clientCertHosts` of a block are fixed where the
52
- * file gives them, and `attestation: true` / `proofOfPossession: true` there
53
- * cannot be switched off; in a release build JS may give `tokenHosts` and
54
- * `clientCertHosts` only where the file names them (who gets the device's
55
- * token and identity is the file's call);
51
+ * - `url`, `enrollmentUrl`, `renewalUrl`, `tokenHosts` and `clientCertHosts`
52
+ * of a block are fixed where the file gives them, and `attestation: true` /
53
+ * `proofOfPossession: true` there cannot be switched off; in a release build
54
+ * a declared block must name its `url`, and JS may give `enrollmentUrl`,
55
+ * `renewalUrl`, `tokenHosts` and `clientCertHosts` only where the file names
56
+ * them (where the device's token and identity go is the file's call);
57
+ * - `ios.resolve` (top level) is the iOS side's; here it is read for the shape;
56
58
  * - `require` only tightens: a protection it turns on stays on whatever JS
57
59
  * says, `requireCaTrust` hosts are added to JS's, `expectedSignerSha256` is
58
60
  * fixed, and `expiredConfigGraceSeconds` is the most JS may ask for;
@@ -192,6 +194,11 @@ internal class NativeSecurity(
192
194
  )
193
195
  b.finish()
194
196
  }
197
+ // iOS-only (connection addresses, `ios.resolve`): read for the shape, used by the iOS side.
198
+ root.obj("ios")?.let { ios ->
199
+ ios.stringMap("resolve", 64, 255, 255)
200
+ ios.finish()
201
+ }
195
202
  val staticPins = root.obj("staticPins")?.let(ConfigParser::staticPins)
196
203
  val require = root.obj("require")?.let { r ->
197
204
  Require(
@@ -309,6 +316,10 @@ internal object SecurityPolicy {
309
316
  "declare $key for Config API '$id' there (or $relaxation)")
310
317
  if (block.bootstrapPins == null && !block.allowUnpinnedConfigApi) anchor("bootstrapPins", "allowUnpinnedConfigApi")
311
318
  if (block.signaturePublicKeys == null && !block.allowUnsigned) anchor("signaturePublicKeys", "allowUnsigned")
319
+ // Where the block talks to: the library hands it the device's token and identity.
320
+ if (block.url == null) throw BridgeInputException(
321
+ "$path.url: a release build takes it only from the app's native security file (${native.source}); " +
322
+ "declare url for Config API '$id' there")
312
323
  }
313
324
 
314
325
  val pins = block.bootstrapPins?.also { n ->
@@ -45,6 +45,11 @@ internal object PinnedFetch {
45
45
  /** The attestation headers: only the library sets them, for its token hosts (and a proof only it can sign). */
46
46
  val LIBRARY_HEADERS = listOf("PinVault-Token", "PinVault-Proof")
47
47
 
48
+ /** The HTTP client's own headers: the host the URL names, the connection and the body's framing. */
49
+ val CLIENT_HEADERS = listOf(
50
+ "Host", "Connection", "Keep-Alive", "Proxy-Connection", "TE", "Trailer", "Transfer-Encoding", "Upgrade", "Content-Length",
51
+ )
52
+
48
53
  fun parse(json: String): FetchRequest {
49
54
  // Base64 inflates by 4/3; leave room for the other fields.
50
55
  val f = StrictJson.parseObject(json, "request", maxChars = (MAX_REQUEST_BYTES * 4 / 3 + 64 * 1024).toInt())
@@ -59,6 +64,9 @@ internal object PinnedFetch {
59
64
  if (LIBRARY_HEADERS.any { it.equals(name, ignoreCase = true) }) {
60
65
  throw BridgeInputException("request.headers.$name: set by PinVault itself (a token host's requests carry it)")
61
66
  }
67
+ if (CLIENT_HEADERS.any { it.equals(name, ignoreCase = true) }) {
68
+ throw BridgeInputException("request.headers.$name: set by the HTTP client (host, connection and framing headers)")
69
+ }
62
70
  if (value.any { it == '\r' || it == '\n' || it.code == 0 }) {
63
71
  throw BridgeInputException("request.headers.$name: line breaks are not allowed")
64
72
  }
@@ -123,9 +123,8 @@ public enum ConfigParser {
123
123
  var maxRN = ReactNetworking.defaultMaxResponseBytes
124
124
  if let ios = try root.object("ios") {
125
125
  if let v = try ios.int64("reactNativeMaxResponseBytes", min: 1, max: ReactNetworking.maxResponseBytesLimit) { maxRN = v }
126
- if let resolve = try ios.stringMap("resolve", maxItems: 64, maxKeyLength: 255, maxValueLength: 255) {
127
- for (host, address) in resolve.sorted(by: { $0.key < $1.key }) { builder.resolve(host: host, to: address) }
128
- }
126
+ try applyResolve(try ios.stringMap("resolve", maxItems: 64, maxKeyLength: 255, maxValueLength: 255),
127
+ native: native, release: release, to: builder)
129
128
  if let v = try fixedList("config.ios.expectedBundleIds", require.expectedBundleIds,
130
129
  try ios.stringList("expectedBundleIds", maxItems: 16, maxLength: 255), native) {
131
130
  builder.expectedBundleId(v)
@@ -143,6 +142,7 @@ public enum ConfigParser {
143
142
  }
144
143
 
145
144
  else {
145
+ try applyResolve(nil, native: native, release: release, to: builder)
146
146
  if let v = require.expectedBundleIds { builder.expectedBundleId(v) }
147
147
  if let v = require.expectedTeamIds { builder.expectedTeamId(v) }
148
148
  if let v = require.userAuthStrength { builder.userAuthStrength(v) }
@@ -157,6 +157,29 @@ public enum ConfigParser {
157
157
  return parsed
158
158
  }
159
159
 
160
+ /// `config.ios.resolve` decides where a pinned host's TCP connection goes. The native
161
+ /// security file's map is the value (JS may leave it out or repeat it); a release build
162
+ /// takes it only from the file, so an OTA bundle cannot send the app's connections elsewhere.
163
+ static func applyResolve(_ given: [String: String]?, native: NativeSecurity?, release: Bool,
164
+ to builder: PinVaultConfig.Builder) throws {
165
+ let lowered: ([String: String]) -> [String: String] = { map in
166
+ Dictionary(map.map { ($0.key.lowercased(), $0.value) }, uniquingKeysWith: { first, _ in first })
167
+ }
168
+ var map = given
169
+ if let native {
170
+ if let fixed = native.resolve {
171
+ if let given, lowered(given) != lowered(fixed) {
172
+ throw BridgeInputError("config.ios.resolve: differs from the app's native security file (\(native.source)); the native value is fixed")
173
+ }
174
+ map = fixed
175
+ } else if release, given != nil {
176
+ throw BridgeInputError("config.ios.resolve: a release build takes it only from the app's native security file "
177
+ + "(\(native.source)); declare ios.resolve there")
178
+ }
179
+ }
180
+ for (host, address) in (map ?? [:]).sorted(by: { $0.key < $1.key }) { builder.resolve(host: host, to: address) }
181
+ }
182
+
160
183
  /// A JS list against the native file's: fixed where the file gives it.
161
184
  static func fixedList(_ path: String, _ native: [String]?, _ js: [String]?, _ file: NativeSecurity?) throws -> [String]? {
162
185
  guard let native else { return js }
@@ -317,6 +340,17 @@ public enum ConfigParser {
317
340
  if let j = renewalUrl, j != n { throw fixed("renewalUrl") }
318
341
  fixedRenewalUrl = n
319
342
  }
343
+ // The device's identity goes to the enrollment and renewal addresses as well as to url:
344
+ // in a release build JS may give them only where the file names them (url itself must be
345
+ // in the file, SecurityPolicy.apply), like tokenHosts and clientCertHosts.
346
+ if release, let nativeBlock {
347
+ for (key, given, declared) in [("enrollmentUrl", enrollmentUrl, nativeBlock.enrollmentUrl),
348
+ ("renewalUrl", renewalUrl, nativeBlock.renewalUrl)]
349
+ where declared == nil && given != nil {
350
+ throw BridgeInputError("\(b.path).\(key): a release build takes it only from the app's native security file "
351
+ + "(\(native?.source ?? "")); declare \(key) for Config API '\(id)' there")
352
+ }
353
+ }
320
354
  // Who gets the device's identity is the file's call in a release build: JS cannot
321
355
  // add a host the file does not name.
322
356
  if release, let nativeBlock, nativeBlock.clientCertHosts == nil, clientCertHosts != nil {
@@ -11,6 +11,7 @@
11
11
  // "clientCertHosts",
12
12
  // "enrollmentUrl", "renewalUrl" }],
13
13
  // "staticPins": { "pins": [...], "version": 1 },
14
+ // "ios": { "resolve": { "<host>": "<address>" } },
14
15
  // "require": { "requireUnlockedDevice", "requireHardwareBackedKeys",
15
16
  // "requirePinnedReactNativeNetworking", "managedTrustRoots",
16
17
  // "wipeVaultFilesOnRevocation", "requireCaTrust", "expectedBundleIds",
@@ -21,7 +22,11 @@
21
22
  // When the file is there: every JS Config API (and JS `staticPins`) must be
22
23
  // declared in it; a declared field is the value (JS may omit or repeat it, a
23
24
  // different value is refused); the three relaxations need the file's consent;
24
- // `attestation: true` and `proofOfPossession: true` cannot be switched off; `require` only tightens (its
25
+ // `attestation: true` and `proofOfPossession: true` cannot be switched off; in a
26
+ // release build a declared block names its `url`, and JS may give `enrollmentUrl`,
27
+ // `renewalUrl`, `tokenHosts`, `clientCertHosts` and `ios.resolve` only where the
28
+ // file names them (where the device's token, identity and connections go is the
29
+ // file's call); `require` only tightens (its
25
30
  // protections stay on, its `requireCaTrust` hosts are added to JS's, its
26
31
  // expected ids are fixed, its grace is the most JS may ask for); `vaultFiles`
27
32
  // fixes those files' signature key, encryption and lock, and a JS vault file
@@ -100,6 +105,8 @@ public struct NativeSecurity {
100
105
  var require = Require()
101
106
  /// Nil when the file has no `vaultFiles` section.
102
107
  var vaultFiles: [String: VaultFilePolicy]? = nil
108
+ /// `ios.resolve` (logical host → connection address); nil when the file has none.
109
+ var resolve: [String: String]? = nil
103
110
 
104
111
  /// True when the app's Info.plist accepts a release start without the file.
105
112
  public static func noFileAllowed(_ bundle: Bundle = .main) -> Bool {
@@ -147,6 +154,12 @@ public struct NativeSecurity {
147
154
  )
148
155
  try b.finish()
149
156
  }
157
+ // Where a pinned host's connection goes (`ios.resolve`): fixed where the file gives it.
158
+ var resolve: [String: String]? = nil
159
+ if let ios = try root.object("ios") {
160
+ resolve = try ios.stringMap("resolve", maxItems: 64, maxKeyLength: 255, maxValueLength: 255)
161
+ try ios.finish()
162
+ }
150
163
  let staticPins = try root.object("staticPins").map(ConfigParser.staticPins)
151
164
  var require = Require()
152
165
  if let r = try root.object("require") {
@@ -188,6 +201,7 @@ public struct NativeSecurity {
188
201
  var native = NativeSecurity(source: source, blocks: blocks, staticPins: staticPins)
189
202
  native.require = require
190
203
  native.vaultFiles = vaultFiles
204
+ native.resolve = resolve
191
205
  return native
192
206
  } catch let e as BridgeInputError {
193
207
  throw e.message.hasPrefix("native security file") ? e : BridgeInputError("native security file \(e.message)")
@@ -268,6 +282,11 @@ enum SecurityPolicy {
268
282
  }
269
283
  if block.bootstrapPins == nil && !block.allowUnpinnedConfigApi { throw anchor("bootstrapPins", "allowUnpinnedConfigApi") }
270
284
  if block.signaturePublicKeys == nil && !block.allowUnsigned { throw anchor("signaturePublicKeys", "allowUnsigned") }
285
+ // Where the block talks to: the library hands it the device's token and identity.
286
+ if block.url == nil {
287
+ throw BridgeInputError("\(path).url: a release build takes it only from the app's native security file (\(native.source)); "
288
+ + "declare url for Config API '\(id)' there")
289
+ }
271
290
  }
272
291
 
273
292
  var out = js
@@ -29,6 +29,9 @@ public enum PinnedFetch {
29
29
  static let tokenChars = CharacterSet(charactersIn: "!#$%&'*+.^_`|~0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz-")
30
30
  /// The attestation headers: only the library sets them, for its token hosts (and a proof only it can sign).
31
31
  static let libraryHeaders = ["PinVault-Token", "PinVault-Proof"]
32
+ /// The HTTP client's own headers: the host the URL names, the connection and the body's framing.
33
+ static let clientHeaders = ["Host", "Connection", "Keep-Alive", "Proxy-Connection", "TE", "Trailer",
34
+ "Transfer-Encoding", "Upgrade", "Content-Length"]
32
35
 
33
36
  public static func parse(_ json: String) throws -> FetchRequest {
34
37
  let f = try StrictJSON.parseObject(json, path: "request", maxChars: Int(maxRequestBytes * 4 / 3) + 64 * 1024)
@@ -47,6 +50,9 @@ public enum PinnedFetch {
47
50
  if libraryHeaders.contains(where: { $0.caseInsensitiveCompare(name) == .orderedSame }) {
48
51
  throw BridgeInputError("request.headers.\(name): set by PinVault itself (a token host's requests carry it)")
49
52
  }
53
+ if clientHeaders.contains(where: { $0.caseInsensitiveCompare(name) == .orderedSame }) {
54
+ throw BridgeInputError("request.headers.\(name): set by the HTTP client (host, connection and framing headers)")
55
+ }
50
56
  if value.unicodeScalars.contains(where: { $0 == "\r" || $0 == "\n" || $0.value == 0 }) {
51
57
  throw BridgeInputError("request.headers.\(name): line breaks are not allowed")
52
58
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@umutcansu/react-native-pinvault",
3
- "version": "2.7.0",
3
+ "version": "2.8.1",
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",