@umutcansu/react-native-pinvault 0.0.0-stage → 2.4.0

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 (48) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/LICENSE +21 -0
  3. package/README.md +437 -2
  4. package/RNPinVault.podspec +43 -0
  5. package/android/build.gradle +68 -0
  6. package/android/consumer-rules.pro +25 -0
  7. package/android/src/main/AndroidManifest.xml +16 -0
  8. package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/ConfigParser.kt +396 -0
  9. package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/JsEnvironmentGuard.kt +53 -0
  10. package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/NativeSecurity.kt +354 -0
  11. package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultModule.kt +419 -0
  12. package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultNetworking.kt +379 -0
  13. package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinVaultPackage.kt +25 -0
  14. package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/PinnedFetch.kt +131 -0
  15. package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/ResultMapper.kt +188 -0
  16. package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/StrictJson.kt +225 -0
  17. package/android/src/main/java/io/github/umutcansu/pinvault/reactnative/VaultTokenStore.kt +63 -0
  18. package/ios/Core/ConfigParser.swift +437 -0
  19. package/ios/Core/JSEnvironmentGuard.swift +129 -0
  20. package/ios/Core/NativeSecurity.swift +303 -0
  21. package/ios/Core/PinnedFetch.swift +119 -0
  22. package/ios/Core/ReactNetworking.swift +172 -0
  23. package/ios/Core/ResultMapper.swift +163 -0
  24. package/ios/Core/StrictJSON.swift +214 -0
  25. package/ios/Core/VaultTokenStore.swift +66 -0
  26. package/ios/PinVaultBridge.swift +424 -0
  27. package/ios/PinVaultReactNetworking.swift +47 -0
  28. package/ios/RNPinVault.h +7 -0
  29. package/ios/RNPinVault.mm +293 -0
  30. package/ios/RNPinVaultURLRequestHandler.mm +92 -0
  31. package/lib/module/NativePinVault.js +13 -0
  32. package/lib/module/NativePinVault.js.map +1 -0
  33. package/lib/module/index.js +331 -0
  34. package/lib/module/index.js.map +1 -0
  35. package/lib/module/package.json +1 -0
  36. package/lib/module/types.js +2 -0
  37. package/lib/module/types.js.map +1 -0
  38. package/lib/typescript/package.json +1 -0
  39. package/lib/typescript/src/NativePinVault.d.ts +50 -0
  40. package/lib/typescript/src/NativePinVault.d.ts.map +1 -0
  41. package/lib/typescript/src/index.d.ts +150 -0
  42. package/lib/typescript/src/index.d.ts.map +1 -0
  43. package/lib/typescript/src/types.d.ts +359 -0
  44. package/lib/typescript/src/types.d.ts.map +1 -0
  45. package/package.json +128 -4
  46. package/src/NativePinVault.ts +72 -0
  47. package/src/index.ts +398 -0
  48. package/src/types.ts +362 -0
