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,15 +1,25 @@
1
- import WebKit
2
1
  import Foundation
3
- import Network
2
+ import WebKit
4
3
 
4
+ /// Default URL for the WebView
5
+ let DEFAULT_SOURCE = "\(BUNDLE_BASE_URI)/edge-core-js.bundle/index.html"
6
+
7
+ /// A WebView that loads edge-core-js content using a custom URL scheme handler.
8
+ ///
9
+ /// Uses WKURLSchemeHandler to serve local assets via custom URLs (edgebundle://edge.bundle/...),
10
+ /// which provides a proper non-null origin for same-origin policy compliance
11
+ /// without requiring a local HTTP server.
12
+ ///
13
+ /// Includes Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers
14
+ /// required for SharedArrayBuffer support (needed by mixFetch web workers).
15
+ ///
16
+ /// Note: WKURLSchemeHandler only handles requests within this specific WKWebView instance.
17
+ /// It does not register a system-wide URL scheme - other apps cannot access this handler.
5
18
  class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
6
19
  var native = EdgeNative()
7
20
  var webView: WKWebView?
8
- private var httpServer: BundleHTTPServer?
9
- private var serverPort: UInt16 = 0
10
- private var serverReady = false
11
-
12
- // react api--------------------------------------------------------------
21
+
22
+ // MARK: - React API
13
23
 
14
24
  @objc var onMessage: RCTDirectEventBlock?
15
25
  @objc var onScriptError: RCTDirectEventBlock?
@@ -32,7 +42,7 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
32
42
  completionHandler: { result, error in return })
33
43
  }
34
44
 
35
- // view api --------------------------------------------------------------
45
+ // MARK: - View API
36
46
 
37
47
  required init?(coder: NSCoder) {
38
48
  return nil
@@ -41,34 +51,23 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
41
51
  override init(frame: CGRect) {
42
52
  super.init(frame: frame)
43
53
 
44
- // Set up our native bridge:
54
+ // Set up our native bridge and custom URL scheme handler:
45
55
  let configuration = WKWebViewConfiguration()
46
56
  configuration.userContentController = WKUserContentController()
47
57
  configuration.userContentController.add(self, name: "edgeCore")
48
58
 
59
+ // Register custom URL scheme handler BEFORE creating WKWebView
60
+ let schemeHandler = EdgeAssetsSchemeHandler()
61
+ configuration.setURLSchemeHandler(schemeHandler, forURLScheme: EDGE_SCHEME)
62
+
49
63
  // Set up the WKWebView child:
50
64
  let webView = WKWebView(frame: bounds, configuration: configuration)
51
65
  webView.navigationDelegate = self
52
66
  addSubview(webView)
53
67
  self.webView = webView
54
68
 
55
- // Start the HTTP server on an ephemeral port bound to loopback only
56
- let server = BundleHTTPServer()
57
- self.httpServer = server
58
- server.start { [weak self] result in
59
- DispatchQueue.main.async {
60
- switch result {
61
- case .success(let port):
62
- self?.serverPort = port
63
- self?.serverReady = true
64
- // Now that the server is ready with its assigned port, load the page
65
- self?.visitPage()
66
- case .failure(let error):
67
- print("Failed to start HTTP server: \(error)")
68
- // Server failed to start - the WebView won't be able to load local content
69
- }
70
- }
71
- }
69
+ // Scheme handler is ready immediately - no async startup needed
70
+ visitPage()
72
71
  }
73
72
 
74
73
  override func layoutSubviews() {
@@ -84,13 +83,9 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
84
83
  webView.removeFromSuperview()
85
84
  self.webView = nil
86
85
  }
87
-
88
- // Stop the HTTP server when view is removed
89
- httpServer?.stop()
90
- httpServer = nil
91
86
  }
92
87
 
93
- // callbacks -------------------------------------------------------------
88
+ // MARK: - Navigation Delegate
94
89
 
95
90
  func webView(
96
91
  _: WKWebView,
@@ -106,6 +101,8 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
106
101
  visitPage()
107
102
  }
108
103
 
104
+ // MARK: - Script Message Handler
105
+
109
106
  func userContentController(
110
107
  _: WKUserContentController,
111
108
  didReceive scriptMessage: WKScriptMessage
@@ -135,7 +132,7 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
135
132
  }
136
133
  }
137
134
 
138
- // utilities -------------------------------------------------------------
135
+ // MARK: - Utilities
139
136
 
