/** * proxy.js – zero-dependency worker-based HTTP/WS forwarder */ const { parentPort, workerData } = require("worker_threads"); const http = require("http"); const https = require("https"); const { URL } = require("url"); const fs = require("fs"); const path = require("path"); /* ──────────────────────────── worker code ─────────────────────────────── */ const LISTEN_HOST = "localhost"; const LISTEN_PORT = workerData.port; let rememberedOrigin = null; // e.g. "http://localhost:5173" /* ---------- pre-configure rememberedOrigin from workerData ------- */ { const fixed = workerData?.targetOrigin; if (fixed) { try { rememberedOrigin = new URL(fixed).origin; parentPort?.postMessage( `[proxy-worker] fixed upstream: ${rememberedOrigin}`, ); } catch { throw new Error( `Invalid target origin "${fixed}". Must be absolute http/https URL.`, ); } } } /* ---------- optional resources for HTML injection ---------------------- */ let stacktraceJsContent = null; let dyadShimContent = null; let dyadComponentSelectorClientContent = null; try { const stackTraceLibPath = path.join( __dirname, "..", "node_modules", "stacktrace-js", "dist", "stacktrace.min.js", ); stacktraceJsContent = fs.readFileSync(stackTraceLibPath, "utf-8"); parentPort?.postMessage("[proxy-worker] stacktrace.js loaded."); } catch (error) { parentPort?.postMessage( `[proxy-worker] Failed to read stacktrace.js: ${error.message}`, ); } try { const dyadShimPath = path.join(__dirname, "dyad-shim.js"); dyadShimContent = fs.readFileSync(dyadShimPath, "utf-8"); parentPort?.postMessage("[proxy-worker] dyad-shim.js loaded."); } catch (error) { parentPort?.postMessage( `[proxy-worker] Failed to read dyad-shim.js: ${error.message}`, ); } try { const dyadComponentSelectorClientPath = path.join( __dirname, "dyad-component-selector-client.js", ); dyadComponentSelectorClientContent = fs.readFileSync( dyadComponentSelectorClientPath, "utf-8", ); parentPort?.postMessage( "[proxy-worker] dyad-component-selector-client.js loaded.", ); } catch (error) { parentPort?.postMessage( `[proxy-worker] Failed to read dyad-component-selector-client.js: ${error.message}`, ); } /* ---------------------- helper: need to inject? ------------------------ */ function needsInjection(pathname) { return pathname.endsWith("index.html") || pathname === "/"; } function injectHTML(buf) { let txt = buf.toString("utf8"); // These are strings that were used since the first version of the dyad shim. // If the dyad shim is used from legacy apps which came pre-baked with the shim // as a vite plugin, then do not inject the shim twice to avoid weird behaviors. const legacyAppWithShim = txt.includes("window-error") && txt.includes("unhandled-rejection"); const scripts = []; if (!legacyAppWithShim) { if (stacktraceJsContent) { scripts.push(``); } else { scripts.push( '', ); } if (dyadShimContent) { scripts.push(``); } else { scripts.push( '', ); } } if (dyadComponentSelectorClientContent) { scripts.push(``); } else { scripts.push( '', ); } const allScripts = scripts.join("\n"); const headRegex = /]*>/i; if (headRegex.test(txt)) { txt = txt.replace(headRegex, `$&\n${allScripts}`); } else { txt = allScripts + "\n" + txt; parentPort?.postMessage( "[proxy-worker] Warning: tag not found – scripts prepended.", ); } return Buffer.from(txt, "utf8"); } /* ---------------- helper: build upstream URL from request -------------- */ function buildTargetURL(clientReq) { if (!rememberedOrigin) throw new Error("No upstream configured."); // Forward to the remembered origin keeping path & query return new URL(clientReq.url, rememberedOrigin); } /* ----------------------------------------------------------------------- */ /* 1. Plain HTTP request / response */ /* ----------------------------------------------------------------------- */ const server = http.createServer((clientReq, clientRes) => { let target; try { target = buildTargetURL(clientReq); } catch (err) { clientRes.writeHead(400, { "content-type": "text/plain" }); return void clientRes.end("Bad request: " + err.message); } const isTLS = target.protocol === "https:"; const lib = isTLS ? https : http; /* Copy request headers but rewrite Host / Origin / Referer */ const headers = { ...clientReq.headers, host: target.host }; if (headers.origin) headers.origin = target.origin; if (headers.referer) { try { const ref = new URL(headers.referer); headers.referer = target.origin + ref.pathname + ref.search; } catch { delete headers.referer; } } if (needsInjection) { // Request uncompressed content from upstream delete headers["accept-encoding"]; } if (headers["if-none-match"] && needsInjection(target.pathname)) delete headers["if-none-match"]; const upOpts = { protocol: target.protocol, hostname: target.hostname, port: target.port || (isTLS ? 443 : 80), path: target.pathname + target.search, method: clientReq.method, headers, }; const upReq = lib.request(upOpts, (upRes) => { const inject = needsInjection(target.pathname); if (!inject) { clientRes.writeHead(upRes.statusCode, upRes.headers); return void upRes.pipe(clientRes); } const chunks = []; upRes.on("data", (c) => chunks.push(c)); upRes.on("end", () => { try { const merged = Buffer.concat(chunks); const patched = injectHTML(merged); const hdrs = { ...upRes.headers, "content-length": Buffer.byteLength(patched), }; // If we injected content, it's no longer encoded in the original way delete hdrs["content-encoding"]; // Also, remove ETag as content has changed delete hdrs["etag"]; clientRes.writeHead(upRes.statusCode, hdrs); clientRes.end(patched); } catch (e) { clientRes.writeHead(500, { "content-type": "text/plain" }); clientRes.end("Injection failed: " + e.message); } }); }); clientReq.pipe(upReq); upReq.on("error", (e) => { clientRes.writeHead(502, { "content-type": "text/plain" }); clientRes.end("Upstream error: " + e.message); }); }); /* ----------------------------------------------------------------------- */ /* 2. WebSocket / generic Upgrade tunnelling */ /* ----------------------------------------------------------------------- */ server.on("upgrade", (req, socket, _head) => { let target; try { target = buildTargetURL(req); } catch (err) { socket.write("HTTP/1.1 400 Bad Request\r\n\r\n" + err.message); return socket.destroy(); } const isTLS = target.protocol === "https:"; const headers = { ...req.headers, host: target.host }; if (headers.origin) headers.origin = target.origin; const upReq = (isTLS ? https : http).request({ protocol: target.protocol, hostname: target.hostname, port: target.port || (isTLS ? 443 : 80), path: target.pathname + target.search, method: "GET", headers, }); upReq.on("upgrade", (upRes, upSocket, upHead) => { socket.write( "HTTP/1.1 101 Switching Protocols\r\n" + Object.entries(upRes.headers) .map(([k, v]) => `${k}: ${v}`) .join("\r\n") + "\r\n\r\n", ); if (upHead && upHead.length) socket.write(upHead); upSocket.pipe(socket).pipe(upSocket); }); upReq.on("error", () => socket.destroy()); upReq.end(); }); /* ----------------------------------------------------------------------- */ server.listen(LISTEN_PORT, LISTEN_HOST, () => { parentPort?.postMessage( `proxy-server-start url=http://${LISTEN_HOST}:${LISTEN_PORT}`, ); });