@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 Bonjour advertising:
12
- /// NWBrowser returns .ready even when permission is revoked (iOS bug).
13
- /// NWListener advertising a Bonjour service correctly fails with PolicyDenied
14
- /// when Local Network permission is OFF, including previously-revoked cases.
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
- /// NWListener is the only iOS API that correctly detects a revoked Local Network
41
- /// permission. NWBrowser incorrectly returns .ready in that case (iOS bug).
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 — resolving as .unknown")
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
- listener.stateUpdateHandler = { state in
89
- print("🔍 LocalNetworkPermission NWListener state: \(state)")
90
- switch state {
91
- case .ready:
92
- // Bonjour advertisement succeeded — permission is granted.
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 code: \(code) — \(error.localizedDescription)")
99
-
100
- if Self.isPolicyDenied(error) {
101
- // PolicyDenied or NoAuth — user has denied Local Network permission.
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 // Expected — we cancelled it ourselves in finish()
117
+ break
114
118
 
115
119
  default:
116
- break // .setup, .waiting — keep waiting
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 available after the user denies the permission.
128
- /// - Warning: On iOS 17, the user may need to restart the device after granting
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.23",
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",