140
137
  func handleMessage(
141
138
  _ name: String, args: NSArray
@@ -150,12 +147,6 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
150
147
  }
151
148
  }
152
149
 
153
- /// Returns the base URL for the local bundle HTTP server, or nil if the server isn't ready.
154
- func defaultSource() -> String? {
155
- guard serverReady else { return nil }
156
- return "http://127.0.0.1:\(serverPort)/index.html"
157
- }
158
-
159
150
  func stringify(_ raw: Any?) -> String {
160
151
  if let value = raw,
161
152
  let data = try? JSONSerialization.data(
@@ -172,18 +163,14 @@ class EdgeCoreWebView: RCTView, WKNavigationDelegate, WKScriptMessageHandler {
172
163
 
173
164
  func visitPage() {
174
165
  // If source is set, use it directly (e.g., webpack dev server for debugging)
175
- // Otherwise, use the local bundle HTTP server with ephemeral port
166
+ // Otherwise, use the custom URL scheme handler
176
167
  let baseUrl: String
177
168
  if let src = source, !src.isEmpty {
178
169
  baseUrl = src
179
170
  } else {
180
- guard let defaultUrl = defaultSource() else {
181
- print("EdgeCoreWebView: visitPage called before server is ready")
182
- return
183
- }
184
- baseUrl = defaultUrl
171
+ baseUrl = DEFAULT_SOURCE
185
172
  }
186
-
173
+
187
174
  guard let url = URL(string: baseUrl) else {
188
175
  print("EdgeCoreWebView: Invalid URL string: \(baseUrl)")
189
176
  return
@@ -73,35 +73,6 @@ window.addEdgeCorePlugins = addEdgeCorePlugins
73
73
  window.nativeBridge = nativeBridge
74
74
  window.reactBridge = reactBridge
75
75
 
76
- /**
77
- * Convert a file:// URI to a domain-relative path served by our local bundle server.
78
- * This is needed because cross-origin isolation (COOP/COEP) blocks file:// loads.
79
- * Using domain-relative paths ensures plugins load from the same origin as the page
80
- * (localhost:3693 in production, localhost:8080 in debug mode).
81
- */
82
- function convertPluginUri(uri) {
83
- // If already an HTTP URI, use as-is (e.g., custom bundles from metro bundler)
84
- if (uri.startsWith('http://') || uri.startsWith('https://')) {
85
- return uri
86
- }
87
-
88
- // Convert file:// URIs to domain-relative paths
89
- // Extract the bundle path (e.g., "edge-currency-accountbased.bundle/edge-currency-accountbased.js")
90
- const bundleMatch = uri.match(/([^/]+\.bundle\/[^/]+\.js)$/)
91
- if (bundleMatch != null) {
92
- return `/plugin/${bundleMatch[1]}`
93
- }
94
-
95
- // Fallback: try to extract just the filename
96
- const fileMatch = uri.match(/([^/]+\.js)$/)
97
- if (fileMatch != null) {
98
- return `/plugin/${fileMatch[1]}`
99
- }
100
-
101
- // If we can't parse it, return as-is (will likely fail, but provides debugging info)
102
- return uri
103
- }
104
-
105
76
  function loadPlugins(pluginUris) {
106
77
  const { head } = window.document
107
78
  if (head == null || pluginUris.length === 0) {
@@ -120,7 +91,7 @@ function loadPlugins(pluginUris) {
120
91
  script.addEventListener('load', handleLoad)
121
92
  script.charset = 'utf-8'
122
93
  script.defer = true
123
- script.src = convertPluginUri(uri)
94
+ script.src = uri
124
95
  head.appendChild(script)
125
96
  }
126
97
  }
@@ -208,10 +179,11 @@ async function makeIo(logBackend) {
208
179
  // Ensure mixFetch is initialized before use
209
180
  await initMixFetch(log)
210
181
  // Use queued fetch to handle mixFetch's one-request-per-host limitation
211
- return await queueMixFetch(uri, {
182
+ const response = await queueMixFetch(uri, {
212
183
  ...opts,
213
184
  mode: 'unsafe-ignore-cors'
214
185
  })
186
+ return response
215
187
  }
216
188
  if (corsBypass === 'always') {
217
189
  return await nativeFetch(uri, opts)
@@ -1,6 +1,7 @@
1
1
  const _jsxFileName = "src/react-native.tsx";import { asObject, asString } from 'cleaners'
2
2
  import { makeReactNativeDisklet } from 'disklet'
3
3
  import * as React from 'react'
4
+ import { NativeModules } from 'react-native'
4
5
  import { base64 } from 'rfc4648'
5
6
  import { bridgifyObject } from 'yaob'
6
7
 
@@ -22,6 +23,15 @@ import { timeout } from './util/promise'
22
23
  export { makeFakeIo } from './core/fake/fake-io'
23
24
  export * from './types/types'
24
25
 
26
+ const { EdgeCoreModule } = NativeModules
27
+
28
+ /**
29
+ * Constants exported from native:
30
+ * - bundleBaseUri: iOS "edgebundle://edge.bundle", Android "https://edge.bundle"
31
+ * - rootBaseUri: iOS "file:///path/to/Edge.app/", Android "file:///android_asset/"
32
+ */
33
+ const { bundleBaseUri, rootBaseUri } = EdgeCoreModule.getConstants()
34
+
25
35
  function defaultOnError(error) {
26
36
  console.error(error)
27
37
  }
@@ -69,7 +79,7 @@ export function MakeEdgeContext(props) {
69
79
  const context = await root.makeEdgeContext(
70
80
  bridgifyNativeIo(nativeIo),
71
81
  bridgifyLogBackend({ crashReporter, onLog }),
72
- pluginUris,
82
+ pluginUris.map(normalizePluginUri),
73
83
  {
74
84
  airbitzSupport,
75
85
  apiKey,
@@ -91,7 +101,7 @@ export function MakeEdgeContext(props) {
91
101
  }
92
102
  )
93
103
  await onLoad(context)
94
- }, __self: this, __source: {fileName: _jsxFileName, lineNumber: 64}}
104
+ }, __self: this, __source: {fileName: _jsxFileName, lineNumber: 74}}
95
105
  )
96
106
  )
97
107
  }
@@ -121,11 +131,11 @@ export function MakeFakeEdgeWorld(props) {
121
131
  const fakeWorld = await root.makeFakeEdgeWorld(
122
132
  bridgifyNativeIo(nativeIo),
123
133
  bridgifyLogBackend({ crashReporter, onLog }),
124
- pluginUris,
134
+ pluginUris.map(normalizePluginUri),
125
135
  props.users
126
136
  )
127
137
  await onLoad(fakeWorld)
128
- }, __self: this, __source: {fileName: _jsxFileName, lineNumber: 116}}
138
+ }, __self: this, __source: {fileName: _jsxFileName, lineNumber: 126}}
129
139
  )
