@capgo/inappbrowser 8.2.0 → 8.3.0-alpha.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.
- package/README.md +110 -28
- package/android/src/main/assets/proxy-bridge.js +197 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/InAppBrowserPlugin.java +29 -26
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/Options.java +5 -7
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/ProxyBridge.java +60 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewCallbacks.java +2 -0
- package/android/src/main/java/ee/forgr/capacitor_inappbrowser/WebViewDialog.java +262 -165
- package/dist/docs.json +150 -3
- package/dist/esm/definitions.d.ts +65 -3
- package/dist/esm/definitions.js.map +1 -1
- package/dist/esm/index.d.ts +20 -2
- package/dist/esm/index.js +79 -1
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/web.d.ts +1 -0
- package/dist/esm/web.js +4 -0
- package/dist/esm/web.js.map +1 -1
- package/dist/plugin.cjs.js +83 -0
- package/dist/plugin.cjs.js.map +1 -1
- package/dist/plugin.js +83 -0
- package/dist/plugin.js.map +1 -1
- package/ios/Sources/InAppBrowserPlugin/InAppBrowserPlugin.swift +64 -1
- package/ios/Sources/InAppBrowserPlugin/ProxySchemeHandler.swift +257 -0
- package/ios/Sources/InAppBrowserPlugin/WKWebView+SchemeHandling.swift +53 -0
- package/ios/Sources/InAppBrowserPlugin/WKWebViewController.swift +18 -1
- package/package.json +4 -2
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import WebKit
|
|
3
|
+
import Capacitor
|
|
4
|
+
|
|
5
|
+
public class ProxySchemeHandler: NSObject, WKURLSchemeHandler {
|
|
6
|
+
weak var plugin: InAppBrowserPlugin?
|
|
7
|
+
private var pendingTasks: [String: WKURLSchemeTask] = [:]
|
|
8
|
+
private var pendingBodies: [String: Data] = [:]
|
|
9
|
+
private var pendingNetworkTasks: [String: URLSessionDataTask] = [:]
|
|
10
|
+
private var stoppedRequests: Set<String> = []
|
|
11
|
+
private let taskLock = NSLock()
|
|
12
|
+
private let webviewId: String
|
|
13
|
+
private let proxyTimeoutSeconds: TimeInterval = 10
|
|
14
|
+
|
|
15
|
+
init(plugin: InAppBrowserPlugin, webviewId: String) {
|
|
16
|
+
self.plugin = plugin
|
|
17
|
+
self.webviewId = webviewId
|
|
18
|
+
super.init()
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public func webView(_ webView: WKWebView, start urlSchemeTask: WKURLSchemeTask) {
|
|
22
|
+
let request = urlSchemeTask.request
|
|
23
|
+
guard let url = request.url else {
|
|
24
|
+
urlSchemeTask.didFailWithError(NSError(
|
|
25
|
+
domain: "ProxySchemeHandler",
|
|
26
|
+
code: -1,
|
|
27
|
+
userInfo: [NSLocalizedDescriptionKey: "No URL in request"]
|
|
28
|
+
))
|
|
29
|
+
return
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let requestId = UUID().uuidString
|
|
33
|
+
|
|
34
|
+
taskLock.lock()
|
|
35
|
+
pendingTasks[requestId] = urlSchemeTask
|
|
36
|
+
taskLock.unlock()
|
|
37
|
+
|
|
38
|
+
// Encode body to base64, buffering stream data for later pass-through
|
|
39
|
+
var base64Body: String? = nil
|
|
40
|
+
if let bodyData = request.httpBody {
|
|
41
|
+
base64Body = bodyData.base64EncodedString()
|
|
42
|
+
} else if let bodyStream = request.httpBodyStream {
|
|
43
|
+
let data = Data(reading: bodyStream)
|
|
44
|
+
if !data.isEmpty {
|
|
45
|
+
base64Body = data.base64EncodedString()
|
|
46
|
+
taskLock.lock()
|
|
47
|
+
pendingBodies[requestId] = data
|
|
48
|
+
taskLock.unlock()
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Build headers dict
|
|
53
|
+
var headers: [String: String] = [:]
|
|
54
|
+
if let allHeaders = request.allHTTPHeaderFields {
|
|
55
|
+
headers = allHeaders
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
let eventData: [String: Any] = [
|
|
59
|
+
"requestId": requestId,
|
|
60
|
+
"url": url.absoluteString,
|
|
61
|
+
"method": request.httpMethod ?? "GET",
|
|
62
|
+
"headers": headers,
|
|
63
|
+
"body": base64Body as Any,
|
|
64
|
+
"webviewId": webviewId,
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
plugin?.notifyListeners("proxyRequest", data: eventData)
|
|
68
|
+
|
|
69
|
+
// Timeout: if JS never responds, fail the request after proxyTimeoutSeconds
|
|
70
|
+
DispatchQueue.global().asyncAfter(deadline: .now() + proxyTimeoutSeconds) { [weak self] in
|
|
71
|
+
guard let self = self else { return }
|
|
72
|
+
self.taskLock.lock()
|
|
73
|
+
guard let task = self.pendingTasks.removeValue(forKey: requestId) else {
|
|
74
|
+
self.taskLock.unlock()
|
|
75
|
+
return
|
|
76
|
+
}
|
|
77
|
+
self.pendingBodies.removeValue(forKey: requestId)
|
|
78
|
+
self.taskLock.unlock()
|
|
79
|
+
|
|
80
|
+
print("[InAppBrowser] Proxy request timed out after \(Int(self.proxyTimeoutSeconds))s: \(requestId)")
|
|
81
|
+
task.didFailWithError(NSError(
|
|
82
|
+
domain: "ProxySchemeHandler",
|
|
83
|
+
code: NSURLErrorTimedOut,
|
|
84
|
+
userInfo: [NSLocalizedDescriptionKey: "Proxy handler did not respond within \(Int(self.proxyTimeoutSeconds)) seconds"]
|
|
85
|
+
))
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public func webView(_ webView: WKWebView, stop urlSchemeTask: WKURLSchemeTask) {
|
|
90
|
+
taskLock.lock()
|
|
91
|
+
let requestIdToRemove = pendingTasks.first(where: { $0.value === urlSchemeTask })?.key
|
|
92
|
+
var networkTask: URLSessionDataTask?
|
|
93
|
+
if let key = requestIdToRemove {
|
|
94
|
+
pendingTasks.removeValue(forKey: key)
|
|
95
|
+
pendingBodies.removeValue(forKey: key)
|
|
96
|
+
networkTask = pendingNetworkTasks.removeValue(forKey: key)
|
|
97
|
+
stoppedRequests.insert(key)
|
|
98
|
+
}
|
|
99
|
+
taskLock.unlock()
|
|
100
|
+
networkTask?.cancel()
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/// Called from handleProxyRequest plugin method with the JS response
|
|
104
|
+
func handleResponse(requestId: String, responseData: [String: Any]?) {
|
|
105
|
+
taskLock.lock()
|
|
106
|
+
let isStopped = stoppedRequests.remove(requestId) != nil
|
|
107
|
+
guard let urlSchemeTask = pendingTasks[requestId] else {
|
|
108
|
+
pendingTasks.removeValue(forKey: requestId)
|
|
109
|
+
pendingBodies.removeValue(forKey: requestId)
|
|
110
|
+
taskLock.unlock()
|
|
111
|
+
return
|
|
112
|
+
}
|
|
113
|
+
let bufferedBody = pendingBodies.removeValue(forKey: requestId)
|
|
114
|
+
|
|
115
|
+
if isStopped {
|
|
116
|
+
pendingTasks.removeValue(forKey: requestId)
|
|
117
|
+
taskLock.unlock()
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if responseData != nil {
|
|
122
|
+
// JS provided a response — remove from pending now
|
|
123
|
+
pendingTasks.removeValue(forKey: requestId)
|
|
124
|
+
}
|
|
125
|
+
// For pass-through (nil), keep pendingTasks entry so stop can find it
|
|
126
|
+
taskLock.unlock()
|
|
127
|
+
|
|
128
|
+
if let responseData = responseData {
|
|
129
|
+
let statusCode = responseData["status"] as? Int ?? 200
|
|
130
|
+
let headersDict = responseData["headers"] as? [String: String] ?? [:]
|
|
131
|
+
let base64Body = responseData["body"] as? String ?? ""
|
|
132
|
+
|
|
133
|
+
let bodyData = Data(base64Encoded: base64Body) ?? Data()
|
|
134
|
+
|
|
135
|
+
guard let url = urlSchemeTask.request.url else {
|
|
136
|
+
urlSchemeTask.didFailWithError(NSError(
|
|
137
|
+
domain: "ProxySchemeHandler",
|
|
138
|
+
code: -1,
|
|
139
|
+
userInfo: [NSLocalizedDescriptionKey: "No URL"]
|
|
140
|
+
))
|
|
141
|
+
return
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
guard let httpResponse = HTTPURLResponse(
|
|
145
|
+
url: url,
|
|
146
|
+
statusCode: statusCode,
|
|
147
|
+
httpVersion: "HTTP/1.1",
|
|
148
|
+
headerFields: headersDict
|
|
149
|
+
) else {
|
|
150
|
+
urlSchemeTask.didFailWithError(NSError(
|
|
151
|
+
domain: "ProxySchemeHandler",
|
|
152
|
+
code: -2,
|
|
153
|
+
userInfo: [NSLocalizedDescriptionKey: "Failed to create response"]
|
|
154
|
+
))
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
urlSchemeTask.didReceive(httpResponse)
|
|
159
|
+
urlSchemeTask.didReceive(bodyData)
|
|
160
|
+
urlSchemeTask.didFinish()
|
|
161
|
+
} else {
|
|
162
|
+
// Null response = pass-through via URLSession
|
|
163
|
+
executePassThrough(requestId: requestId, urlSchemeTask: urlSchemeTask, bufferedBody: bufferedBody)
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
private func executePassThrough(requestId: String, urlSchemeTask: WKURLSchemeTask, bufferedBody: Data?) {
|
|
168
|
+
var request = urlSchemeTask.request
|
|
169
|
+
// Restore body if it was consumed from httpBodyStream during interception
|
|
170
|
+
if request.httpBody == nil, let body = bufferedBody {
|
|
171
|
+
request.httpBody = body
|
|
172
|
+
}
|
|
173
|
+
let session = URLSession.shared
|
|
174
|
+
let task = session.dataTask(with: request) { [weak self] data, response, error in
|
|
175
|
+
guard let self = self else { return }
|
|
176
|
+
|
|
177
|
+
// Clean up and check if this request was stopped while in-flight
|
|
178
|
+
self.taskLock.lock()
|
|
179
|
+
self.pendingTasks.removeValue(forKey: requestId)
|
|
180
|
+
self.pendingNetworkTasks.removeValue(forKey: requestId)
|
|
181
|
+
let wasStopped = self.stoppedRequests.remove(requestId) != nil
|
|
182
|
+
self.taskLock.unlock()
|
|
183
|
+
|
|
184
|
+
if wasStopped {
|
|
185
|
+
return
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
if let error = error {
|
|
189
|
+
if (error as NSError).code == NSURLErrorCancelled {
|
|
190
|
+
return
|
|
191
|
+
}
|
|
192
|
+
urlSchemeTask.didFailWithError(error)
|
|
193
|
+
return
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
guard let httpResponse = response as? HTTPURLResponse else {
|
|
197
|
+
urlSchemeTask.didFailWithError(NSError(
|
|
198
|
+
domain: "ProxySchemeHandler",
|
|
199
|
+
code: -3,
|
|
200
|
+
userInfo: [NSLocalizedDescriptionKey: "Non-HTTP response"]
|
|
201
|
+
))
|
|
202
|
+
return
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
urlSchemeTask.didReceive(httpResponse)
|
|
206
|
+
if let data = data {
|
|
207
|
+
urlSchemeTask.didReceive(data)
|
|
208
|
+
}
|
|
209
|
+
urlSchemeTask.didFinish()
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
taskLock.lock()
|
|
213
|
+
pendingNetworkTasks[requestId] = task
|
|
214
|
+
taskLock.unlock()
|
|
215
|
+
task.resume()
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
func cancelAllPendingTasks() {
|
|
219
|
+
taskLock.lock()
|
|
220
|
+
let tasks = pendingTasks
|
|
221
|
+
let networkTasks = pendingNetworkTasks
|
|
222
|
+
pendingTasks.removeAll()
|
|
223
|
+
pendingBodies.removeAll()
|
|
224
|
+
pendingNetworkTasks.removeAll()
|
|
225
|
+
stoppedRequests.removeAll()
|
|
226
|
+
taskLock.unlock()
|
|
227
|
+
|
|
228
|
+
for (_, networkTask) in networkTasks {
|
|
229
|
+
networkTask.cancel()
|
|
230
|
+
}
|
|
231
|
+
for (_, task) in tasks {
|
|
232
|
+
task.didFailWithError(NSError(
|
|
233
|
+
domain: "ProxySchemeHandler",
|
|
234
|
+
code: NSURLErrorCancelled,
|
|
235
|
+
userInfo: [NSLocalizedDescriptionKey: "WebView closed"]
|
|
236
|
+
))
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
extension Data {
|
|
242
|
+
init(reading input: InputStream) {
|
|
243
|
+
self.init()
|
|
244
|
+
input.open()
|
|
245
|
+
let bufferSize = 1024
|
|
246
|
+
let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: bufferSize)
|
|
247
|
+
while input.hasBytesAvailable {
|
|
248
|
+
let read = input.read(buffer, maxLength: bufferSize)
|
|
249
|
+
if read < 0 {
|
|
250
|
+
break
|
|
251
|
+
}
|
|
252
|
+
self.append(buffer, count: read)
|
|
253
|
+
}
|
|
254
|
+
buffer.deallocate()
|
|
255
|
+
input.close()
|
|
256
|
+
}
|
|
257
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import ObjectiveC
|
|
3
|
+
import WebKit
|
|
4
|
+
|
|
5
|
+
/// Enables registering a `WKURLSchemeHandler` for built-in schemes like `http` and `https`.
|
|
6
|
+
///
|
|
7
|
+
/// Apple's public `setURLSchemeHandler(_:forURLScheme:)` API throws for http/https because
|
|
8
|
+
/// `WKWebView.handlesURLScheme(_:)` returns `true` for them. This extension swizzles that
|
|
9
|
+
/// class method to return `false` for specified schemes, allowing the public API to work.
|
|
10
|
+
///
|
|
11
|
+
/// This is the industry-standard approach used by DuckDuckGo Browser, TON Proxy, and others.
|
|
12
|
+
extension WKWebView {
|
|
13
|
+
|
|
14
|
+
/// Tracks which schemes have been overridden to allow custom handling
|
|
15
|
+
private static var _overriddenSchemes = Set<String>()
|
|
16
|
+
|
|
17
|
+
/// One-time swizzle — matches the pattern used by DuckDuckGo and ton-proxy-swift
|
|
18
|
+
private static let _swizzleOnce: Void = {
|
|
19
|
+
let original = class_getClassMethod(WKWebView.self, #selector(WKWebView.handlesURLScheme(_:)))
|
|
20
|
+
let swizzled = class_getClassMethod(
|
|
21
|
+
WKWebView.self,
|
|
22
|
+
#selector(WKWebView._capgo_handlesURLScheme(_:))
|
|
23
|
+
)
|
|
24
|
+
|
|
25
|
+
guard let original, let swizzled else {
|
|
26
|
+
print("[InAppBrowser][Proxy] WARNING: Could not get methods for swizzle")
|
|
27
|
+
return
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
method_exchangeImplementations(original, swizzled)
|
|
31
|
+
}()
|
|
32
|
+
|
|
33
|
+
/// Swizzled replacement — returns false for overridden schemes, calls original for all others
|
|
34
|
+
@objc(capgo_handlesURLScheme:)
|
|
35
|
+
private static func _capgo_handlesURLScheme(_ urlScheme: String) -> Bool {
|
|
36
|
+
if _overriddenSchemes.contains(urlScheme.lowercased()) {
|
|
37
|
+
return false
|
|
38
|
+
}
|
|
39
|
+
// After swizzle, this actually calls the ORIGINAL handlesURLScheme
|
|
40
|
+
return _capgo_handlesURLScheme(urlScheme)
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/// Call this before `setURLSchemeHandler(_:forURLScheme:)` to allow registering for http/https.
|
|
44
|
+
static func enableCustomSchemeHandling(for schemes: [String]) {
|
|
45
|
+
// Add schemes BEFORE triggering the swizzle
|
|
46
|
+
for scheme in schemes {
|
|
47
|
+
_overriddenSchemes.insert(scheme.lowercased())
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Trigger the one-time swizzle
|
|
51
|
+
_ = _swizzleOnce
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -101,11 +101,13 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
101
101
|
self.initWebview(isInspectable: isInspectable)
|
|
102
102
|
}
|
|
103
103
|
|
|
104
|
-
public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool, enabledSafeTopMargin: Bool = true, blockedHosts: [String], authorizedAppLinks: [String]) {
|
|
104
|
+
public init(url: URL, headers: [String: String], isInspectable: Bool, credentials: WKWebViewCredentials? = nil, preventDeeplink: Bool, blankNavigationTab: Bool, enabledSafeBottomMargin: Bool, enabledSafeTopMargin: Bool = true, blockedHosts: [String], authorizedAppLinks: [String], proxyRequests: Bool = false, proxySchemeHandler: ProxySchemeHandler? = nil) {
|
|
105
105
|
super.init(nibName: nil, bundle: nil)
|
|
106
106
|
self.blankNavigationTab = blankNavigationTab
|
|
107
107
|
self.enabledSafeBottomMargin = enabledSafeBottomMargin
|
|
108
108
|
self.enabledSafeTopMargin = enabledSafeTopMargin
|
|
109
|
+
self.proxyRequests = proxyRequests
|
|
110
|
+
self.proxySchemeHandler = proxySchemeHandler
|
|
109
111
|
self.source = .remote(url)
|
|
110
112
|
self.credentials = credentials
|
|
111
113
|
self.setHeaders(headers: headers)
|
|
@@ -153,6 +155,8 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
153
155
|
var authorizedAppLinks: [String] = []
|
|
154
156
|
var activeNativeNavigationForWebview: Bool = true
|
|
155
157
|
var disableOverscroll: Bool = false
|
|
158
|
+
var proxyRequests: Bool = false
|
|
159
|
+
var proxySchemeHandler: ProxySchemeHandler?
|
|
156
160
|
|
|
157
161
|
// Dimension properties
|
|
158
162
|
var customWidth: CGFloat?
|
|
@@ -669,6 +673,19 @@ open class WKWebViewController: UIViewController, WKScriptMessageHandler {
|
|
|
669
673
|
// Enable background task processing
|
|
670
674
|
webConfiguration.processPool = WKProcessPool()
|
|
671
675
|
|
|
676
|
+
// Register proxy scheme handler if enabled
|
|
677
|
+
// Swizzles WKWebView.handlesURLScheme to allow registering for http/https via the public API
|
|
678
|
+
// This is the industry-standard approach used by DuckDuckGo Browser and others
|
|
679
|
+
if proxyRequests, let handler = proxySchemeHandler {
|
|
680
|
+
WKWebView.enableCustomSchemeHandling(for: ["https", "http"])
|
|
681
|
+
if !WKWebView.handlesURLScheme("https") && !WKWebView.handlesURLScheme("http") {
|
|
682
|
+
webConfiguration.setURLSchemeHandler(handler, forURLScheme: "https")
|
|
683
|
+
webConfiguration.setURLSchemeHandler(handler, forURLScheme: "http")
|
|
684
|
+
} else {
|
|
685
|
+
print("[InAppBrowser][Proxy] WARNING: handlesURLScheme swizzle failed; proxy scheme handler not registered")
|
|
686
|
+
}
|
|
687
|
+
}
|
|
688
|
+
|
|
672
689
|
// Enable JavaScript to run automatically (needed for preShowScript and Firebase polyfill)
|
|
673
690
|
webConfiguration.preferences.javaScriptCanOpenWindowsAutomatically = true
|
|
674
691
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@capgo/inappbrowser",
|
|
3
|
-
"version": "8.
|
|
3
|
+
"version": "8.3.0-alpha.0",
|
|
4
4
|
"description": "Capacitor plugin in app browser",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|
|
@@ -41,7 +41,8 @@
|
|
|
41
41
|
"prettier": "prettier-pretty-check \"**/*.{css,html,ts,js,java}\" --plugin=prettier-plugin-java",
|
|
42
42
|
"swiftlint": "node-swiftlint",
|
|
43
43
|
"docgen": "docgen --api InAppBrowserPlugin --output-readme README.md --output-json dist/docs.json",
|
|
44
|
-
"build": "
|
|
44
|
+
"build:proxy-bridge": "esbuild src/proxy-bridge.ts --bundle --outfile=android/src/main/assets/proxy-bridge.js --format=iife --target=es2015",
|
|
45
|
+
"build": "npm run clean && npm run docgen && tsc && rollup -c rollup.config.mjs && npm run build:proxy-bridge",
|
|
45
46
|
"clean": "rimraf ./dist",
|
|
46
47
|
"watch": "tsc --watch",
|
|
47
48
|
"prepublishOnly": "npm run build"
|
|
@@ -56,6 +57,7 @@
|
|
|
56
57
|
"@ionic/prettier-config": "^4.0.0",
|
|
57
58
|
"@ionic/swiftlint-config": "^2.0.0",
|
|
58
59
|
"@types/node": "^24.10.1",
|
|
60
|
+
"esbuild": "^0.27.3",
|
|
59
61
|
"eslint": "^8.57.1",
|
|
60
62
|
"eslint-plugin-import": "^2.31.0",
|
|
61
63
|
"husky": "^9.1.7",
|