edge-core-js 2.41.1 → 2.41.3

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.
@@ -1,394 +0,0 @@
1
- import Foundation
2
- import Network
3
-
4
- class BundleHTTPServer {
5
- private var listener: NWListener?
6
- private(set) var assignedPort: UInt16 = 0
7
- private let queue = DispatchQueue(label: "com.edge.bundleserver")
8
-
9
- enum ServerError: Error {
10
- case portUnavailable
11
- case bindFailed(Error)
12
- }
13
-
14
- init() {}
15
-
16
- /// Starts the HTTP server on an ephemeral port bound to loopback only (127.0.0.1).
17
- /// This prevents other devices on the network from connecting to the server.
18
- /// - Parameter completion: Called with the assigned port on success, or an error on failure.
19
- /// This is called on the server's dispatch queue.
20
- func start(completion: @escaping (Result<UInt16, Error>) -> Void) {
21
- do {
22
- // Configure TCP parameters to bind to loopback only (127.0.0.1)
23
- // Port 0 tells the OS to assign an available ephemeral port
24
- let params = NWParameters.tcp
25
- params.requiredLocalEndpoint = NWEndpoint.hostPort(host: "127.0.0.1", port: 0)
26
-
27
- listener = try NWListener(using: params)
28
-
29
- var completionCalled = false
30
- listener?.stateUpdateHandler = { [weak self] state in
31
- switch state {
32
- case .ready:
33
- // Get the assigned ephemeral port from the listener
34
- if let port = self?.listener?.port?.rawValue {
35
- self?.assignedPort = port
36
- print("BundleHttpServer ready on 127.0.0.1:\(port)")
37
- if !completionCalled {
38
- completionCalled = true
39
- completion(.success(port))
40
- }
41
- } else {
42
- print("BundleHttpServer failed: could not get assigned port")
43
- if !completionCalled {
44
- completionCalled = true
45
- completion(.failure(ServerError.portUnavailable))
46
- }
47
- }
48
- case .failed(let error):
49
- print("BundleHttpServer failed with error: \(error)")
50
- if !completionCalled {
51
- completionCalled = true
52
- completion(.failure(error))
53
- }
54
- case .cancelled:
55
- print("BundleHttpServer was cancelled")
56
- default:
57
- break
58
- }
59
- }
60
-
61
- listener?.newConnectionHandler = { [weak self] connection in
62
- self?.handleConnection(connection)
63
- }
64
-
65
- listener?.start(queue: queue)
66
- } catch {
67
- print("Failed to start HTTP server: \(error)")
68
- completion(.failure(ServerError.bindFailed(error)))
69
- }
70
- }
71
-
72
- func stop() {
73
- listener?.cancel()
74
- }
75
-
76
- private func handleConnection(_ connection: NWConnection) {
77
- connection.stateUpdateHandler = { state in
78
- switch state {
79
- case .ready:
80
- self.receiveRequest(on: connection)
81
- case .failed(let error):
82
- print("Connection failed: \(error)")
83
- connection.cancel()
84
- case .cancelled:
85
- break
86
- default:
87
- break
88
- }
89
- }
90
-
91
- connection.start(queue: queue)
92
- }
93
-
94
- private func receiveRequest(on connection: NWConnection) {
95
- connection.receive(minimumIncompleteLength: 1, maximumLength: 2048) { [weak self] content, _, isComplete, error in
96
- guard let self = self else { return }
97
-
98
- if let error = error {
99
- print("Error receiving request: \(error)")
100
- connection.cancel()
101
- return
102
- }
103
-
104
- guard let content = content, !content.isEmpty else {
105
- if isComplete {
106
- connection.cancel()
107
- }
108
- return
109
- }
110
-
111
- // Parse the request
112
- if let requestString = String(data: content, encoding: .utf8) {
113
- let requestLines = requestString.components(separatedBy: "\r\n")
114
- if let firstLine = requestLines.first {
115
- let components = firstLine.components(separatedBy: " ")
116
- if components.count >= 2 {
117
- let method = components[0]
118
- var path = components[1]
119
-
120
- // Remove query parameters if any
121
- if let queryStart = path.firstIndex(of: "?") {
122
- path = String(path[..<queryStart])
123
- }
124
-
125
- // Require explicit file name - no auto-matching for root path
126
- if path == "/" {
127
- self.sendResponse(code: 404, body: "Not Found", connection: connection)
128
- return
129
- }
130
-
131
- // Handle plugin bundle requests (e.g., /plugin/edge-currency-accountbased.bundle/edge-currency-accountbased.js)
132
- if path.hasPrefix("/plugin/") {
133
- let pluginPath = String(path.dropFirst("/plugin/".count))
134
- self.servePluginFile(pluginPath, method: method, connection: connection)
135
- return
136
- }
137
-
138
- // Remove leading slash for bundle resource lookup
139
- let resourcePath = String(path.dropFirst())
140
-
141
- self.serveFile(resourcePath, method: method, connection: connection)
142
- return
143
- }
144
- }
145
- }
146
-
147
- // If we got here, the request was invalid
148
- self.sendResponse(code: 400, body: "Bad Request", connection: connection)
149
- }
150
- }
151
-
152
- private func serveFile(_ path: String, method: String, connection: NWConnection) {
153
- // Only support GET requests
154
- guard method == "GET" else {
155
- sendResponse(code: 405, body: "Method Not Allowed", connection: connection)
156
- return
157
- }
158
-
159
- // Require explicit file name - no auto-matching for empty paths
160
- guard !path.isEmpty else {
161
- sendResponse(code: 404, body: "Not Found", connection: connection)
162
- return
163
- }
164
-
165
- // Parse filename and extension properly (handles multi-dot filenames like "some.file.js")
166
- let nsPath = path as NSString
167
- let fileExtension = nsPath.pathExtension
168
- let filename = nsPath.deletingPathExtension
169
-
170
- // Try to find the resource in the main bundle first
171
- var url: URL?
172
- var data: Data?
173
-
174
- // Check if this is a request for a bundled file
175
- if let bundleUrl = Bundle.main.url(forResource: "edge-core-js", withExtension: "bundle"),
176
- let bundle = Bundle(url: bundleUrl) {
177
- if !fileExtension.isEmpty {
178
- url = bundle.url(forResource: filename, withExtension: fileExtension)
179
- } else {
180
- url = bundle.url(forResource: path, withExtension: nil)
181
- }
182
- }
183
-
184
- // If not found in the bundle, check the main bundle directly
185
- if url == nil {
186
- if !fileExtension.isEmpty {
187
- url = Bundle.main.url(forResource: filename, withExtension: fileExtension)
188
- } else {
189
- url = Bundle.main.url(forResource: path, withExtension: nil)
190
- }
191
- }
192
-
193
- if let url = url {
194
- do {
195
- data = try Data(contentsOf: url)
196
- } catch {
197
- print("Error reading file: \(error)")
198
- }
199
- }
200
-
201
- guard let fileData = data else {
202
- sendResponse(code: 404, body: "Not Found", connection: connection)
203
- return
204
- }
205
-
206
- let mimeType = mimeTypeForPath(path)
207
- let headers = [
208
- "HTTP/1.1 200 OK",
209
- "Content-Type: \(mimeType)",
210
- "Content-Length: \(fileData.count)",
211
- "Connection: close",
212
- "Server: EdgeCoreBundleServer/1.0",
213
- // Cross-origin isolation headers required for SharedArrayBuffer (needed by mixFetch web workers)
214
- "Cross-Origin-Opener-Policy: same-origin",
215
- "Cross-Origin-Embedder-Policy: require-corp",
216
- "\r\n"
217
- ].joined(separator: "\r\n")
218
-
219
- let headerData = headers.data(using: .utf8)!
220
- let responseData = NSMutableData()
221
- responseData.append(headerData)
222
- responseData.append(fileData)
223
-
224
- connection.send(content: responseData as Data, completion: .contentProcessed { error in
225
- if let error = error {
226
- print("Error sending response: \(error)")
227
- }
228
- connection.cancel()
229
- })
230
- }
231
-
232
- private func servePluginFile(_ path: String, method: String, connection: NWConnection) {
233
- // Only support GET requests
234
- guard method == "GET" else {
235
- sendResponse(code: 405, body: "Method Not Allowed", connection: connection)
236
- return
237
- }
238
-
239
- // Get the app's main bundle path - plugins are in edge-core/ subdirectory
240
- let bundlePath = Bundle.main.bundlePath
241
- let edgeCorePath = (bundlePath as NSString).appendingPathComponent("edge-core")
242
- var data: Data?
243
-
244
- // Try multiple path patterns
245
- let pathsToTry: [String]
246
-
247
- if path.contains(".bundle/") {
248
- // Path like: "edge-currency-accountbased.bundle/edge-currency-accountbased.js"
249
- pathsToTry = [path]
250
- } else {
251
- // Path like: "plugin-bundle.js" - try with .bundle folder too
252
- let fileName = (path as NSString).lastPathComponent
253
- let baseName = (fileName as NSString).deletingPathExtension
254
- pathsToTry = [
255
- path, // plugin-bundle.js
256
- "\(baseName).bundle/\(fileName)" // plugin-bundle.bundle/plugin-bundle.js
257
- ]
258
- }
259
-
260
- for relativePath in pathsToTry {
261
- // Try edge-core/ subdirectory first (for plugin-bundle.js)
262
- let edgeCoreFull = (edgeCorePath as NSString).appendingPathComponent(relativePath)
263
- if FileManager.default.fileExists(atPath: edgeCoreFull) {
264
- do {
265
- data = try Data(contentsOf: URL(fileURLWithPath: edgeCoreFull))
266
- print("Found plugin file at: \(edgeCoreFull)")
267
- break
268
- } catch {
269
- print("Error reading file at \(edgeCoreFull): \(error)")
270
- }
271
- }
272
-
273
- // Fall back to app bundle root (for .bundle/ plugins)
274
- if data == nil {
275
- let rootFull = (bundlePath as NSString).appendingPathComponent(relativePath)
276
- if FileManager.default.fileExists(atPath: rootFull) {
277
- do {
278
- data = try Data(contentsOf: URL(fileURLWithPath: rootFull))
279
- print("Found plugin file at: \(rootFull)")
280
- break
281
- } catch {
282
- print("Error reading file at \(rootFull): \(error)")
283
- }
284
- }
285
- }
286
- }
287
-
288
- guard let fileData = data else {
289
- print("Plugin file not found: \(path)")
290
- sendResponse(code: 404, body: "Not Found: \(path)", connection: connection)
291
- return
292
- }
293
-
294
- let mimeType = mimeTypeForPath(path)
295
- let headers = [
296
- "HTTP/1.1 200 OK",
297
- "Content-Type: \(mimeType)",
298
- "Content-Length: \(fileData.count)",
299
- "Connection: close",
300
- "Server: EdgeCoreBundleServer/1.0",
301
- // Cross-origin isolation headers required for SharedArrayBuffer (needed by mixFetch web workers)
302
- "Cross-Origin-Opener-Policy: same-origin",
303
- "Cross-Origin-Embedder-Policy: require-corp",
304
- "\r\n"
305
- ].joined(separator: "\r\n")
306
-
307
- let headerData = headers.data(using: .utf8)!
308
- let responseData = NSMutableData()
309
- responseData.append(headerData)
310
- responseData.append(fileData)
311
-
312
- connection.send(content: responseData as Data, completion: .contentProcessed { error in
313
- if let error = error {
314
- print("Error sending plugin response: \(error)")
315
- }
316
- connection.cancel()
317
- })
318
- }
319
-
320
- private func sendResponse(code: Int, body: String, connection: NWConnection) {
321
- var status = ""
322
- switch code {
323
- case 200: status = "OK"
324
- case 400: status = "Bad Request"
325
- case 404: status = "Not Found"
326
- case 405: status = "Method Not Allowed"
327
- default: status = "Internal Server Error"
328
- }
329
-
330
- let bodyData = body.data(using: .utf8)!
331
- let headers = [
332
- "HTTP/1.1 \(code) \(status)",
333
- "Content-Type: text/plain",
334
- "Content-Length: \(bodyData.count)",
335
- "Connection: close",
336
- "Server: EdgeCoreBundleServer/1.0",
337
- // Cross-origin isolation headers required for SharedArrayBuffer (needed by mixFetch web workers)
338
- "Cross-Origin-Opener-Policy: same-origin",
339
- "Cross-Origin-Embedder-Policy: require-corp",
340
- "\r\n"
341
- ].joined(separator: "\r\n")
342
-
343
- let headerData = headers.data(using: .utf8)!
344
- let responseData = NSMutableData()
345
- responseData.append(headerData)
346
- responseData.append(bodyData)
347
-
348
- connection.send(content: responseData as Data, completion: .contentProcessed { error in
349
- if let error = error {
350
- print("Error sending response: \(error)")
351
- }
352
- connection.cancel()
353
- })
354
- }
355
-
356
- private func sendHtmlResponse(html: String, connection: NWConnection) {
357
- let bodyData = html.data(using: .utf8)!
358
- let headers = [
359
- "HTTP/1.1 200 OK",
360
- "Content-Type: text/html",
361
- "Content-Length: \(bodyData.count)",
362
- "Connection: close",
363
- "Server: EdgeCoreBundleServer/1.0",
364
- // Cross-origin isolation headers required for SharedArrayBuffer (needed by mixFetch web workers)
365
- "Cross-Origin-Opener-Policy: same-origin",
366
- "Cross-Origin-Embedder-Policy: require-corp",
367
- "\r\n"
368
- ].joined(separator: "\r\n")
369
-
370
- let headerData = headers.data(using: .utf8)!
371
- let responseData = NSMutableData()
372
- responseData.append(headerData)
373
- responseData.append(bodyData)
374
-
375
- connection.send(content: responseData as Data, completion: .contentProcessed { error in
376
- if let error = error {
377
- print("Error sending response: \(error)")
378
- }
379
- connection.cancel()
380
- })
381
- }
382
-
383
- private func mimeTypeForPath(_ path: String) -> String {
384
- let ext = (path as NSString).pathExtension.lowercased()
385
-
386
- // We only serve HTML, JS, and WASM files
387
- switch ext {
388
- case "html", "htm": return "text/html"
389
- case "js": return "application/javascript"
390
- case "wasm": return "application/wasm"
391
- default: return "application/octet-stream"
392
- }
393
- }
394
- }