130
140
  )
131
141
  }
@@ -217,3 +227,30 @@ export async function fetchLoginMessages(
217
227
  })
218
228
  })
219
229
  }
230
+
231
+ /**
232
+ * Convert a legacy plugin URI to an absolute URL that can be loaded by the WebView.
233
+ *
234
+ * Handles `file://` URIs by replacing the rootBaseUri prefix with bundleBaseUri:
235
+ * - `file:///android_asset/folder/file.js` → `{bundleBaseUri}/folder/file.js`
236
+ * - `file:///path/to/Edge.app/name.bundle/file.js` → `{bundleBaseUri}/name.bundle/file.js`
237
+ * - `edge-core/plugin-bundle.js` → `{bundleBaseUri}/edge-core/plugin-bundle.js`
238
+ *
239
+ * Full URLs are returned unchanged (http, https, edgebundle).
240
+ */
241
+ function normalizePluginUri(uri) {
242
+ // Handle file:// URIs that start with our root base URI
243
+ if (uri.startsWith(rootBaseUri)) {
244
+ const relativePath = uri.slice(rootBaseUri.length)
245
+ return `${bundleBaseUri}/${relativePath}`
246
+ }
247
+
248
+ // Handle relative paths (no schema) like "edge-core/plugin-bundle.js"
249
+ if (!uri.includes('://') && uri.match(/^[^/]+\/[^/]+\.(js)$/) != null) {
250
+ // Relative paths are return as absolute paths
251
+ return `${bundleBaseUri}/${uri}`
252
+ }
253
+
254
+ // Full URLs and anything else pass through unchanged
255
+ return uri
256
+ }
package/lib/util/nym.js CHANGED
@@ -11,14 +11,7 @@
11
11
  * Configuration options for the NYM mixFetch client.
12
12
  */