@@ -0,0 +1,424 @@
1
+ // The Swift side of the TurboModule (RNPinVault.mm forwards every method here).
2
+ // It calls PinVault.shared and nothing else: no pinning, key handling or vault
3
+ // decryption happens in this file. Inputs are parsed by Core/ConfigParser and
4
+ // Core/PinnedFetch, results mapped by Core/ResultMapper.
5
+ import Foundation
6
+ import PinVault
7
+ #if canImport(UIKit)
8
+ import UIKit
9
+ #endif
10
+
11
+ @objc(RNPinVaultBridge)
12
+ public final class PinVaultBridge: NSObject {
13
+ public typealias Resolve = (Any?) -> Void
14
+ public typealias Reject = (String, String, [String: Any]?) -> Void
15
+
16
+ // Process-wide, like the library: a JS reload makes a new module, not a new PinVault.
17
+ private static let tokens = VaultTokenStore()
18
+ private static let lock = NSLock()
19
+ private static var startedInProcess = false
20
+ private static var jsGuard: JSEnvironmentGuard?
21
+ private static weak var current: PinVaultBridge?
22
+ private static let startQueue = StartQueue()
23
+ private static var rnMaxResponseBytes = ReactNetworking.defaultMaxResponseBytes
24
+
25
+ private let mapper = ResultMapper(tokens: PinVaultBridge.tokens)
26
+
27
+ @objc public var emitConnectionEvent: (([String: Any]) -> Void)?
28
+ @objc public var emitGuardRequest: (([String: Any]) -> Void)?
29
+ /// RNPinVault.mm: whether RCTNetworking hands an https request to
30
+ /// RNPinVaultURLRequestHandler (true), to another handler (false), or nil = unknown.
31
+ @objc public var reactNetworkingIsPinned: (() -> NSNumber?)?
32
+
33
+ /// `PinVault.shared.registerBackgroundTask()` for the app's AppDelegate (before launch ends):
34
+ /// the app target does not link the PinVault package itself.
35
+ @objc public static func registerBackgroundTask() -> Bool {
36
+ PinVault.shared.registerBackgroundTask()
37
+ }
38
+
39
+ @objc public override init() {
40
+ super.init()
41
+ Self.lock.lock()
42
+ Self.current = self
43
+ Self.lock.unlock()
44
+ }
45
+
46
+ private static func locked<T>(_ body: () -> T) -> T {
47
+ lock.lock(); defer { lock.unlock() }
48
+ return body()
49
+ }
50
+
51
+ private static var started: Bool {
52
+ lock.lock(); defer { lock.unlock() }
53
+ return startedInProcess
54
+ }
55
+
56
+ /// For the React Native request handler: `start` has run (a failed start
57
+ /// leaves the library's sessions refusing every request).
58
+ static var isStarted: Bool { started }
59
+
60
+ /// `ios.reactNativeMaxResponseBytes` of the last start.
61
+ static var reactNativeMaxResponseBytes: Int64 { locked { rnMaxResponseBytes } }
62
+
63
+ /// An error → (exception name, message) for JS, tokens redacted.
64
+ static let describe: @Sendable (any Error) -> (String, String) = { error in
65
+ let mapped = ResultMapper(tokens: tokens).exception(error) as? [String: Any] ?? [:]
66
+ return ((mapped["name"] as? String) ?? "Error", (mapped["message"] as? String) ?? "")
67
+ }
68
+
69
+ /// Runs a library call with the JS guard's verdicts on `operations` asked
70
+ /// beforehand, so the library's synchronous guard check holds no thread.
71
+ private static func guarded<T>(_ operations: [GuardedOperation], _ body: () async throws -> T) async rethrows -> T {
72
+ guard let g = locked({ jsGuard }) else { return try await body() }
73
+ return try await g.withVerdicts(operations, body)
74
+ }
75
+
76
+ // MARK: helpers
77
+
78
+ private func run(_ resolve: @escaping Resolve, _ reject: @escaping Reject, _ body: @escaping () async throws -> Any?) {
79
+ Task.detached {
80
+ do {
81
+ resolve(try await body() ?? NSNull())
82
+ } catch let e as BridgeInputError {
83
+ reject("E_INVALID_ARGUMENT", Self.tokens.redact(e.message) ?? "", nil)
84
+ } catch {
85
+ reject("E_NATIVE", Self.tokens.redact("\(error)") ?? "", self.exceptionInfo(error))
86
+ }
87
+ }
88
+ }
89
+
90
+ private func exceptionInfo(_ error: any Error) -> [String: Any] {
91
+ let mapped = mapper.exception(error) as? [String: Any] ?? [:]
92
+ return ["exceptionName": mapped["name"] ?? "Error", "exceptionMessage": mapped["message"] ?? NSNull()]
93
+ }
94
+
95
+ /// A vault file key: the library's rule (it names the stored copy's file).
96
+ private static func checkKey(_ key: String) throws {
97
+ let allowed = key.utf8.allSatisfy { byte in
98
+ (byte >= 0x30 && byte <= 0x39) || (byte >= 0x41 && byte <= 0x5A) || (byte >= 0x61 && byte <= 0x7A) ||
99
+ byte == 0x2E || byte == 0x5F || byte == 0x2D
100
+ }
101
+ if key.isEmpty || key.utf8.count > 64 || !allowed || key.allSatisfy({ $0 == "." }) {
102
+ throw BridgeInputError("key: must match [A-Za-z0-9._-]{1,64} and not be only dots")
103
+ }
104
+ }
105
+
106
+ private static func checkToken(_ token: String?) throws {
107
+ if let token, token.count > 4096 { throw BridgeInputError("token: longer than 4096 characters") }
108
+ }
109
+
110
+ // MARK: start / config
111
+
112
+ @objc public func start(_ json: String, resolve: @escaping Resolve, reject: @escaping Reject) {
113
+ Task.detached {
114
+ await Self.startQueue.run {
115
+ let parsed: ParsedConfig
116
+ var newGuard: JSEnvironmentGuard?
117
+ do {
118
+ #if DEBUG
119
+ let release = false
120
+ #else
121
+ let release = true
122
+ #endif
123
+ parsed = try ConfigParser.parse(
124
+ json, tokens: Self.tokens,
125
+ guardFactory: { timeout in
126
+ let g = JSEnvironmentGuard(timeoutMs: timeout) { requestId, operation in
127
+ let bridge = Self.locked { Self.current }
128
+ bridge?.emitGuardRequest?(["requestId": requestId, "operation": operation])
129
+ }
130
+ newGuard = g
131
+ return g
132
+ },
133
+ listener: { event in
134
+ let bridge = Self.locked { Self.current }
135
+ guard let bridge else { return }
136
+ bridge.emitConnectionEvent?(bridge.mapper.event(event))
137
+ },
138
+ native: try NativeSecurity.load(),
139
+ release: release,
140
+ noFileAllowed: NativeSecurity.noFileAllowed(),
141
+ reactNetworkingEnabled: PinVaultReactNetworking.enabled
142
+ )
143
+ } catch let e as BridgeInputError {
144
+ // The guard of the running config stays: a refused config changes nothing.
145
+ reject("E_INVALID_CONFIG", Self.tokens.redact(e.message) ?? "", nil)
146
+ return
147
+ } catch {
148
+ let message = (error as? PinVaultError)?.message ?? "\(error)"
149
+ reject("E_INVALID_CONFIG", Self.tokens.redact(message) ?? "", nil)
150
+ return
151
+ }
152
+ let pinned = self.reactNetworkingIsPinned?()?.boolValue
153
+ if pinned != true {
154
+ let why = !PinVaultReactNetworking.enabled
155
+ ? "Info.plist PinVaultPinReactNativeNetworking is NO"
156
+ : pinned == nil ? "RCTNetworking's handler could not be checked"
157
+ : "RCTNetworking hands https to another RCTURLRequestHandler"
158
+ NSLog("PinVault: React Native's own fetch / XMLHttpRequest / images may NOT be pinned (\(why)); README, \"Networking\"")
159
+ if parsed.requirePinnedReactNativeNetworking {
160
+ reject("E_NETWORKING_NOT_PINNED", "requirePinnedReactNativeNetworking: \(why)", nil)
161
+ return
162
+ }
163
+ }
164
+ Self.locked {
165
+ Self.jsGuard = newGuard
166
+ Self.rnMaxResponseBytes = parsed.reactNativeMaxResponseBytes
167
+ }
168
+ // A second start applies the new config: the library keeps the
169
+ // first one otherwise (the samples restart the same way).
170
+ if Self.started { PinVault.shared.reset() }
171
+ // INIT, and ENROLL for a pending enrollment the start picks up.
172
+ let result = await Self.guarded([.start, .enroll]) { await PinVault.shared.start(config: parsed.config) }
173
+ Self.locked { Self.startedInProcess = true }
174
+ // Says whether the anchors came from the native security file (README "Native security file").
175
+ var answer = self.mapper.initResult(result)
176
+ answer["nativeSecurityApplied"] = parsed.nativeSecurityApplied
177
+ resolve(answer)
178
+ }
179
+ }
180
+ }
181
+
182
+ @objc public func updateNow(resolve: @escaping Resolve, reject: @escaping Reject) {
183
+ run(resolve, reject) { self.mapper.update(await PinVault.shared.updateNow()) }
184
+ }
185
+
186
+ @objc public func currentVersion(resolve: @escaping Resolve, reject: @escaping Reject) {
187
+ run(resolve, reject) { PinVault.shared.currentVersion() }
188
+ }
189
+
190
+ @objc public func hostPinVersions(resolve: @escaping Resolve, reject: @escaping Reject) {
191
+ run(resolve, reject) { PinVault.shared.hostPinVersions() }
192
+ }
193
+
194
+ @objc public func pinsForHost(_ hostname: String, resolve: @escaping Resolve, reject: @escaping Reject) {
195
+ run(resolve, reject) { ["pins": PinVault.shared.pinsForHost(hostname) ?? NSNull()] as [String: Any] }
196
+ }
197
+
198
+ @objc public func signingStatus(_ configApiId: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
199
+ run(resolve, reject) { PinVault.shared.signingStatus(configApiId: configApiId).map(self.mapper.signing) }
200
+ }
201
+
202
+ @objc public func isForceUpdate(resolve: @escaping Resolve, reject: @escaping Reject) {
203
+ run(resolve, reject) { PinVault.shared.isForceUpdate() }
204
+ }
205
+
206
+ @objc public func reset(resolve: @escaping Resolve, reject: @escaping Reject) {
207
+ run(resolve, reject) { PinVault.shared.reset(); return nil }
208
+ }
209
+
210
+ @objc public func schedulePeriodicUpdates(_ intervalHours: NSNumber?, resolve: @escaping Resolve, reject: @escaping Reject) {
211
+ run(resolve, reject) {
212
+ guard Self.started else { return false }
213
+ if let h = intervalHours?.int64Value, h < 1 || h > 24 * 30 {
214
+ throw BridgeInputError("intervalHours: must be between 1 and 720")
215
+ }
216
+ return PinVault.shared.schedulePeriodicUpdates(intervalHours: intervalHours?.int64Value)
217
+ }
218
+ }
219
+
220
+ @objc public func cancelPeriodicUpdates(resolve: @escaping Resolve, reject: @escaping Reject) {
221
+ run(resolve, reject) { PinVault.shared.cancelPeriodicUpdates(); return nil }
222
+ }
223
+
224
+ @objc public func enableDebugLogging(resolve: @escaping Resolve, reject: @escaping Reject) {
225
+ run(resolve, reject) {
226
+ #if DEBUG
227
+ PinVault.enableDebugLogging()
228
+ return true
229
+ #else
230
+ return false
231
+ #endif
232
+ }
233
+ }
234
+
235
+ // MARK: pinned HTTP
236
+
237
+ @objc public func fetch(_ json: String, resolve: @escaping Resolve, reject: @escaping Reject) {
238
+ Task.detached {
239
+ let request: FetchRequest
240
+ do {
241
+ request = try PinnedFetch.parse(json)
242
+ } catch let e as BridgeInputError {
243
+ reject("E_INVALID_ARGUMENT", Self.tokens.redact(e.message) ?? "", nil)
244
+ return
245
+ } catch {
246
+ reject("E_INVALID_ARGUMENT", Self.tokens.redact("\(error)") ?? "", nil)
247
+ return
248
+ }
249
+ guard Self.started else {
250
+ reject("E_NOT_STARTED", "PinVault has not started: call start() first", nil)
251
+ return
252
+ }
253
+ let session = request.settings.map { PinVault.shared.session(settings: $0) } ?? PinVault.shared.session()
254
+ do {
255
+ resolve(try await PinnedFetch.execute(session, request))
256
+ } catch {
257
+ let info = self.exceptionInfo(error)
258
+ let name = info["exceptionName"] as? String ?? "Error"
259
+ let message = info["exceptionMessage"] as? String ?? ""
260
+ reject("E_FETCH", "\(name): \(message)", info)
261
+ }
262
+ }
263
+ }
264
+
265
+ // MARK: enrollment
266
+
267
+ @objc public func deviceId(resolve: @escaping Resolve, reject: @escaping Reject) {
268
+ run(resolve, reject) {
269
+ #if canImport(UIKit)
270
+ return await MainActor.run { UIDevice.current.identifierForVendor?.uuidString.lowercased() }
271
+ #else
272
+ return nil
273
+ #endif
274
+ }
275
+ }
276
+
277
+ @objc public func enrollForResult(_ token: String, label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
278
+ run(resolve, reject) {
279
+ if token.isEmpty || token.count > 4096 { throw BridgeInputError("token: must be 1 to 4096 characters") }
280
+ return await Self.tokens.withTransientSecret(token) {
281
+ self.mapper.enrollment(await Self.guarded([.enroll]) { await PinVault.shared.enrollForResult(token: token, label: label) })
282
+ }
283
+ }
284
+ }
285
+
286
+ @objc public func autoEnrollForResult(resolve: @escaping Resolve, reject: @escaping Reject) {
287
+ run(resolve, reject) { self.mapper.enrollment(await Self.guarded([.enroll]) { await PinVault.shared.autoEnrollForResult() }) }
288
+ }
289
+
290
+ @objc public func checkPendingEnrollment(resolve: @escaping Resolve, reject: @escaping Reject) {
291
+ run(resolve, reject) { self.mapper.enrollment(await Self.guarded([.enroll]) { await PinVault.shared.checkPendingEnrollment() }) }
292
+ }
293
+
294
+ @objc public func isEnrolled(_ label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
295
+ run(resolve, reject) { PinVault.shared.isEnrolled(label: label) }
296
+ }
297
+
298
+ @objc public func isEnrollmentPending(_ label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
299
+ run(resolve, reject) { PinVault.shared.isEnrollmentPending(label: label) }
300
+ }
301
+
302
+ @objc public func enrollmentVerificationCode(_ label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
303
+ run(resolve, reject) { PinVault.shared.enrollmentVerificationCode(label: label) }
304
+ }
305
+
306
+ @objc public func enrolledClientCN(_ label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
307
+ run(resolve, reject) { PinVault.shared.enrolledClientCN(label: label) }
308
+ }
309
+
310
+ @objc public func enrolledClientNotAfter(_ label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
311
+ run(resolve, reject) { PinVault.shared.enrolledClientNotAfter(label: label) }
312
+ }
313
+
314
+ @objc public func unenroll(_ label: String?, wipeVaultFiles: Bool, resolve: @escaping Resolve, reject: @escaping Reject) {
315
+ run(resolve, reject) { PinVault.shared.unenroll(label: label, wipeVaultFiles: wipeVaultFiles); return nil }
316
+ }
317
+
318
+ @objc public func identityKeySecurityLevel(_ label: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
319
+ run(resolve, reject) { PinVault.shared.identityKeySecurityLevel(label: label)?.rawValue }
320
+ }
321
+
322
+ // MARK: vault files
323
+
324
+ @objc public func setVaultToken(_ key: String, token: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
325
+ run(resolve, reject) {
326
+ try Self.checkKey(key)
327
+ try Self.checkToken(token)
328
+ Self.tokens.put(key, token)
329
+ return nil
330
+ }
331
+ }
332
+
333
+ @objc public func clearVaultTokens(resolve: @escaping Resolve, reject: @escaping Reject) {
334
+ run(resolve, reject) { Self.tokens.clear() }
335
+ }
336
+
337
+ @objc public func fetchFile(_ key: String, token: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
338
+ run(resolve, reject) {
339
+ try Self.checkKey(key)
340
+ try Self.checkToken(token)
341
+ if let token { Self.tokens.put(key, token) }
342
+ return self.mapper.vaultFile(await Self.guarded([.fetchFile]) { await PinVault.shared.fetchFile(key) })
343
+ }
344
+ }
345
+
346
+ @objc public func loadFile(_ key: String, encoding: String, resolve: @escaping Resolve, reject: @escaping Reject) {
347
+ run(resolve, reject) {
348
+ try Self.checkKey(key)
349
+ let enc = try ResultMapper.checkEncoding(encoding)
350
+ return await Self.guarded([.loadFile]) { PinVault.shared.loadFile(key) }.map { ResultMapper.encode($0, enc) }
351
+ }
352
+ }
353
+
354
+ @objc public func fileStatus(_ key: String, resolve: @escaping Resolve, reject: @escaping Reject) {
355
+ run(resolve, reject) { try Self.checkKey(key); return PinVault.shared.fileStatus(key).rawValue }
356
+ }
357
+
358
+ @objc public func unlockFile(_ key: String, prompt json: String, resolve: @escaping Resolve, reject: @escaping Reject) {
359
+ run(resolve, reject) {
360
+ try Self.checkKey(key)
361
+ let (prompt, encoding) = try ConfigParser.unlockPrompt(json)
362
+ // LAContext's Face ID / passcode prompt is the library's.
363
+ return self.mapper.unlock(
364
+ await Self.guarded([.unlockFile]) { await PinVault.shared.unlockFile(key: key, prompt: prompt) }, encoding: encoding
365
+ )
366
+ }
367
+ }
368
+
369
+ @objc public func isFileLocked(_ key: String, resolve: @escaping Resolve, reject: @escaping Reject) {
370
+ run(resolve, reject) { try Self.checkKey(key); return PinVault.shared.isFileLocked(key) }
371
+ }
372
+
373
+ @objc public func hasFile(_ key: String, resolve: @escaping Resolve, reject: @escaping Reject) {
374
+ run(resolve, reject) { try Self.checkKey(key); return PinVault.shared.hasFile(key) }
375
+ }
376
+
377
+ @objc public func fileVersion(_ key: String, resolve: @escaping Resolve, reject: @escaping Reject) {
378
+ run(resolve, reject) { try Self.checkKey(key); return PinVault.shared.fileVersion(key) }
379
+ }
380
+
381
+ @objc public func clearFile(_ key: String, resolve: @escaping Resolve, reject: @escaping Reject) {
382
+ run(resolve, reject) { try Self.checkKey(key); PinVault.shared.clearFile(key); return nil }
383
+ }
384
+
385
+ @objc public func syncAllFiles(resolve: @escaping Resolve, reject: @escaping Reject) {
386
+ run(resolve, reject) { (await Self.guarded([.fetchFile]) { await PinVault.shared.syncAllFiles() }).mapValues(self.mapper.vaultFile) }
387
+ }
388
+
389
+ // MARK: attestation
390
+
391
+ @objc public func attestNow(_ configApiId: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
392
+ run(resolve, reject) { self.mapper.attestation(await PinVault.shared.attestNow(configApiId: configApiId)) }
393
+ }
394
+
395
+ @objc public func fetchAttestationToken(_ host: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
396
+ run(resolve, reject) { self.mapper.attestationToken(await PinVault.shared.fetchAttestationToken(host: host)) }
397
+ }
398
+
399
+ @objc public func attestationStatus(_ configApiId: String?, resolve: @escaping Resolve, reject: @escaping Reject) {
400
+ run(resolve, reject) { self.mapper.attestation(PinVault.shared.attestationStatus(configApiId: configApiId)) }
401
+ }
402
+
403
+ // MARK: environmentGuard
404
+
405
+ @objc public func answerGuard(_ requestId: String, allowed: Bool) {
406
+ let g = Self.locked { Self.jsGuard }
407
+ g?.answer(requestId, allowed: allowed)
408
+ }
409
+ }
410
+
411
+ /// Serialises `start` calls (a second start resets and starts again).
412
+ private actor StartQueue {
413
+ private var tail: Task<Void, Never>?
414
+
415
+ func run(_ body: @escaping @Sendable () async -> Void) async {
416
+ let previous = tail
417
+ let task = Task {
418
+ await previous?.value
419
+ await body()
420
+ }
421
+ tail = task
422
+ await task.value
423
+ }
424
+ }
@@ -0,0 +1,47 @@
1
+ // The Swift side of RNPinVaultURLRequestHandler.mm: React Native's https
2
+ // requests (fetch, XMLHttpRequest, <Image>) through PinVault's pinned session.
3
+ // The rules live in Core/ReactNetworking.swift.
4
+ import Foundation
5
+ import PinVault
6
+
7
+ @objc(RNPinVaultReactNetworking)
8
+ public final class PinVaultReactNetworking: NSObject {
9
+
10
+ /// Info.plist `PinVaultPinReactNativeNetworking` = NO hands https back to
11
+ /// React Native's own (unpinned) handler — a native opt-out, which a JS
12
+ /// bundle cannot flip. Default: on.
13
+ @objc public static let enabled: Bool =
14
+ (Bundle.main.object(forInfoDictionaryKey: "PinVaultPinReactNativeNetworking") as? Bool) ?? true
15
+
16
+ @objc public static func canHandle(_ request: URLRequest) -> Bool {
17
+ enabled && ReactNetworking.handles(request)
18
+ }
19
+
20
+ /// The request token for RN; nothing reaches the blocks before `resume()`.
21
+ @objc public static func prepare(
22
+ _ request: URLRequest,
23
+ didSendData: @escaping (Int64) -> Void,
24
+ didReceiveResponse: @escaping (URLResponse) -> Void,
25
+ didReceiveData: @escaping (Data) -> Void,
26
+ didComplete: @escaping (NSError?) -> Void
27
+ ) -> ReactNetworkingTask {
28
+ var send: ReactNetworking.Send?
29
+ if PinVaultBridge.isStarted {
30
+ send = { (request: URLRequest, limit: Int64) async throws -> (Data, URLResponse) in
31
+ try await PinVault.shared.session().data(for: request, maxResponseBytes: limit)
32
+ }
33
+ }
34
+ return ReactNetworking.prepare(
35
+ request,
36
+ send: send,
37
+ maxResponseBytes: PinVaultBridge.reactNativeMaxResponseBytes,
38
+ describe: PinVaultBridge.describe,
39
+ callbacks: ReactNetworking.Callbacks(
40
+ didSendData: didSendData,
41
+ didReceiveResponse: didReceiveResponse,
42
+ didReceiveData: didReceiveData,
43
+ didComplete: { didComplete($0.map { $0 as NSError }) }
44
+ )
45
+ )
46
+ }
47
+ }
@@ -0,0 +1,7 @@
1
+ // RNPinVault — the ObjC++ shell of the TurboModule. It conforms to the
2
+ // codegen protocol and forwards every call to the Swift bridge
3
+ // (PinVaultBridge.swift), which calls PinVault.shared.
4
+ #import <RNPinVaultSpec/RNPinVaultSpec.h>
5
+
6
+ @interface RNPinVault : NativePinVaultSpecBase <NativePinVaultSpec>
7
+ @end