@cappitolian/http-local-server-swifter 0.0.23 → 0.0.24
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.
|
@@ -8,12 +8,11 @@ import UIKit
|
|
|
8
8
|
/// It only blocks mDNS/Bonjour service discovery. This means Swifter's
|
|
9
9
|
/// server.start() will always succeed regardless of permission status.
|
|
10
10
|
///
|
|
11
|
-
/// Detection strategy — NWListener with
|
|
12
|
-
///
|
|
13
|
-
/// NWListener
|
|
14
|
-
///
|
|
15
|
-
///
|
|
16
|
-
/// The service type MUST match NSBonjourServices in Info.plist exactly.
|
|
11
|
+
/// Detection strategy — NWListener with serviceRegistrationUpdateHandler:
|
|
12
|
+
/// The PolicyDenied error for a revoked Local Network permission surfaces
|
|
13
|
+
/// in NWListener.serviceRegistrationUpdateHandler, not in stateUpdateHandler.
|
|
14
|
+
/// A connection handler must also be set or NWListener fails with error 22
|
|
15
|
+
/// before iOS even evaluates the permission.
|
|
17
16
|
@objc public class LocalNetworkPermission: NSObject {
|
|
18
17
|
|
|
19
18
|
// MARK: - Types
|
|
@@ -37,14 +36,11 @@ import UIKit
|
|
|
37
36
|
|
|
38
37
|
/// Checks Local Network permission by advertising a Bonjour service via NWListener.
|
|
39
38
|
///
|
|
40
|
-
///
|
|
41
|
-
///
|
|
39
|
+
/// The permission denial for a revoked permission surfaces in
|
|
40
|
+
/// serviceRegistrationUpdateHandler — not in stateUpdateHandler.
|
|
41
|
+
/// A dummy connection handler is required to prevent error 22 (Invalid argument).
|
|
42
42
|
///
|
|
43
43
|
/// - Parameter completion: Called on the main thread with the permission status.
|
|
44
|
-
/// `.granted` / `.denied` once iOS responds, or `.unknown` on timeout.
|
|
45
|
-
///
|
|
46
|
-
/// - Note: iOS shows the system dialog only on the first call. Subsequent calls
|
|
47
|
-
/// return the cached decision immediately without prompting the user.
|
|
48
44
|
public func checkPermission(completion: @escaping (PermissionStatus) -> Void) {
|
|
49
45
|
var completed = false
|
|
50
46
|
|
|
@@ -59,61 +55,69 @@ import UIKit
|
|
|
59
55
|
}
|
|
60
56
|
}
|
|
61
57
|
|
|
62
|
-
// Timeout fallback — allow startup if iOS never calls the state handler.
|
|
63
|
-
// Covers: simulator, and rare edge cases where the OS skips the callback.
|
|
64
58
|
let timeout = DispatchWorkItem {
|
|
65
59
|
print("⚠️ LocalNetworkPermission: timeout — resolving as .unknown")
|
|
66
60
|
finish(.unknown)
|
|
67
61
|
}
|
|
68
62
|
DispatchQueue.main.asyncAfter(deadline: .now() + 5.0, execute: timeout)
|
|
69
63
|
|
|
70
|
-
// Create a TCP listener and advertise it as a Bonjour service.
|
|
71
|
-
// iOS evaluates the Local Network permission at advertisement time,
|
|
72
|
-
// not at socket bind time — which is why server.start() always succeeds.
|
|
73
64
|
let params = NWParameters.tcp
|
|
74
65
|
params.includePeerToPeer = true
|
|
75
66
|
|
|
76
67
|
guard let listener = try? NWListener(using: params) else {
|
|
77
|
-
print("⚠️ LocalNetworkPermission: could not create NWListener
|
|
68
|
+
print("⚠️ LocalNetworkPermission: could not create NWListener")
|
|
78
69
|
timeout.cancel()
|
|
79
70
|
finish(.unknown)
|
|
80
71
|
return
|
|
81
72
|
}
|
|
82
73
|
|
|
83
|
-
// Advertising this service type triggers the system dialog on first launch
|
|
84
|
-
// and also immediately returns PolicyDenied when permission is revoked.
|
|
85
74
|
listener.service = NWListener.Service(type: Self.bonjourServiceType)
|
|
86
75
|
self.listener = listener
|
|
87
76
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
77
|
+
// Required: NWListener fails with error 22 (Invalid argument) if no
|
|
78
|
+
// connection handler is set. This dummy handler satisfies the requirement.
|
|
79
|
+
listener.newConnectionHandler = { connection in
|
|
80
|
+
connection.cancel()
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// This is where iOS reports PolicyDenied for a revoked Local Network permission.
|
|
84
|
+
// stateUpdateHandler only reports generic listener errors, not Bonjour policy errors.
|
|
85
|
+
listener.serviceRegistrationUpdateHandler = { change in
|
|
86
|
+
print("🔍 LocalNetworkPermission serviceRegistration: \(change)")
|
|
87
|
+
switch change {
|
|
88
|
+
case .add:
|
|
89
|
+
// Service registered successfully — permission is granted.
|
|
93
90
|
timeout.cancel()
|
|
94
91
|
finish(.granted)
|
|
95
92
|
|
|
93
|
+
case .remove:
|
|
94
|
+
// Service was removed. On permission denial this fires immediately
|
|
95
|
+
// after the _NWAdvertiser PolicyDenied error in the system logs.
|
|
96
|
+
// We treat an immediate remove (before .add) as denied.
|
|
97
|
+
timeout.cancel()
|
|
98
|
+
finish(.denied)
|
|
99
|
+
|
|
100
|
+
@unknown default:
|
|
101
|
+
break
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// stateUpdateHandler still needed to catch non-permission listener failures.
|
|
106
|
+
listener.stateUpdateHandler = { state in
|
|
107
|
+
print("🔍 LocalNetworkPermission NWListener state: \(state)")
|
|
108
|
+
switch state {
|
|
96
109
|
case .failed(let error):
|
|
97
110
|
let code = (error as NSError).code
|
|
98
|
-
print("🔍 LocalNetworkPermission NWListener error
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
timeout.cancel()
|
|
103
|
-
finish(.denied)
|
|
104
|
-
} else {
|
|
105
|
-
// Port conflict or other non-permission error — treat as unknown
|
|
106
|
-
// and allow the server to start rather than blocking the user.
|
|
107
|
-
print("⚠️ LocalNetworkPermission: NWListener failed (non-policy): \(error)")
|
|
108
|
-
timeout.cancel()
|
|
109
|
-
finish(.unknown)
|
|
110
|
-
}
|
|
111
|
+
print("🔍 LocalNetworkPermission NWListener error: \(code) — \(error.localizedDescription)")
|
|
112
|
+
// Only treat explicit policy errors as denied; other failures are unknown.
|
|
113
|
+
timeout.cancel()
|
|
114
|
+
finish(Self.isPolicyDenied(error) ? .denied : .unknown)
|
|
111
115
|
|
|
112
116
|
case .cancelled:
|
|
113
|
-
break
|
|
117
|
+
break
|
|
114
118
|
|
|
115
119
|
default:
|
|
116
|
-
break
|
|
120
|
+
break
|
|
117
121
|
}
|
|
118
122
|
}
|
|
119
123
|
|
|
@@ -124,12 +128,10 @@ import UIKit
|
|
|
124
128
|
|
|
125
129
|
/// Opens the app's page in iOS Settings so the user can manually grant Local Network access.
|
|
126
130
|
///
|
|
127
|
-
/// - Note: This is the only recovery path
|
|
128
|
-
/// - Warning: On iOS 17,
|
|
129
|
-
/// the permission for it to take effect. This is a known iOS 17 bug.
|
|
131
|
+
/// - Note: This is the only recovery path after denial — iOS cannot re-prompt.
|
|
132
|
+
/// - Warning: On iOS 17, a device restart may be needed after granting the permission.
|
|
130
133
|
public func openAppSettings() {
|
|
131
134
|
guard let url = URL(string: UIApplication.openSettingsURLString) else { return }
|
|
132
|
-
|
|
133
135
|
DispatchQueue.main.async {
|
|
134
136
|
UIApplication.shared.open(url)
|
|
135
137
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cappitolian/http-local-server-swifter",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.24",
|
|
4
4
|
"description": "Runs a local HTTP server on your device, accessible over LAN. Supports connect, disconnect, GET, and POST methods with IP and port discovery.",
|
|
5
5
|
"main": "dist/plugin.cjs.js",
|
|
6
6
|
"module": "dist/esm/index.js",
|