13
13
  export const mixFetchOptions = {
14
- preferredGateway: '5rXcNe2a44vXisK3uqLHCzpzvEwcnsijDMU7hg4fcYk8', // with WSS
15
- preferredNetworkRequester:
16
- '5x6q9UfVHs5AohKMUqeivj7a556kVVy7QwoKige8xHxh.6CFoB3kJaDbYz6oafPJxNxNjzahpT2NtgtytcSyN9EvF@5rXcNe2a44vXisK3uqLHCzpzvEwcnsijDMU7hg4fcYk8',
17
- mixFetchOverride: {
18
- requestTimeoutMs: 60000
19
- },
20
- forceTls: true, // force WSS
21
- extra: {}
14
+ forceTls: true // force WSS
22
15
  }
23
16
 
24
17
  // MixFetch initialization state
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "edge-core-js",
3
- "version": "2.41.1",
3
+ "version": "2.41.3",
4
4
  "description": "Edge account & wallet management library",
5
5
  "keywords": [
6
6
  "bitcoin",
@@ -1,318 +0,0 @@
1
- package app.edge.reactnative.core;
2
-
3
- import android.content.Context;
4
- import android.content.res.AssetManager;
5
- import android.util.Log;
6
-
7
- import java.io.BufferedReader;
8
- import java.io.ByteArrayOutputStream;
9
- import java.io.IOException;
10
- import java.io.InputStream;
11
- import java.io.InputStreamReader;
12
- import java.io.OutputStream;
13
- import java.net.InetAddress;
14
- import java.net.ServerSocket;
15
- import java.net.Socket;
16
- import java.util.concurrent.ExecutorService;
17
- import java.util.concurrent.Executors;
18
- import java.util.concurrent.atomic.AtomicBoolean;
19
-
20
- /**
21
- * A simple HTTP server that serves files from the Android assets folder.
22
- * Includes Cross-Origin-Opener-Policy and Cross-Origin-Embedder-Policy headers
23
- * required for SharedArrayBuffer support (needed by mixFetch web workers).
24
- *
25
- * Security: Binds to loopback interface only (127.0.0.1) on an ephemeral port
26
- * to prevent access from other devices on the network.
27
- */
28
- class BundleHTTPServer {
29
- private static final String TAG = "BundleHTTPServer";
30
-
31
- private final Context mContext;
32
- private ServerSocket mServerSocket;
33
- private ExecutorService mExecutor;
34
- private final AtomicBoolean mRunning = new AtomicBoolean(false);
35
- private int mAssignedPort = 0;
36
-
37
- /**
38
- * Callback interface for server start events.
39
- */
40
- public interface OnServerStartedListener {
41
- void onServerStarted(int port);
42
- void onServerError(Exception error);
43
- }
44
-
45
- public BundleHTTPServer(Context context) {
46
- mContext = context;
47
- }
48
-
49
- /**
50
- * Returns the assigned port after the server has started.
51
- * Returns 0 if the server hasn't started yet.
52
- */
53
- public int getAssignedPort() {
54
- return mAssignedPort;
55
- }
56
-
57
- /**
58
- * Starts the HTTP server on an ephemeral port bound to loopback only (127.0.0.1).
59
- * This prevents other devices on the network from connecting to the server.
60
- *
61
- * @param listener Callback to receive the assigned port or error
62
- */
63
- public void start(OnServerStartedListener listener) {
64
- if (mRunning.get()) {
65
- if (listener != null && mAssignedPort > 0) {
66
- listener.onServerStarted(mAssignedPort);
67
- }
68
- return;
69
- }
70
-
71
- mExecutor = Executors.newCachedThreadPool();
72
- mRunning.set(true);
73
-
74
- new Thread(() -> {
75
- try {
76
- // Bind to loopback only (127.0.0.1) on port 0 (ephemeral)
77
- // This prevents access from other devices on the network
78
- InetAddress loopback = InetAddress.getByName("127.0.0.1");
79
- mServerSocket = new ServerSocket(0, 50, loopback);
80
-
81
- // Get the assigned ephemeral port
82
- mAssignedPort = mServerSocket.getLocalPort();
83
- Log.d(TAG, "BundleHttpServer ready on 127.0.0.1:" + mAssignedPort);
84
-
85
- // Notify listener of the assigned port
86
- if (listener != null) {
87
- listener.onServerStarted(mAssignedPort);
88
- }
89
-
90
- while (mRunning.get()) {
91
- try {
92
- Socket clientSocket = mServerSocket.accept();
93
- mExecutor.execute(() -> handleConnection(clientSocket));
94
- } catch (IOException e) {
95
- if (mRunning.get()) {
96
- Log.e(TAG, "Error accepting connection: " + e.getMessage());
97
- }
98
- }
99
- }
100
- } catch (IOException e) {
101
- Log.e(TAG, "Failed to start HTTP server: " + e.getMessage());
102
- mRunning.set(false);
103
- if (listener != null) {
104
- listener.onServerError(e);
105
- }
106
- }
107
- }).start();
108
- }
109
-
110
- public void stop() {
111
- mRunning.set(false);
112
-
113
- if (mServerSocket != null) {
114
- try {
115
- mServerSocket.close();
116
- } catch (IOException e) {
117
- Log.e(TAG, "Error closing server socket: " + e.getMessage());
118
- }
119
- mServerSocket = null;
120
- }
121
-
122
- if (mExecutor != null) {
123
- mExecutor.shutdown();
124
- mExecutor = null;
125
- }
126
-
127
- Log.d(TAG, "BundleHttpServer stopped");
128
- }
129
-
130
- private void handleConnection(Socket clientSocket) {
131
- try {
132
- BufferedReader reader = new BufferedReader(new InputStreamReader(clientSocket.getInputStream()));
133
- OutputStream output = clientSocket.getOutputStream();
134
-
135
- // Read the request line
136
- String requestLine = reader.readLine();
137
- if (requestLine == null || requestLine.isEmpty()) {
138
- clientSocket.close();
139
- return;
140
- }
141
-
142
- // Parse method and path
143
- String[] parts = requestLine.split(" ");
144
- if (parts.length < 2) {
145
- sendResponse(output, 400, "Bad Request", "text/plain", "Bad Request".getBytes());
146
- clientSocket.close();
147
- return;
148
- }
149
-
150
- String method = parts[0];
151
- String path = parts[1];
152
-
153
- // Read and discard headers
154
- String line;
155
- while ((line = reader.readLine()) != null && !line.isEmpty()) {
156
- // Just consume headers
157
- }
158
-
159
- // Only support GET
160
- if (!method.equals("GET")) {
161
- sendResponse(output, 405, "Method Not Allowed", "text/plain", "Method Not Allowed".getBytes());
162
- clientSocket.close();
163
- return;
164
- }
165
-
166
- // Remove query parameters
167
- int queryIndex = path.indexOf('?');
168
- if (queryIndex > 0) {
169
- path = path.substring(0, queryIndex);
170
- }
171
-
172
- // Require explicit file name - no auto-matching for root path
173
- if (path.equals("/")) {
174
- sendResponse(output, 404, "Not Found", "text/plain", "Not Found".getBytes());
175
- clientSocket.close();
176
- return;
177
- }
178
-
179
- // Handle plugin bundle requests (e.g., /plugin/edge-currency-accountbased.bundle/edge-currency-accountbased.js)
180
- if (path.startsWith("/plugin/")) {
181
- String pluginPath = path.substring("/plugin/".length());
182
- servePluginFile(output, pluginPath);
183
- clientSocket.close();
184
- return;
185
- }
186
-
187
- // Remove leading slash and prepend assets path
188
- String assetPath = "edge-core-js" + path;
189
-
190
- // Try to read the asset (using try-with-resources to ensure stream is closed)
191
- try {
192
- AssetManager assetManager = mContext.getAssets();
193
- try (InputStream inputStream = assetManager.open(assetPath)) {
194
- byte[] data = readAllBytes(inputStream);
195
- String mimeType = getMimeType(path);
196
- sendResponse(output, 200, "OK", mimeType, data);
197
- }
198
- } catch (IOException e) {
199
- Log.d(TAG, "File not found: " + assetPath);
200
- sendResponse(output, 404, "Not Found", "text/plain", "Not Found".getBytes());
201
- }
202
-
203
- clientSocket.close();
204
- } catch (IOException e) {
205
- Log.e(TAG, "Error handling connection: " + e.getMessage());
206
- try {
207
- clientSocket.close();
208
- } catch (IOException ignored) {
209
- }
210
- }
211
- }
212
-
213
- private void servePluginFile(OutputStream output, String pluginPath) throws IOException {
214
- AssetManager assetManager = mContext.getAssets();
215
- byte[] data = null;
216
-
217
- // Plugin path format: "edge-currency-accountbased.bundle/edge-currency-accountbased.js"
218
- // or just: "plugin-bundle.js"
219
- // Try multiple asset path patterns
220
- String[] pathsToTry;
221
-
222
- if (pluginPath.contains(".bundle/")) {
223
- // Extract bundle name and file name
224
- String[] parts = pluginPath.split("\\.bundle/");
225
- if (parts.length >= 2) {
226
- String bundleName = parts[0];
227
- String fileName = parts[1];
228
- pathsToTry = new String[] {
229
- pluginPath, // As-is
230
- bundleName + "/" + fileName, // Without .bundle
231
- bundleName + ".bundle/" + fileName, // With .bundle as folder
232
- fileName, // Just the filename
233
- "edge-core/" + pluginPath, // In edge-core assets folder
234
- "edge-core-js/" + pluginPath // In edge-core-js assets folder
235
- };
236
- } else {
237
- pathsToTry = new String[] { pluginPath, "edge-core/" + pluginPath, "edge-core-js/" + pluginPath };
238
- }
239
- } else {
240
- // Just a filename like "plugin-bundle.js"
241
- // Try to find it in various asset locations
242
- String fileName = pluginPath;
243
- int dotIndex = fileName.lastIndexOf('.');
244
- if (dotIndex > 0) {
245
- String baseName = fileName.substring(0, dotIndex);
246
- pathsToTry = new String[] {
247
- "edge-core/" + pluginPath, // e.g., edge-core/plugin-bundle.js (app's plugin bundle)
248
- "edge-core-js/" + pluginPath, // e.g., edge-core-js/plugin-bundle.js
249
- baseName + ".bundle/" + fileName, // e.g., plugin-bundle.bundle/plugin-bundle.js
250
- baseName + "/" + fileName, // e.g., plugin-bundle/plugin-bundle.js
251
- pluginPath // Just the filename
252
- };
253
- } else {
254
- pathsToTry = new String[] { "edge-core/" + pluginPath, "edge-core-js/" + pluginPath, pluginPath };
255
- }
256
- }
257
-
258
- for (String assetPath : pathsToTry) {
259
- try (InputStream inputStream = assetManager.open(assetPath)) {
260
- data = readAllBytes(inputStream);
261
- Log.d(TAG, "Found plugin at: " + assetPath);
262
- break;
263
- } catch (IOException e) {
264
- // Try next path
265
- Log.d(TAG, "Plugin not found at: " + assetPath);
266
- }
267
- }
268
-
269
- if (data != null) {
270
- String mimeType = getMimeType(pluginPath);
271
- sendResponse(output, 200, "OK", mimeType, data);
272
- } else {
273
- Log.e(TAG, "Plugin file not found: " + pluginPath);
274
- sendResponse(output, 404, "Not Found", "text/plain", ("Not Found: " + pluginPath).getBytes());
275
- }
276
- }
277
-
278
- private void sendResponse(OutputStream output, int code, String status, String contentType, byte[] body)
279
- throws IOException {
280
- StringBuilder headers = new StringBuilder();
281
- headers.append("HTTP/1.1 ").append(code).append(" ").append(status).append("\r\n");
282
- headers.append("Content-Type: ").append(contentType).append("\r\n");
283
- headers.append("Content-Length: ").append(body.length).append("\r\n");
284
- headers.append("Connection: close\r\n");
285
- headers.append("Server: EdgeCoreBundleServer/1.0\r\n");
286
- // Cross-origin isolation headers required for SharedArrayBuffer (needed by mixFetch web workers)
287
- headers.append("Cross-Origin-Opener-Policy: same-origin\r\n");
288
- headers.append("Cross-Origin-Embedder-Policy: require-corp\r\n");
289
- headers.append("\r\n");
290
-
291
- output.write(headers.toString().getBytes("UTF-8"));
292
- output.write(body);
293
- output.flush();
294
- }
295
-
296
- private byte[] readAllBytes(InputStream inputStream) throws IOException {
297
- ByteArrayOutputStream buffer = new ByteArrayOutputStream();
298
- byte[] chunk = new byte[4096];
299
- int bytesRead;
300
- while ((bytesRead = inputStream.read(chunk)) != -1) {
301
- buffer.write(chunk, 0, bytesRead);
302
- }
303
- return buffer.toByteArray();
304
- }
305
-
306
- // We only serve HTML, JS, and WASM files
307
- private String getMimeType(String path) {
308
- String lowerPath = path.toLowerCase();
309
- if (lowerPath.endsWith(".html") || lowerPath.endsWith(".htm")) {
310
- return "text/html";
311
- } else if (lowerPath.endsWith(".js")) {
312
- return "application/javascript";
313
- } else if (lowerPath.endsWith(".wasm")) {
314
- return "application/wasm";
315
- }
316
- return "application/octet-stream";
317
- }
318
- }