@jaw.id/cli 0.0.2 → 0.0.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.
- package/dist/lib/ws-daemon.js +42 -4
- package/dist/lib/ws-daemon.js.map +1 -1
- package/package.json +1 -1
package/dist/lib/ws-daemon.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
import * as crypto from 'crypto';
|
|
3
3
|
import * as fs from 'fs';
|
|
4
|
+
import * as http from 'http';
|
|
4
5
|
import * as path from 'path';
|
|
5
6
|
import * as os from 'os';
|
|
6
7
|
import { WebSocketServer, WebSocket } from 'ws';
|
|
@@ -101,13 +102,50 @@ process.on("SIGINT", () => {
|
|
|
101
102
|
});
|
|
102
103
|
process.on("exit", cleanup);
|
|
103
104
|
var MAX_PAYLOAD_BYTES = 1048576;
|
|
105
|
+
var trustedOrigin = new URL(args.keysUrl).origin;
|
|
106
|
+
function isTrustedOrigin(origin) {
|
|
107
|
+
if (origin === trustedOrigin) return true;
|
|
108
|
+
try {
|
|
109
|
+
const parsed = new URL(origin);
|
|
110
|
+
return parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
111
|
+
} catch {
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
var httpServer = http.createServer((req, res) => {
|
|
116
|
+
if (req.method === "OPTIONS") {
|
|
117
|
+
const origin = req.headers.origin ?? "";
|
|
118
|
+
if (!isTrustedOrigin(origin)) {
|
|
119
|
+
res.writeHead(403, { "Content-Type": "text/plain" });
|
|
120
|
+
res.end("Forbidden");
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
res.writeHead(204, {
|
|
124
|
+
"Access-Control-Allow-Origin": origin,
|
|
125
|
+
"Access-Control-Allow-Methods": "GET, OPTIONS",
|
|
126
|
+
"Access-Control-Allow-Headers": "Content-Type",
|
|
127
|
+
"Access-Control-Allow-Private-Network": "true",
|
|
128
|
+
"Access-Control-Max-Age": "86400"
|
|
129
|
+
});
|
|
130
|
+
res.end();
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
res.writeHead(426, { "Content-Type": "text/plain" });
|
|
134
|
+
res.end("Upgrade required");
|
|
135
|
+
});
|
|
104
136
|
var wss = new WebSocketServer({
|
|
105
|
-
|
|
106
|
-
port: 0,
|
|
137
|
+
server: httpServer,
|
|
107
138
|
maxPayload: MAX_PAYLOAD_BYTES
|
|
108
139
|
});
|
|
109
|
-
wss.on("
|
|
110
|
-
const
|
|
140
|
+
wss.on("headers", (headers, req) => {
|
|
141
|
+
const origin = req.headers.origin;
|
|
142
|
+
if (origin && isTrustedOrigin(origin)) {
|
|
143
|
+
headers.push(`Access-Control-Allow-Origin: ${origin}`);
|
|
144
|
+
headers.push("Access-Control-Allow-Private-Network: true");
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
httpServer.listen(0, "127.0.0.1", async () => {
|
|
148
|
+
const addr = httpServer.address();
|
|
111
149
|
if (typeof addr === "string" || !addr) {
|
|
112
150
|
process.exit(1);
|
|
113
151
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/lib/ws-daemon.ts"],"names":[],"mappings":"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","file":"ws-daemon.js","sourcesContent":["#!/usr/bin/env node\n/**\n * WS Bridge Daemon\n *\n * Long-lived background process that:\n * 1. Starts a WebSocket server on 127.0.0.1:{random_port}\n * 2. Opens browser to keys.jaw.id/cli-bridge\n * 3. Accepts WebSocket connections from the browser (SDK) and CLI clients\n * 4. Routes RPC requests from CLI clients to the browser SDK\n * 5. Writes connection info to ~/.jaw/bridge.json\n *\n * The daemon stays alive across CLI commands so the browser SDK instance\n * (and its in-memory state) persists.\n */\n\nimport * as crypto from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { WebSocketServer, WebSocket } from \"ws\";\n\nconst JAW_DIR = path.join(os.homedir(), \".jaw\");\nconst BRIDGE_PATH = path.join(JAW_DIR, \"bridge.json\");\nconst HEARTBEAT_INTERVAL_MS = 30_000;\nconst IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 min idle → auto-shutdown\n\n// ── Limits ────────────────────────────────────────────────────────\nconst MAX_PENDING_REQUESTS = 100;\nconst MAX_CLI_CLIENTS = 50;\nconst RATE_LIMIT_WINDOW_MS = 60_000; // 1 minute\nconst RATE_LIMIT_MAX_REQUESTS = 60; // 60 requests per minute per client\n\n// ── Types ──────────────────────────────────────────────────────────\n\ninterface BridgeInfo {\n port: number;\n token: string;\n pid: number;\n startedAt: string;\n}\n\ninterface PendingRequest {\n clientWs: WebSocket;\n clientId: string;\n timer: ReturnType<typeof setTimeout>;\n}\n\ninterface RateLimitEntry {\n timestamps: number[];\n}\n\n// ── keysUrl validation (duplicated from validation.ts for standalone daemon) ─\n\nfunction isValidKeysUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"https:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n return isTrustedHost && isSecure;\n } catch {\n return false;\n }\n}\n\n// ── Parse args from parent process ─────────────────────────────────\n\nconst args = JSON.parse(process.argv[2] ?? \"{}\") as {\n keysUrl: string;\n chainId: number;\n ens?: string;\n paymasterUrl?: string;\n timeout?: number;\n};\n\n// Validate keysUrl before proceeding\nif (!isValidKeysUrl(args.keysUrl)) {\n process.stderr.write(\n `Untrusted keysUrl: ${args.keysUrl}. Must be a *.jaw.id domain (HTTPS) or localhost.\\n`,\n );\n process.exit(1);\n}\n\n// API key is passed via env var to avoid exposure in `ps aux` output\nconst apiKey = process.env.JAW_DAEMON_API_KEY ?? \"\";\n// Clear from environment immediately after reading\ndelete process.env.JAW_DAEMON_API_KEY;\n\nconst token = crypto.randomUUID();\nconst timeout = args.timeout ?? 120_000;\n\n// Track pending requests: daemonRequestId -> { clientWs, clientId }\nconst pendingRequests = new Map<string, PendingRequest>();\n\n// Browser SDK connection\nlet browserWs: WebSocket | null = null;\nlet browserReady = false;\nlet idleTimer: ReturnType<typeof setTimeout> | null = null;\nlet heartbeat: ReturnType<typeof setInterval> | null = null;\n\n// Browser reopen state\nlet lastBrowserOpenTime = 0;\nconst BROWSER_REOPEN_COOLDOWN_MS = 5_000; // Min 5s between reopen attempts\nlet serverPort: number | null = null; // Stored after server starts listening\n\n// Track connected CLI clients (to notify when browser connects)\nconst cliClients = new Set<WebSocket>();\n\n// Per-client rate limiting\nconst rateLimits = new WeakMap<WebSocket, RateLimitEntry>();\n\n// ── Rate limiting ─────────────────────────────────────────────────\n\nfunction isRateLimited(ws: WebSocket): boolean {\n const now = Date.now();\n let entry = rateLimits.get(ws);\n if (!entry) {\n entry = { timestamps: [] };\n rateLimits.set(ws, entry);\n }\n\n // Remove timestamps outside the window\n entry.timestamps = entry.timestamps.filter(\n (t) => now - t < RATE_LIMIT_WINDOW_MS,\n );\n\n if (entry.timestamps.length >= RATE_LIMIT_MAX_REQUESTS) {\n return true;\n }\n\n entry.timestamps.push(now);\n return false;\n}\n\n// ── Browser reopen ────────────────────────────────────────────────\n\nasync function reopenBrowser(): Promise<void> {\n if (!serverPort) return;\n if (browserReady || browserWs?.readyState === WebSocket.OPEN) return;\n\n const now = Date.now();\n if (now - lastBrowserOpenTime < BROWSER_REOPEN_COOLDOWN_MS) return;\n\n lastBrowserOpenTime = now;\n const bridgeUrl = buildBridgeUrl(serverPort);\n try {\n const { default: open } = await import(\"open\");\n await open(bridgeUrl);\n } catch {\n // Best-effort; CLI will time out if browser never connects\n }\n}\n\n// ── Idle management ────────────────────────────────────────────────\n\nfunction resetIdleTimer(): void {\n if (idleTimer) clearTimeout(idleTimer);\n idleTimer = setTimeout(() => {\n cleanup();\n process.exit(0);\n }, IDLE_TIMEOUT_MS);\n}\n\n// ── Cleanup ────────────────────────────────────────────────────────\n\nfunction cleanup(): void {\n if (heartbeat) clearInterval(heartbeat);\n // Notify browser to close the tab\n if (browserWs?.readyState === WebSocket.OPEN) {\n browserWs.send(JSON.stringify({ type: \"shutdown\" }));\n }\n try {\n if (fs.existsSync(BRIDGE_PATH)) fs.unlinkSync(BRIDGE_PATH);\n } catch {\n // ignore\n }\n}\n\nprocess.on(\"SIGTERM\", () => {\n cleanup();\n process.exit(0);\n});\nprocess.on(\"SIGINT\", () => {\n cleanup();\n process.exit(0);\n});\nprocess.on(\"exit\", cleanup);\n\n// ── Start WebSocket server ─────────────────────────────────────────\n\nconst MAX_PAYLOAD_BYTES = 1_048_576; // 1 MB – plenty for any RPC payload\nconst wss = new WebSocketServer({\n host: \"127.0.0.1\",\n port: 0,\n maxPayload: MAX_PAYLOAD_BYTES,\n});\n\nwss.on(\"listening\", async () => {\n const addr = wss.address();\n if (typeof addr === \"string\" || !addr) {\n process.exit(1);\n }\n\n const port = addr.port;\n serverPort = port;\n\n // Write bridge info\n fs.mkdirSync(JAW_DIR, { recursive: true, mode: 0o700 });\n const info: BridgeInfo = {\n port,\n token,\n pid: process.pid,\n startedAt: new Date().toISOString(),\n };\n fs.writeFileSync(BRIDGE_PATH, JSON.stringify(info, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n\n // Open browser\n lastBrowserOpenTime = Date.now();\n const bridgeUrl = buildBridgeUrl(port);\n const { default: open } = await import(\"open\");\n await open(bridgeUrl);\n\n resetIdleTimer();\n});\n\nwss.on(\"connection\", (ws, req) => {\n const url = new URL(req.url ?? \"\", \"http://127.0.0.1\");\n const urlToken = url.searchParams.get(\"token\");\n const role = url.searchParams.get(\"role\"); // \"browser\" or \"cli\"\n\n if (urlToken !== token) {\n ws.close(4001, \"Invalid token\");\n return;\n }\n\n if (role === \"browser\") {\n handleBrowserConnection(ws);\n } else {\n handleCliConnection(ws);\n }\n});\n\n// ── Browser connection ─────────────────────────────────────────────\n\nfunction handleBrowserConnection(ws: WebSocket): void {\n if (browserWs) {\n browserWs.close(4000, \"Replaced by new browser connection\");\n }\n browserWs = ws;\n browserReady = false;\n\n // Send config over the authenticated WebSocket rather than via URL\n ws.send(\n JSON.stringify({\n type: \"init\",\n apiKey,\n chainId: args.chainId,\n ens: args.ens,\n paymasterUrl: args.paymasterUrl,\n }),\n );\n\n ws.on(\"message\", (data) => {\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(data.toString()) as Record<string, unknown>;\n } catch {\n return;\n }\n\n switch (msg.type) {\n case \"ready\":\n browserReady = true;\n // Start heartbeat\n if (heartbeat) clearInterval(heartbeat);\n heartbeat = setInterval(() => {\n if (browserWs?.readyState === WebSocket.OPEN) {\n browserWs.send(JSON.stringify({ type: \"ping\" }));\n }\n }, HEARTBEAT_INTERVAL_MS);\n // Notify any waiting CLI clients that browser is now connected\n for (const cli of cliClients) {\n if (cli.readyState === WebSocket.OPEN) {\n cli.send(JSON.stringify({ type: \"browser_connected\" }));\n }\n }\n break;\n\n case \"rpc_response\": {\n const id = msg.id as string;\n const pending = pendingRequests.get(id);\n if (!pending) return;\n\n clearTimeout(pending.timer);\n pendingRequests.delete(id);\n\n // Forward response to the CLI client using its original request ID\n if (pending.clientWs.readyState === WebSocket.OPEN) {\n pending.clientWs.send(\n JSON.stringify({\n id: pending.clientId,\n type: \"rpc_response\",\n success: msg.success,\n data: msg.data,\n error: msg.error,\n address: msg.address,\n }),\n );\n }\n break;\n }\n\n case \"pong\":\n break;\n }\n });\n\n ws.on(\"close\", () => {\n if (browserWs === ws) {\n browserReady = false;\n browserWs = null;\n\n // Fail in-flight requests immediately — they can't complete without a browser.\n // The next CLI request will trigger reopenBrowser() automatically.\n for (const [, pending] of pendingRequests) {\n clearTimeout(pending.timer);\n if (pending.clientWs.readyState === WebSocket.OPEN) {\n pending.clientWs.send(\n JSON.stringify({\n id: pending.clientId,\n type: \"rpc_response\",\n success: false,\n error: {\n code: -32001,\n message:\n \"Browser tab was closed. Reopening — please retry in a few seconds.\",\n },\n }),\n );\n }\n }\n pendingRequests.clear();\n }\n });\n}\n\n// ── CLI client connection ──────────────────────────────────────────\n\nfunction handleCliConnection(ws: WebSocket): void {\n // Enforce max CLI client connections\n if (cliClients.size >= MAX_CLI_CLIENTS) {\n ws.close(4002, \"Too many CLI connections\");\n return;\n }\n\n resetIdleTimer();\n cliClients.add(ws);\n\n // Send current status\n ws.send(\n JSON.stringify({\n type: \"status\",\n browserConnected: browserReady,\n }),\n );\n\n // Auto-reopen browser if it's disconnected\n if (!browserReady) {\n reopenBrowser();\n }\n\n ws.on(\"close\", () => {\n cliClients.delete(ws);\n });\n\n ws.on(\"message\", (data) => {\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(data.toString()) as Record<string, unknown>;\n } catch {\n return;\n }\n\n if (msg.type === \"rpc_request\") {\n resetIdleTimer();\n handleCliRpcRequest(ws, msg);\n } else if (msg.type === \"shutdown\") {\n cleanup();\n process.exit(0);\n }\n });\n}\n\nfunction handleCliRpcRequest(\n clientWs: WebSocket,\n msg: Record<string, unknown>,\n): void {\n const clientId = msg.id as string;\n\n // Rate limit per client\n if (isRateLimited(clientWs)) {\n clientWs.send(\n JSON.stringify({\n id: clientId,\n type: \"rpc_response\",\n success: false,\n error: {\n code: -32005,\n message: \"Rate limited. Too many requests — try again shortly.\",\n },\n }),\n );\n return;\n }\n\n // Cap pending requests to prevent memory exhaustion\n if (pendingRequests.size >= MAX_PENDING_REQUESTS) {\n clientWs.send(\n JSON.stringify({\n id: clientId,\n type: \"rpc_response\",\n success: false,\n error: {\n code: -32006,\n message:\n \"Too many pending requests. Wait for current requests to complete.\",\n },\n }),\n );\n return;\n }\n\n if (!browserReady || !browserWs || browserWs.readyState !== WebSocket.OPEN) {\n // Attempt to reopen browser and tell CLI to wait\n reopenBrowser();\n clientWs.send(\n JSON.stringify({\n id: clientId,\n type: \"rpc_response\",\n success: false,\n error: {\n code: -32001,\n message:\n \"Browser tab was closed. Reopening — please retry in a few seconds.\",\n },\n }),\n );\n return;\n }\n\n // Generate a daemon-internal ID and forward to browser\n const daemonId = crypto.randomUUID();\n\n const timer = setTimeout(() => {\n pendingRequests.delete(daemonId);\n if (clientWs.readyState === WebSocket.OPEN) {\n clientWs.send(\n JSON.stringify({\n id: clientId,\n type: \"rpc_response\",\n success: false,\n error: {\n code: -32000,\n message: `Request timed out after ${timeout / 1000}s`,\n },\n }),\n );\n }\n }, timeout);\n\n pendingRequests.set(daemonId, { clientWs, clientId, timer });\n\n browserWs.send(\n JSON.stringify({\n id: daemonId,\n type: \"rpc_request\",\n method: msg.method,\n params: msg.params,\n }),\n );\n}\n\n// ── Build bridge URL ───────────────────────────────────────────────\n\nfunction buildBridgeUrl(port: number): string {\n const url = new URL(\"/cli-bridge\", args.keysUrl);\n url.searchParams.set(\"wsPort\", String(port));\n // Only wsPort in query params — all config (apiKey, chainId, ens, paymasterUrl)\n // is sent over the authenticated WebSocket after connection\n url.hash = `token=${encodeURIComponent(token)}`;\n return url.toString();\n}\n"]}
|
|
1
|
+
{"version":3,"sources":["../../src/lib/ws-daemon.ts"],"names":[],"mappings":"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","file":"ws-daemon.js","sourcesContent":["#!/usr/bin/env node\n/**\n * WS Bridge Daemon\n *\n * Long-lived background process that:\n * 1. Starts a WebSocket server on 127.0.0.1:{random_port}\n * 2. Opens browser to keys.jaw.id/cli-bridge\n * 3. Accepts WebSocket connections from the browser (SDK) and CLI clients\n * 4. Routes RPC requests from CLI clients to the browser SDK\n * 5. Writes connection info to ~/.jaw/bridge.json\n *\n * The daemon stays alive across CLI commands so the browser SDK instance\n * (and its in-memory state) persists.\n */\n\nimport * as crypto from \"node:crypto\";\nimport * as fs from \"node:fs\";\nimport * as http from \"node:http\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { WebSocketServer, WebSocket } from \"ws\";\n\nconst JAW_DIR = path.join(os.homedir(), \".jaw\");\nconst BRIDGE_PATH = path.join(JAW_DIR, \"bridge.json\");\nconst HEARTBEAT_INTERVAL_MS = 30_000;\nconst IDLE_TIMEOUT_MS = 30 * 60 * 1000; // 30 min idle → auto-shutdown\n\n// ── Limits ────────────────────────────────────────────────────────\nconst MAX_PENDING_REQUESTS = 100;\nconst MAX_CLI_CLIENTS = 50;\nconst RATE_LIMIT_WINDOW_MS = 60_000; // 1 minute\nconst RATE_LIMIT_MAX_REQUESTS = 60; // 60 requests per minute per client\n\n// ── Types ──────────────────────────────────────────────────────────\n\ninterface BridgeInfo {\n port: number;\n token: string;\n pid: number;\n startedAt: string;\n}\n\ninterface PendingRequest {\n clientWs: WebSocket;\n clientId: string;\n timer: ReturnType<typeof setTimeout>;\n}\n\ninterface RateLimitEntry {\n timestamps: number[];\n}\n\n// ── keysUrl validation (duplicated from validation.ts for standalone daemon) ─\n\nfunction isValidKeysUrl(url: string): boolean {\n try {\n const parsed = new URL(url);\n const isTrustedHost =\n parsed.hostname.endsWith(\".jaw.id\") ||\n parsed.hostname === \"jaw.id\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n const isSecure =\n parsed.protocol === \"https:\" ||\n parsed.hostname === \"localhost\" ||\n parsed.hostname === \"127.0.0.1\";\n return isTrustedHost && isSecure;\n } catch {\n return false;\n }\n}\n\n// ── Parse args from parent process ─────────────────────────────────\n\nconst args = JSON.parse(process.argv[2] ?? \"{}\") as {\n keysUrl: string;\n chainId: number;\n ens?: string;\n paymasterUrl?: string;\n timeout?: number;\n};\n\n// Validate keysUrl before proceeding\nif (!isValidKeysUrl(args.keysUrl)) {\n process.stderr.write(\n `Untrusted keysUrl: ${args.keysUrl}. Must be a *.jaw.id domain (HTTPS) or localhost.\\n`,\n );\n process.exit(1);\n}\n\n// API key is passed via env var to avoid exposure in `ps aux` output\nconst apiKey = process.env.JAW_DAEMON_API_KEY ?? \"\";\n// Clear from environment immediately after reading\ndelete process.env.JAW_DAEMON_API_KEY;\n\nconst token = crypto.randomUUID();\nconst timeout = args.timeout ?? 120_000;\n\n// Track pending requests: daemonRequestId -> { clientWs, clientId }\nconst pendingRequests = new Map<string, PendingRequest>();\n\n// Browser SDK connection\nlet browserWs: WebSocket | null = null;\nlet browserReady = false;\nlet idleTimer: ReturnType<typeof setTimeout> | null = null;\nlet heartbeat: ReturnType<typeof setInterval> | null = null;\n\n// Browser reopen state\nlet lastBrowserOpenTime = 0;\nconst BROWSER_REOPEN_COOLDOWN_MS = 5_000; // Min 5s between reopen attempts\nlet serverPort: number | null = null; // Stored after server starts listening\n\n// Track connected CLI clients (to notify when browser connects)\nconst cliClients = new Set<WebSocket>();\n\n// Per-client rate limiting\nconst rateLimits = new WeakMap<WebSocket, RateLimitEntry>();\n\n// ── Rate limiting ─────────────────────────────────────────────────\n\nfunction isRateLimited(ws: WebSocket): boolean {\n const now = Date.now();\n let entry = rateLimits.get(ws);\n if (!entry) {\n entry = { timestamps: [] };\n rateLimits.set(ws, entry);\n }\n\n // Remove timestamps outside the window\n entry.timestamps = entry.timestamps.filter(\n (t) => now - t < RATE_LIMIT_WINDOW_MS,\n );\n\n if (entry.timestamps.length >= RATE_LIMIT_MAX_REQUESTS) {\n return true;\n }\n\n entry.timestamps.push(now);\n return false;\n}\n\n// ── Browser reopen ────────────────────────────────────────────────\n\nasync function reopenBrowser(): Promise<void> {\n if (!serverPort) return;\n if (browserReady || browserWs?.readyState === WebSocket.OPEN) return;\n\n const now = Date.now();\n if (now - lastBrowserOpenTime < BROWSER_REOPEN_COOLDOWN_MS) return;\n\n lastBrowserOpenTime = now;\n const bridgeUrl = buildBridgeUrl(serverPort);\n try {\n const { default: open } = await import(\"open\");\n await open(bridgeUrl);\n } catch {\n // Best-effort; CLI will time out if browser never connects\n }\n}\n\n// ── Idle management ────────────────────────────────────────────────\n\nfunction resetIdleTimer(): void {\n if (idleTimer) clearTimeout(idleTimer);\n idleTimer = setTimeout(() => {\n cleanup();\n process.exit(0);\n }, IDLE_TIMEOUT_MS);\n}\n\n// ── Cleanup ────────────────────────────────────────────────────────\n\nfunction cleanup(): void {\n if (heartbeat) clearInterval(heartbeat);\n // Notify browser to close the tab\n if (browserWs?.readyState === WebSocket.OPEN) {\n browserWs.send(JSON.stringify({ type: \"shutdown\" }));\n }\n try {\n if (fs.existsSync(BRIDGE_PATH)) fs.unlinkSync(BRIDGE_PATH);\n } catch {\n // ignore\n }\n}\n\nprocess.on(\"SIGTERM\", () => {\n cleanup();\n process.exit(0);\n});\nprocess.on(\"SIGINT\", () => {\n cleanup();\n process.exit(0);\n});\nprocess.on(\"exit\", cleanup);\n\n// ── Start WebSocket server ─────────────────────────────────────────\n//\n// We create an HTTP server manually so we can respond to Private Network\n// Access (PNA) preflight requests. Modern Chrome (130+) sends an OPTIONS\n// preflight with Access-Control-Request-Private-Network before allowing\n// WebSocket connections from public HTTPS origins (keys.jaw.id) to\n// private network addresses (127.0.0.1). Without a proper preflight\n// response the browser refuses the WebSocket handshake entirely.\n\nconst MAX_PAYLOAD_BYTES = 1_048_576; // 1 MB – plenty for any RPC payload\n\n// Derive the trusted origin from the configured keysUrl so we only allow\n// PNA preflight from the expected browser page (e.g. https://keys.jaw.id),\n// not from arbitrary websites scanning localhost.\nconst trustedOrigin = new URL(args.keysUrl).origin;\n\nfunction isTrustedOrigin(origin: string): boolean {\n if (origin === trustedOrigin) return true;\n // Allow localhost/loopback for development\n try {\n const parsed = new URL(origin);\n return parsed.hostname === \"localhost\" || parsed.hostname === \"127.0.0.1\";\n } catch {\n return false;\n }\n}\n\nconst httpServer = http.createServer((req, res) => {\n // Handle PNA preflight (OPTIONS) requests\n if (req.method === \"OPTIONS\") {\n const origin = req.headers.origin ?? \"\";\n if (!isTrustedOrigin(origin)) {\n res.writeHead(403, { \"Content-Type\": \"text/plain\" });\n res.end(\"Forbidden\");\n return;\n }\n res.writeHead(204, {\n \"Access-Control-Allow-Origin\": origin,\n \"Access-Control-Allow-Methods\": \"GET, OPTIONS\",\n \"Access-Control-Allow-Headers\": \"Content-Type\",\n \"Access-Control-Allow-Private-Network\": \"true\",\n \"Access-Control-Max-Age\": \"86400\",\n });\n res.end();\n return;\n }\n // Reject all other HTTP requests\n res.writeHead(426, { \"Content-Type\": \"text/plain\" });\n res.end(\"Upgrade required\");\n});\n\nconst wss = new WebSocketServer({\n server: httpServer,\n maxPayload: MAX_PAYLOAD_BYTES,\n});\n\n// Inject PNA + CORS headers into the WebSocket 101 upgrade response.\n// Chrome's PNA enforcement checks these headers on the upgrade response\n// in addition to the preflight.\nwss.on(\"headers\", (headers, req) => {\n const origin = req.headers.origin;\n if (origin && isTrustedOrigin(origin)) {\n headers.push(`Access-Control-Allow-Origin: ${origin}`);\n headers.push(\"Access-Control-Allow-Private-Network: true\");\n }\n});\n\nhttpServer.listen(0, \"127.0.0.1\", async () => {\n const addr = httpServer.address();\n if (typeof addr === \"string\" || !addr) {\n process.exit(1);\n }\n\n const port = addr.port;\n serverPort = port;\n\n // Write bridge info\n fs.mkdirSync(JAW_DIR, { recursive: true, mode: 0o700 });\n const info: BridgeInfo = {\n port,\n token,\n pid: process.pid,\n startedAt: new Date().toISOString(),\n };\n fs.writeFileSync(BRIDGE_PATH, JSON.stringify(info, null, 2) + \"\\n\", {\n encoding: \"utf-8\",\n mode: 0o600,\n });\n\n // Open browser\n lastBrowserOpenTime = Date.now();\n const bridgeUrl = buildBridgeUrl(port);\n const { default: open } = await import(\"open\");\n await open(bridgeUrl);\n\n resetIdleTimer();\n});\n\nwss.on(\"connection\", (ws, req) => {\n const url = new URL(req.url ?? \"\", \"http://127.0.0.1\");\n const urlToken = url.searchParams.get(\"token\");\n const role = url.searchParams.get(\"role\"); // \"browser\" or \"cli\"\n\n if (urlToken !== token) {\n ws.close(4001, \"Invalid token\");\n return;\n }\n\n if (role === \"browser\") {\n handleBrowserConnection(ws);\n } else {\n handleCliConnection(ws);\n }\n});\n\n// ── Browser connection ─────────────────────────────────────────────\n\nfunction handleBrowserConnection(ws: WebSocket): void {\n if (browserWs) {\n browserWs.close(4000, \"Replaced by new browser connection\");\n }\n browserWs = ws;\n browserReady = false;\n\n // Send config over the authenticated WebSocket rather than via URL\n ws.send(\n JSON.stringify({\n type: \"init\",\n apiKey,\n chainId: args.chainId,\n ens: args.ens,\n paymasterUrl: args.paymasterUrl,\n }),\n );\n\n ws.on(\"message\", (data) => {\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(data.toString()) as Record<string, unknown>;\n } catch {\n return;\n }\n\n switch (msg.type) {\n case \"ready\":\n browserReady = true;\n // Start heartbeat\n if (heartbeat) clearInterval(heartbeat);\n heartbeat = setInterval(() => {\n if (browserWs?.readyState === WebSocket.OPEN) {\n browserWs.send(JSON.stringify({ type: \"ping\" }));\n }\n }, HEARTBEAT_INTERVAL_MS);\n // Notify any waiting CLI clients that browser is now connected\n for (const cli of cliClients) {\n if (cli.readyState === WebSocket.OPEN) {\n cli.send(JSON.stringify({ type: \"browser_connected\" }));\n }\n }\n break;\n\n case \"rpc_response\": {\n const id = msg.id as string;\n const pending = pendingRequests.get(id);\n if (!pending) return;\n\n clearTimeout(pending.timer);\n pendingRequests.delete(id);\n\n // Forward response to the CLI client using its original request ID\n if (pending.clientWs.readyState === WebSocket.OPEN) {\n pending.clientWs.send(\n JSON.stringify({\n id: pending.clientId,\n type: \"rpc_response\",\n success: msg.success,\n data: msg.data,\n error: msg.error,\n address: msg.address,\n }),\n );\n }\n break;\n }\n\n case \"pong\":\n break;\n }\n });\n\n ws.on(\"close\", () => {\n if (browserWs === ws) {\n browserReady = false;\n browserWs = null;\n\n // Fail in-flight requests immediately — they can't complete without a browser.\n // The next CLI request will trigger reopenBrowser() automatically.\n for (const [, pending] of pendingRequests) {\n clearTimeout(pending.timer);\n if (pending.clientWs.readyState === WebSocket.OPEN) {\n pending.clientWs.send(\n JSON.stringify({\n id: pending.clientId,\n type: \"rpc_response\",\n success: false,\n error: {\n code: -32001,\n message:\n \"Browser tab was closed. Reopening — please retry in a few seconds.\",\n },\n }),\n );\n }\n }\n pendingRequests.clear();\n }\n });\n}\n\n// ── CLI client connection ──────────────────────────────────────────\n\nfunction handleCliConnection(ws: WebSocket): void {\n // Enforce max CLI client connections\n if (cliClients.size >= MAX_CLI_CLIENTS) {\n ws.close(4002, \"Too many CLI connections\");\n return;\n }\n\n resetIdleTimer();\n cliClients.add(ws);\n\n // Send current status\n ws.send(\n JSON.stringify({\n type: \"status\",\n browserConnected: browserReady,\n }),\n );\n\n // Auto-reopen browser if it's disconnected\n if (!browserReady) {\n reopenBrowser();\n }\n\n ws.on(\"close\", () => {\n cliClients.delete(ws);\n });\n\n ws.on(\"message\", (data) => {\n let msg: Record<string, unknown>;\n try {\n msg = JSON.parse(data.toString()) as Record<string, unknown>;\n } catch {\n return;\n }\n\n if (msg.type === \"rpc_request\") {\n resetIdleTimer();\n handleCliRpcRequest(ws, msg);\n } else if (msg.type === \"shutdown\") {\n cleanup();\n process.exit(0);\n }\n });\n}\n\nfunction handleCliRpcRequest(\n clientWs: WebSocket,\n msg: Record<string, unknown>,\n): void {\n const clientId = msg.id as string;\n\n // Rate limit per client\n if (isRateLimited(clientWs)) {\n clientWs.send(\n JSON.stringify({\n id: clientId,\n type: \"rpc_response\",\n success: false,\n error: {\n code: -32005,\n message: \"Rate limited. Too many requests — try again shortly.\",\n },\n }),\n );\n return;\n }\n\n // Cap pending requests to prevent memory exhaustion\n if (pendingRequests.size >= MAX_PENDING_REQUESTS) {\n clientWs.send(\n JSON.stringify({\n id: clientId,\n type: \"rpc_response\",\n success: false,\n error: {\n code: -32006,\n message:\n \"Too many pending requests. Wait for current requests to complete.\",\n },\n }),\n );\n return;\n }\n\n if (!browserReady || !browserWs || browserWs.readyState !== WebSocket.OPEN) {\n // Attempt to reopen browser and tell CLI to wait\n reopenBrowser();\n clientWs.send(\n JSON.stringify({\n id: clientId,\n type: \"rpc_response\",\n success: false,\n error: {\n code: -32001,\n message:\n \"Browser tab was closed. Reopening — please retry in a few seconds.\",\n },\n }),\n );\n return;\n }\n\n // Generate a daemon-internal ID and forward to browser\n const daemonId = crypto.randomUUID();\n\n const timer = setTimeout(() => {\n pendingRequests.delete(daemonId);\n if (clientWs.readyState === WebSocket.OPEN) {\n clientWs.send(\n JSON.stringify({\n id: clientId,\n type: \"rpc_response\",\n success: false,\n error: {\n code: -32000,\n message: `Request timed out after ${timeout / 1000}s`,\n },\n }),\n );\n }\n }, timeout);\n\n pendingRequests.set(daemonId, { clientWs, clientId, timer });\n\n browserWs.send(\n JSON.stringify({\n id: daemonId,\n type: \"rpc_request\",\n method: msg.method,\n params: msg.params,\n }),\n );\n}\n\n// ── Build bridge URL ───────────────────────────────────────────────\n\nfunction buildBridgeUrl(port: number): string {\n const url = new URL(\"/cli-bridge\", args.keysUrl);\n url.searchParams.set(\"wsPort\", String(port));\n // Only wsPort in query params — all config (apiKey, chainId, ens, paymasterUrl)\n // is sent over the authenticated WebSocket after connection\n url.hash = `token=${encodeURIComponent(token)}`;\n return url.toString();\n}\n"]}
|