@jaw.id/cli 0.0.3 → 0.0.7
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/base-command.js +1 -2
- package/dist/base-command.js.map +1 -1
- package/dist/commands/config/set.js +19 -3
- package/dist/commands/config/set.js.map +1 -1
- package/dist/commands/config/show.js +1 -2
- package/dist/commands/config/show.js.map +1 -1
- package/dist/commands/disconnect.js +439 -36
- package/dist/commands/disconnect.js.map +1 -1
- package/dist/commands/mcp/index.js +486 -251
- package/dist/commands/mcp/index.js.map +1 -1
- package/dist/commands/rpc/call.js +429 -204
- package/dist/commands/rpc/call.js.map +1 -1
- package/dist/commands/version.js +134 -0
- package/dist/commands/version.js.map +1 -0
- package/dist/index.js +455 -203
- package/dist/index.js.map +1 -1
- package/dist/lib/bridge-singleton.js +457 -205
- package/dist/lib/bridge-singleton.js.map +1 -1
- package/dist/lib/config.js +17 -2
- package/dist/lib/config.js.map +1 -1
- package/dist/lib/crypto.js +74 -0
- package/dist/lib/crypto.js.map +1 -0
- package/dist/lib/paths.js +1 -2
- package/dist/lib/paths.js.map +1 -1
- package/dist/lib/session-store.js +1 -2
- package/dist/lib/session-store.js.map +1 -1
- package/dist/lib/validation.js +12 -1
- package/dist/lib/validation.js.map +1 -1
- package/dist/lib/ws-bridge.js +321 -59
- package/dist/lib/ws-bridge.js.map +1 -1
- package/dist/mcp/handlers/config.js +19 -4
- package/dist/mcp/handlers/config.js.map +1 -1
- package/dist/mcp/handlers/daemon.js +447 -72
- package/dist/mcp/handlers/daemon.js.map +1 -1
- package/dist/mcp/handlers/rpc.js +442 -235
- package/dist/mcp/handlers/rpc.js.map +1 -1
- package/dist/mcp/server.js +486 -251
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.js +1 -1
- package/dist/mcp/tools.js.map +1 -1
- package/oclif.manifest.json +70 -2
- package/package.json +5 -1
- package/dist/lib/ws-daemon.js +0 -382
- package/dist/lib/ws-daemon.js.map +0 -1
package/dist/lib/ws-daemon.js
DELETED
|
@@ -1,382 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
import * as crypto from 'crypto';
|
|
3
|
-
import * as fs from 'fs';
|
|
4
|
-
import * as http from 'http';
|
|
5
|
-
import * as path from 'path';
|
|
6
|
-
import * as os from 'os';
|
|
7
|
-
import { WebSocketServer, WebSocket } from 'ws';
|
|
8
|
-
|
|
9
|
-
var JAW_DIR = path.join(os.homedir(), ".jaw");
|
|
10
|
-
var BRIDGE_PATH = path.join(JAW_DIR, "bridge.json");
|
|
11
|
-
var HEARTBEAT_INTERVAL_MS = 3e4;
|
|
12
|
-
var IDLE_TIMEOUT_MS = 30 * 60 * 1e3;
|
|
13
|
-
var MAX_PENDING_REQUESTS = 100;
|
|
14
|
-
var MAX_CLI_CLIENTS = 50;
|
|
15
|
-
var RATE_LIMIT_WINDOW_MS = 6e4;
|
|
16
|
-
var RATE_LIMIT_MAX_REQUESTS = 60;
|
|
17
|
-
function isValidKeysUrl(url) {
|
|
18
|
-
try {
|
|
19
|
-
const parsed = new URL(url);
|
|
20
|
-
const isTrustedHost = parsed.hostname.endsWith(".jaw.id") || parsed.hostname === "jaw.id" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
21
|
-
const isSecure = parsed.protocol === "https:" || parsed.hostname === "localhost" || parsed.hostname === "127.0.0.1";
|
|
22
|
-
return isTrustedHost && isSecure;
|
|
23
|
-
} catch {
|
|
24
|
-
return false;
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
var args = JSON.parse(process.argv[2] ?? "{}");
|
|
28
|
-
if (!isValidKeysUrl(args.keysUrl)) {
|
|
29
|
-
process.stderr.write(
|
|
30
|
-
`Untrusted keysUrl: ${args.keysUrl}. Must be a *.jaw.id domain (HTTPS) or localhost.
|
|
31
|
-
`
|
|
32
|
-
);
|
|
33
|
-
process.exit(1);
|
|
34
|
-
}
|
|
35
|
-
var apiKey = process.env.JAW_DAEMON_API_KEY ?? "";
|
|
36
|
-
delete process.env.JAW_DAEMON_API_KEY;
|
|
37
|
-
var token = crypto.randomUUID();
|
|
38
|
-
var timeout = args.timeout ?? 12e4;
|
|
39
|
-
var pendingRequests = /* @__PURE__ */ new Map();
|
|
40
|
-
var browserWs = null;
|
|
41
|
-
var browserReady = false;
|
|
42
|
-
var idleTimer = null;
|
|
43
|
-
var heartbeat = null;
|
|
44
|
-
var lastBrowserOpenTime = 0;
|
|
45
|
-
var BROWSER_REOPEN_COOLDOWN_MS = 5e3;
|
|
46
|
-
var serverPort = null;
|
|
47
|
-
var cliClients = /* @__PURE__ */ new Set();
|
|
48
|
-
var rateLimits = /* @__PURE__ */ new WeakMap();
|
|
49
|
-
function isRateLimited(ws) {
|
|
50
|
-
const now = Date.now();
|
|
51
|
-
let entry = rateLimits.get(ws);
|
|
52
|
-
if (!entry) {
|
|
53
|
-
entry = { timestamps: [] };
|
|
54
|
-
rateLimits.set(ws, entry);
|
|
55
|
-
}
|
|
56
|
-
entry.timestamps = entry.timestamps.filter(
|
|
57
|
-
(t) => now - t < RATE_LIMIT_WINDOW_MS
|
|
58
|
-
);
|
|
59
|
-
if (entry.timestamps.length >= RATE_LIMIT_MAX_REQUESTS) {
|
|
60
|
-
return true;
|
|
61
|
-
}
|
|
62
|
-
entry.timestamps.push(now);
|
|
63
|
-
return false;
|
|
64
|
-
}
|
|
65
|
-
async function reopenBrowser() {
|
|
66
|
-
if (!serverPort) return;
|
|
67
|
-
if (browserReady || browserWs?.readyState === WebSocket.OPEN) return;
|
|
68
|
-
const now = Date.now();
|
|
69
|
-
if (now - lastBrowserOpenTime < BROWSER_REOPEN_COOLDOWN_MS) return;
|
|
70
|
-
lastBrowserOpenTime = now;
|
|
71
|
-
const bridgeUrl = buildBridgeUrl(serverPort);
|
|
72
|
-
try {
|
|
73
|
-
const { default: open } = await import('open');
|
|
74
|
-
await open(bridgeUrl);
|
|
75
|
-
} catch {
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
function resetIdleTimer() {
|
|
79
|
-
if (idleTimer) clearTimeout(idleTimer);
|
|
80
|
-
idleTimer = setTimeout(() => {
|
|
81
|
-
cleanup();
|
|
82
|
-
process.exit(0);
|
|
83
|
-
}, IDLE_TIMEOUT_MS);
|
|
84
|
-
}
|
|
85
|
-
function cleanup() {
|
|
86
|
-
if (heartbeat) clearInterval(heartbeat);
|
|
87
|
-
if (browserWs?.readyState === WebSocket.OPEN) {
|
|
88
|
-
browserWs.send(JSON.stringify({ type: "shutdown" }));
|
|
89
|
-
}
|
|
90
|
-
try {
|
|
91
|
-
if (fs.existsSync(BRIDGE_PATH)) fs.unlinkSync(BRIDGE_PATH);
|
|
92
|
-
} catch {
|
|
93
|
-
}
|
|
94
|
-
}
|
|
95
|
-
process.on("SIGTERM", () => {
|
|
96
|
-
cleanup();
|
|
97
|
-
process.exit(0);
|
|
98
|
-
});
|
|
99
|
-
process.on("SIGINT", () => {
|
|
100
|
-
cleanup();
|
|
101
|
-
process.exit(0);
|
|
102
|
-
});
|
|
103
|
-
process.on("exit", cleanup);
|
|
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
|
-
});
|
|
136
|
-
var wss = new WebSocketServer({
|
|
137
|
-
server: httpServer,
|
|
138
|
-
maxPayload: MAX_PAYLOAD_BYTES
|
|
139
|
-
});
|
|
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();
|
|
149
|
-
if (typeof addr === "string" || !addr) {
|
|
150
|
-
process.exit(1);
|
|
151
|
-
}
|
|
152
|
-
const port = addr.port;
|
|
153
|
-
serverPort = port;
|
|
154
|
-
fs.mkdirSync(JAW_DIR, { recursive: true, mode: 448 });
|
|
155
|
-
const info = {
|
|
156
|
-
port,
|
|
157
|
-
token,
|
|
158
|
-
pid: process.pid,
|
|
159
|
-
startedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
160
|
-
};
|
|
161
|
-
fs.writeFileSync(BRIDGE_PATH, JSON.stringify(info, null, 2) + "\n", {
|
|
162
|
-
encoding: "utf-8",
|
|
163
|
-
mode: 384
|
|
164
|
-
});
|
|
165
|
-
lastBrowserOpenTime = Date.now();
|
|
166
|
-
const bridgeUrl = buildBridgeUrl(port);
|
|
167
|
-
const { default: open } = await import('open');
|
|
168
|
-
await open(bridgeUrl);
|
|
169
|
-
resetIdleTimer();
|
|
170
|
-
});
|
|
171
|
-
wss.on("connection", (ws, req) => {
|
|
172
|
-
const url = new URL(req.url ?? "", "http://127.0.0.1");
|
|
173
|
-
const urlToken = url.searchParams.get("token");
|
|
174
|
-
const role = url.searchParams.get("role");
|
|
175
|
-
if (urlToken !== token) {
|
|
176
|
-
ws.close(4001, "Invalid token");
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (role === "browser") {
|
|
180
|
-
handleBrowserConnection(ws);
|
|
181
|
-
} else {
|
|
182
|
-
handleCliConnection(ws);
|
|
183
|
-
}
|
|
184
|
-
});
|
|
185
|
-
function handleBrowserConnection(ws) {
|
|
186
|
-
if (browserWs) {
|
|
187
|
-
browserWs.close(4e3, "Replaced by new browser connection");
|
|
188
|
-
}
|
|
189
|
-
browserWs = ws;
|
|
190
|
-
browserReady = false;
|
|
191
|
-
ws.send(
|
|
192
|
-
JSON.stringify({
|
|
193
|
-
type: "init",
|
|
194
|
-
apiKey,
|
|
195
|
-
chainId: args.chainId,
|
|
196
|
-
ens: args.ens,
|
|
197
|
-
paymasterUrl: args.paymasterUrl
|
|
198
|
-
})
|
|
199
|
-
);
|
|
200
|
-
ws.on("message", (data) => {
|
|
201
|
-
let msg;
|
|
202
|
-
try {
|
|
203
|
-
msg = JSON.parse(data.toString());
|
|
204
|
-
} catch {
|
|
205
|
-
return;
|
|
206
|
-
}
|
|
207
|
-
switch (msg.type) {
|
|
208
|
-
case "ready":
|
|
209
|
-
browserReady = true;
|
|
210
|
-
if (heartbeat) clearInterval(heartbeat);
|
|
211
|
-
heartbeat = setInterval(() => {
|
|
212
|
-
if (browserWs?.readyState === WebSocket.OPEN) {
|
|
213
|
-
browserWs.send(JSON.stringify({ type: "ping" }));
|
|
214
|
-
}
|
|
215
|
-
}, HEARTBEAT_INTERVAL_MS);
|
|
216
|
-
for (const cli of cliClients) {
|
|
217
|
-
if (cli.readyState === WebSocket.OPEN) {
|
|
218
|
-
cli.send(JSON.stringify({ type: "browser_connected" }));
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
break;
|
|
222
|
-
case "rpc_response": {
|
|
223
|
-
const id = msg.id;
|
|
224
|
-
const pending = pendingRequests.get(id);
|
|
225
|
-
if (!pending) return;
|
|
226
|
-
clearTimeout(pending.timer);
|
|
227
|
-
pendingRequests.delete(id);
|
|
228
|
-
if (pending.clientWs.readyState === WebSocket.OPEN) {
|
|
229
|
-
pending.clientWs.send(
|
|
230
|
-
JSON.stringify({
|
|
231
|
-
id: pending.clientId,
|
|
232
|
-
type: "rpc_response",
|
|
233
|
-
success: msg.success,
|
|
234
|
-
data: msg.data,
|
|
235
|
-
error: msg.error,
|
|
236
|
-
address: msg.address
|
|
237
|
-
})
|
|
238
|
-
);
|
|
239
|
-
}
|
|
240
|
-
break;
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
});
|
|
244
|
-
ws.on("close", () => {
|
|
245
|
-
if (browserWs === ws) {
|
|
246
|
-
browserReady = false;
|
|
247
|
-
browserWs = null;
|
|
248
|
-
for (const [, pending] of pendingRequests) {
|
|
249
|
-
clearTimeout(pending.timer);
|
|
250
|
-
if (pending.clientWs.readyState === WebSocket.OPEN) {
|
|
251
|
-
pending.clientWs.send(
|
|
252
|
-
JSON.stringify({
|
|
253
|
-
id: pending.clientId,
|
|
254
|
-
type: "rpc_response",
|
|
255
|
-
success: false,
|
|
256
|
-
error: {
|
|
257
|
-
code: -32001,
|
|
258
|
-
message: "Browser tab was closed. Reopening \u2014 please retry in a few seconds."
|
|
259
|
-
}
|
|
260
|
-
})
|
|
261
|
-
);
|
|
262
|
-
}
|
|
263
|
-
}
|
|
264
|
-
pendingRequests.clear();
|
|
265
|
-
}
|
|
266
|
-
});
|
|
267
|
-
}
|
|
268
|
-
function handleCliConnection(ws) {
|
|
269
|
-
if (cliClients.size >= MAX_CLI_CLIENTS) {
|
|
270
|
-
ws.close(4002, "Too many CLI connections");
|
|
271
|
-
return;
|
|
272
|
-
}
|
|
273
|
-
resetIdleTimer();
|
|
274
|
-
cliClients.add(ws);
|
|
275
|
-
ws.send(
|
|
276
|
-
JSON.stringify({
|
|
277
|
-
type: "status",
|
|
278
|
-
browserConnected: browserReady
|
|
279
|
-
})
|
|
280
|
-
);
|
|
281
|
-
if (!browserReady) {
|
|
282
|
-
reopenBrowser();
|
|
283
|
-
}
|
|
284
|
-
ws.on("close", () => {
|
|
285
|
-
cliClients.delete(ws);
|
|
286
|
-
});
|
|
287
|
-
ws.on("message", (data) => {
|
|
288
|
-
let msg;
|
|
289
|
-
try {
|
|
290
|
-
msg = JSON.parse(data.toString());
|
|
291
|
-
} catch {
|
|
292
|
-
return;
|
|
293
|
-
}
|
|
294
|
-
if (msg.type === "rpc_request") {
|
|
295
|
-
resetIdleTimer();
|
|
296
|
-
handleCliRpcRequest(ws, msg);
|
|
297
|
-
} else if (msg.type === "shutdown") {
|
|
298
|
-
cleanup();
|
|
299
|
-
process.exit(0);
|
|
300
|
-
}
|
|
301
|
-
});
|
|
302
|
-
}
|
|
303
|
-
function handleCliRpcRequest(clientWs, msg) {
|
|
304
|
-
const clientId = msg.id;
|
|
305
|
-
if (isRateLimited(clientWs)) {
|
|
306
|
-
clientWs.send(
|
|
307
|
-
JSON.stringify({
|
|
308
|
-
id: clientId,
|
|
309
|
-
type: "rpc_response",
|
|
310
|
-
success: false,
|
|
311
|
-
error: {
|
|
312
|
-
code: -32005,
|
|
313
|
-
message: "Rate limited. Too many requests \u2014 try again shortly."
|
|
314
|
-
}
|
|
315
|
-
})
|
|
316
|
-
);
|
|
317
|
-
return;
|
|
318
|
-
}
|
|
319
|
-
if (pendingRequests.size >= MAX_PENDING_REQUESTS) {
|
|
320
|
-
clientWs.send(
|
|
321
|
-
JSON.stringify({
|
|
322
|
-
id: clientId,
|
|
323
|
-
type: "rpc_response",
|
|
324
|
-
success: false,
|
|
325
|
-
error: {
|
|
326
|
-
code: -32006,
|
|
327
|
-
message: "Too many pending requests. Wait for current requests to complete."
|
|
328
|
-
}
|
|
329
|
-
})
|
|
330
|
-
);
|
|
331
|
-
return;
|
|
332
|
-
}
|
|
333
|
-
if (!browserReady || !browserWs || browserWs.readyState !== WebSocket.OPEN) {
|
|
334
|
-
reopenBrowser();
|
|
335
|
-
clientWs.send(
|
|
336
|
-
JSON.stringify({
|
|
337
|
-
id: clientId,
|
|
338
|
-
type: "rpc_response",
|
|
339
|
-
success: false,
|
|
340
|
-
error: {
|
|
341
|
-
code: -32001,
|
|
342
|
-
message: "Browser tab was closed. Reopening \u2014 please retry in a few seconds."
|
|
343
|
-
}
|
|
344
|
-
})
|
|
345
|
-
);
|
|
346
|
-
return;
|
|
347
|
-
}
|
|
348
|
-
const daemonId = crypto.randomUUID();
|
|
349
|
-
const timer = setTimeout(() => {
|
|
350
|
-
pendingRequests.delete(daemonId);
|
|
351
|
-
if (clientWs.readyState === WebSocket.OPEN) {
|
|
352
|
-
clientWs.send(
|
|
353
|
-
JSON.stringify({
|
|
354
|
-
id: clientId,
|
|
355
|
-
type: "rpc_response",
|
|
356
|
-
success: false,
|
|
357
|
-
error: {
|
|
358
|
-
code: -32e3,
|
|
359
|
-
message: `Request timed out after ${timeout / 1e3}s`
|
|
360
|
-
}
|
|
361
|
-
})
|
|
362
|
-
);
|
|
363
|
-
}
|
|
364
|
-
}, timeout);
|
|
365
|
-
pendingRequests.set(daemonId, { clientWs, clientId, timer });
|
|
366
|
-
browserWs.send(
|
|
367
|
-
JSON.stringify({
|
|
368
|
-
id: daemonId,
|
|
369
|
-
type: "rpc_request",
|
|
370
|
-
method: msg.method,
|
|
371
|
-
params: msg.params
|
|
372
|
-
})
|
|
373
|
-
);
|
|
374
|
-
}
|
|
375
|
-
function buildBridgeUrl(port) {
|
|
376
|
-
const url = new URL("/cli-bridge", args.keysUrl);
|
|
377
|
-
url.searchParams.set("wsPort", String(port));
|
|
378
|
-
url.hash = `token=${encodeURIComponent(token)}`;
|
|
379
|
-
return url.toString();
|
|
380
|
-
}
|
|
381
|
-
//# sourceMappingURL=ws-daemon.js.map
|
|
382
|
-
//# sourceMappingURL=ws-daemon.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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"]}
|