import { spawn } from "node:child_process"; import { mkdir, rm } from "node:fs/promises"; import { createWriteStream } from "node:fs"; import process from "node:process"; import { setTimeout as delay } from "node:timers/promises"; const chromePath = "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"; const port = Number(process.env.CDP_PORT || 9230); const navigationUrl = process.env.JSX_URL || "https://www.jsx.com/"; const userDataDir = process.env.CHROME_USER_DATA_DIR || `/tmp/jsx-live-monitor-${Date.now()}`; const artifactsDir = process.env.JSX_ARTIFACTS_DIR || "/tmp/jsx-live-monitor"; const logPath = `${artifactsDir}/network-events.ndjson`; const requestTimeoutMs = Number(process.env.REQUEST_TIMEOUT_MS || 30000); await mkdir(artifactsDir, { recursive: true }); const logStream = createWriteStream(logPath, { flags: "a" }); function writeLine(line) { console.log(line); logStream.write(`${line}\n`); } function log(message, details) { if (details === undefined) { writeLine(`[monitor] ${message}`); return; } writeLine(`[monitor] ${message} ${JSON.stringify(details)}`); } async function fetchJson(url, init = undefined) { const response = await fetch(url, init); if (!response.ok) { throw new Error(`HTTP ${response.status} for ${url}`); } return response.json(); } async function waitForDebugger() { const deadline = Date.now() + requestTimeoutMs; while (Date.now() < deadline) { try { return await fetchJson(`http://127.0.0.1:${port}/json/version`); } catch { await delay(250); } } throw new Error("Timed out waiting for Chrome remote debugger"); } async function waitForPageTarget() { const deadline = Date.now() + requestTimeoutMs; while (Date.now() < deadline) { try { const targets = await fetchJson(`http://127.0.0.1:${port}/json/list`); const pageTarget = targets.find(target => target.type === "page" && typeof target.url === "string" && target.url.startsWith("http") && target.webSocketDebuggerUrl ); if (pageTarget) { return pageTarget; } } catch { // Ignore transient startup failures while Chrome is coming up. } await delay(250); } throw new Error("Timed out waiting for initial Chrome page target"); } function launchChrome() { const args = [ `--remote-debugging-port=${port}`, `--user-data-dir=${userDataDir}`, "--no-first-run", "--no-default-browser-check", "--disable-default-apps", "--disable-popup-blocking", "--window-size=1440,1200", navigationUrl, ]; log("Launching Chrome", { chromePath, port, userDataDir, navigationUrl, logPath }); const child = spawn(chromePath, args, { detached: false, stdio: ["ignore", "pipe", "pipe"], }); child.stdout.on("data", chunk => { const text = chunk.toString().trim(); if (text) { writeLine(`[chrome] ${text}`); } }); child.stderr.on("data", chunk => { const text = chunk.toString().trim(); if (text) { writeLine(`[chrome] ${text}`); } }); return child; } class CDPClient { constructor(socketUrl) { this.socketUrl = socketUrl; this.ws = new WebSocket(socketUrl); this.nextId = 1; this.pending = new Map(); this.eventHandlers = new Map(); this.openPromise = new Promise((resolve, reject) => { this.ws.addEventListener("open", () => resolve()); this.ws.addEventListener("error", event => reject(event.error || new Error("WebSocket open failed"))); }); this.ws.addEventListener("message", event => { const message = JSON.parse(event.data.toString()); if (message.id) { const pending = this.pending.get(message.id); if (!pending) { return; } this.pending.delete(message.id); if (message.error) { pending.reject(new Error(message.error.message || JSON.stringify(message.error))); } else { pending.resolve(message.result); } return; } const handlers = this.eventHandlers.get(message.method) || []; for (const handler of handlers) { handler(message.params || {}); } }); } async ready() { await this.openPromise; } async send(method, params = {}) { await this.ready(); const id = this.nextId++; const payload = JSON.stringify({ id, method, params }); const promise = new Promise((resolve, reject) => { this.pending.set(id, { resolve, reject }); setTimeout(() => { if (this.pending.has(id)) { this.pending.delete(id); reject(new Error(`Timed out waiting for ${method}`)); } }, requestTimeoutMs); }); this.ws.send(payload); return promise; } on(method, handler) { const handlers = this.eventHandlers.get(method) || []; handlers.push(handler); this.eventHandlers.set(method, handlers); } async close() { if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) { this.ws.close(); } } } let shuttingDown = false; async function main() { const chrome = launchChrome(); let cdp; const shutdown = async signal => { if (shuttingDown) { return; } shuttingDown = true; log("Shutting down", { signal }); if (cdp) { await cdp.close(); } chrome.kill("SIGTERM"); await delay(500); await rm(userDataDir, { recursive: true, force: true }); logStream.end(); process.exit(0); }; process.on("SIGINT", shutdown); process.on("SIGTERM", shutdown); try { await waitForDebugger(); const target = await waitForPageTarget(); cdp = new CDPClient(target.webSocketDebuggerUrl); await cdp.ready(); log("Attached to page target", { id: target.id, url: target.url }); log("Manual monitor ready", { instructions: "Drive the JSX page in Chrome. All api.jsx.com traffic will stream here." }); const responseMetaByRequestId = new Map(); cdp.on("Page.frameNavigated", params => { const url = params.frame?.url; if (url) { log("Page navigated", { url }); } }); cdp.on("Network.requestWillBeSent", params => { const url = params.request?.url || ""; if (!url.includes("api.jsx.com")) { return; } const record = { stage: "request", type: params.type || "", method: params.request?.method || "", url, postData: params.request?.postData || "", hasAuthHeader: Boolean(params.request?.headers?.Authorization), }; writeLine(JSON.stringify(record)); }); cdp.on("Network.responseReceived", params => { const url = params.response?.url || ""; if (!url.includes("api.jsx.com")) { return; } const record = { stage: "response", type: params.type || "", status: params.response?.status || 0, mimeType: params.response?.mimeType || "", url, }; responseMetaByRequestId.set(params.requestId, record); writeLine(JSON.stringify(record)); }); cdp.on("Network.loadingFinished", async params => { const meta = responseMetaByRequestId.get(params.requestId); if (!meta || !meta.url.includes("api.jsx.com")) { return; } try { const body = await cdp.send("Network.getResponseBody", { requestId: params.requestId }); writeLine(JSON.stringify({ stage: "body", status: meta.status, url: meta.url, bodySnippet: (body.body || "").slice(0, 4000), base64Encoded: Boolean(body.base64Encoded), })); } catch (error) { writeLine(JSON.stringify({ stage: "body-error", url: meta.url, error: error.message, })); } }); await cdp.send("Page.enable"); await cdp.send("Runtime.enable"); await cdp.send("Network.enable", { maxTotalBufferSize: 100_000_000, maxResourceBufferSize: 10_000_000, maxPostDataSize: 1_000_000, }); await cdp.send("Network.setCacheDisabled", { cacheDisabled: true }); await new Promise(() => {}); } catch (error) { log("Monitor failed", { error: error.message }); await shutdown("error"); } } await main();