@rynx-ai/daemon 0.1.0 → 0.1.10-beta.2

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.
Files changed (109) hide show
  1. package/dist/app-browser-host-supervisor.d.ts +97 -0
  2. package/dist/app-browser-host-supervisor.js +529 -0
  3. package/dist/browser-artifact-management.d.ts +20 -0
  4. package/dist/browser-artifact-management.js +61 -0
  5. package/dist/browser-cli-args.d.ts +20 -0
  6. package/dist/browser-cli-args.js +100 -0
  7. package/dist/browser-runner-session-context.d.ts +24 -0
  8. package/dist/browser-runner-session-context.js +113 -0
  9. package/dist/cdp-keyboard-input.d.ts +6 -0
  10. package/dist/cdp-keyboard-input.js +209 -0
  11. package/dist/channel-store.d.ts +0 -6
  12. package/dist/channel-store.js +2 -18
  13. package/dist/chrome-for-testing-release-resolver.d.ts +34 -0
  14. package/dist/chrome-for-testing-release-resolver.js +214 -0
  15. package/dist/chrome-for-testing-store.d.ts +85 -0
  16. package/dist/chrome-for-testing-store.js +836 -0
  17. package/dist/chrome-inspection-gateway.d.ts +58 -0
  18. package/dist/chrome-inspection-gateway.js +806 -0
  19. package/dist/chrome-inspection-manager.d.ts +94 -0
  20. package/dist/chrome-inspection-manager.js +573 -0
  21. package/dist/cli.js +478 -30
  22. package/dist/control-client.d.ts +122 -0
  23. package/dist/control-client.js +855 -0
  24. package/dist/control-deps.js +37 -6
  25. package/dist/control-endpoint.d.ts +14 -0
  26. package/dist/control-endpoint.js +124 -0
  27. package/dist/control-link-store.d.ts +16 -0
  28. package/dist/control-link-store.js +53 -0
  29. package/dist/daemon-build-status.d.ts +8 -0
  30. package/dist/daemon-build-status.js +18 -0
  31. package/dist/daemon-owner.d.ts +14 -0
  32. package/dist/daemon-owner.js +104 -0
  33. package/dist/daemon-server.d.ts +29 -2
  34. package/dist/daemon-server.js +1113 -26
  35. package/dist/db.js +196 -10
  36. package/dist/desktop-browser-host-client.d.ts +35 -0
  37. package/dist/desktop-browser-host-client.js +415 -0
  38. package/dist/direct-runtime-authenticator.d.ts +13 -0
  39. package/dist/direct-runtime-authenticator.js +132 -0
  40. package/dist/direct-runtime-config.d.ts +20 -0
  41. package/dist/direct-runtime-config.js +101 -0
  42. package/dist/embedded-browser-profile-cleanup.d.ts +58 -0
  43. package/dist/embedded-browser-profile-cleanup.js +338 -0
  44. package/dist/headless-browser-host.d.ts +14 -0
  45. package/dist/headless-browser-host.js +2173 -0
  46. package/dist/index-daemon.js +55 -5
  47. package/dist/installation-id.d.ts +33 -0
  48. package/dist/installation-id.js +692 -0
  49. package/dist/maintenance-management.d.ts +11 -0
  50. package/dist/maintenance-management.js +13 -0
  51. package/dist/migrations/cleanup-legacy-sessions.d.ts +0 -3
  52. package/dist/migrations/cleanup-legacy-sessions.js +10 -28
  53. package/dist/plugin-cli.d.ts +30 -0
  54. package/dist/plugin-cli.js +270 -0
  55. package/dist/plugin-host-rpc.d.ts +85 -0
  56. package/dist/plugin-host-rpc.js +878 -0
  57. package/dist/plugin-install-restart.d.ts +13 -0
  58. package/dist/plugin-install-restart.js +24 -0
  59. package/dist/plugin-installer.d.ts +71 -15
  60. package/dist/plugin-installer.js +521 -155
  61. package/dist/plugin-management-service.d.ts +58 -0
  62. package/dist/plugin-management-service.js +240 -0
  63. package/dist/plugin-package.d.ts +61 -0
  64. package/dist/plugin-package.js +732 -0
  65. package/dist/plugin-runtime-control.d.ts +2 -0
  66. package/dist/plugin-runtime-control.js +37 -0
  67. package/dist/plugin-store.d.ts +37 -8
  68. package/dist/plugin-store.js +80 -15
  69. package/dist/plugin-supervisor.d.ts +160 -0
  70. package/dist/plugin-supervisor.js +1143 -0
  71. package/dist/pm2.d.ts +3 -1
  72. package/dist/pm2.js +92 -18
  73. package/dist/provider-cli-service.d.ts +46 -0
  74. package/dist/provider-cli-service.js +510 -0
  75. package/dist/registry.d.ts +6 -19
  76. package/dist/registry.js +22 -87
  77. package/dist/remote-runtime-access-store.d.ts +62 -0
  78. package/dist/remote-runtime-access-store.js +393 -0
  79. package/dist/remote-runtime-admin.d.ts +35 -0
  80. package/dist/remote-runtime-admin.js +110 -0
  81. package/dist/remote-runtime-connection-manager.d.ts +103 -0
  82. package/dist/remote-runtime-connection-manager.js +1575 -0
  83. package/dist/remote-runtime-credential-store.d.ts +45 -0
  84. package/dist/remote-runtime-credential-store.js +639 -0
  85. package/dist/remote-runtime-target-control.d.ts +51 -0
  86. package/dist/remote-runtime-target-control.js +488 -0
  87. package/dist/remote-runtime-target-store.d.ts +56 -0
  88. package/dist/remote-runtime-target-store.js +326 -0
  89. package/dist/runtime-share-addresses.d.ts +21 -0
  90. package/dist/runtime-share-addresses.js +50 -0
  91. package/dist/session-browser-capability-store.d.ts +13 -0
  92. package/dist/session-browser-capability-store.js +58 -0
  93. package/dist/session-emulator-binding-store.d.ts +10 -0
  94. package/dist/session-emulator-binding-store.js +61 -0
  95. package/dist/session-launch-operations.d.ts +41 -0
  96. package/dist/session-launch-operations.js +124 -0
  97. package/dist/session-log-store.js +29 -0
  98. package/dist/session-meta-store.d.ts +3 -2
  99. package/dist/session-meta-store.js +59 -7
  100. package/dist/session-pending-message-store.d.ts +2 -0
  101. package/dist/session-pending-message-store.js +41 -0
  102. package/dist/session-resource-store.d.ts +32 -0
  103. package/dist/session-resource-store.js +700 -0
  104. package/dist/setup.d.ts +16 -0
  105. package/dist/setup.js +144 -5
  106. package/dist/skills-catalog.js +8 -4
  107. package/dist/update.d.ts +11 -2
  108. package/dist/update.js +0 -27
  109. package/package.json +23 -10
@@ -0,0 +1,806 @@
1
+ import { createHash, randomBytes } from "node:crypto";
2
+ import { createServer, } from "node:http";
3
+ import { createConnection, } from "node:net";
4
+ import WebSocket, { WebSocketServer } from "ws";
5
+ const LOOPBACK_HOST = "127.0.0.1";
6
+ const DEFAULT_PORT = 9_333;
7
+ const DEFAULT_OPEN_TIMEOUT_MS = 10_000;
8
+ const DEFAULT_MAX_CONNECTIONS = 64;
9
+ const DEFAULT_MAX_PENDING_OPENINGS = 16;
10
+ const MAX_CDP_MESSAGE_BYTES = 16 * 1024 * 1024;
11
+ const MAX_BUFFERED_BYTES = 32 * 1024 * 1024;
12
+ const MAX_HANDSHAKE_BYTES = 64 * 1024;
13
+ const SHUTDOWN_TIMEOUT_MS = 1_000;
14
+ const ROUTE_TOKEN_PATTERN = /^[A-Za-z0-9_-]{32,128}$/;
15
+ const PAGE_ROUTE_PREFIX = "/devtools/page/";
16
+ const WEBSOCKET_GUID = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
17
+ /**
18
+ * Stable loopback discovery endpoint consumed by stock Chrome.
19
+ *
20
+ * This gateway owns no Browser. It rewrites only discovery metadata and then
21
+ * bridges each DevTools WebSocket to an exact target supplied by the daemon.
22
+ */
23
+ export async function startChromeInspectionGateway(options) {
24
+ const requestedPort = options.port ?? DEFAULT_PORT;
25
+ if (!Number.isInteger(requestedPort) || requestedPort < 0 || requestedPort > 65_535) {
26
+ throw new Error("Chrome inspection port must be an integer between 0 and 65535");
27
+ }
28
+ const openTimeoutMs = boundedPositiveInteger(options.openTimeoutMs ?? DEFAULT_OPEN_TIMEOUT_MS, "openTimeoutMs");
29
+ const maxConnections = boundedPositiveInteger(options.maxConnections ?? DEFAULT_MAX_CONNECTIONS, "maxConnections");
30
+ const maxPendingOpenings = boundedPositiveInteger(options.maxPendingOpenings ?? DEFAULT_MAX_PENDING_OPENINGS, "maxPendingOpenings");
31
+ if (maxPendingOpenings > maxConnections) {
32
+ throw new Error("maxPendingOpenings must not exceed maxConnections");
33
+ }
34
+ const randomToken = options.randomToken ?? (() => randomBytes(24).toString("base64url"));
35
+ const byKey = new Map();
36
+ const byToken = new Map();
37
+ const peerByClient = new Map();
38
+ const tokenByClient = new Map();
39
+ const tunnelPeerByClient = new Map();
40
+ const tunnelTokenByClient = new Map();
41
+ const pendingOpenings = new Map();
42
+ const upgradeTasks = new Set();
43
+ const closingPeers = new Set();
44
+ let stopping = false;
45
+ const server = createServer((request, response) => {
46
+ void handleHttp(request, response).catch((error) => {
47
+ options.onError?.(asError(error), "HTTP request");
48
+ if (!response.headersSent) {
49
+ response.statusCode = 500;
50
+ setCommonHeaders(response);
51
+ }
52
+ response.end("internal error\n");
53
+ });
54
+ });
55
+ const webSocketServer = new WebSocketServer({
56
+ noServer: true,
57
+ clientTracking: true,
58
+ perMessageDeflate: false,
59
+ maxPayload: MAX_CDP_MESSAGE_BYTES,
60
+ });
61
+ server.on("upgrade", (request, socket, head) => {
62
+ const task = handleUpgrade(request, socket, head).catch((error) => {
63
+ options.onError?.(asError(error), "WebSocket upgrade");
64
+ rejectUpgrade(socket, 502, "Chrome inspection target is unavailable");
65
+ });
66
+ upgradeTasks.add(task);
67
+ void task.finally(() => upgradeTasks.delete(task)).catch(() => undefined);
68
+ });
69
+ server.on("error", (error) => options.onError?.(error, "listener"));
70
+ await listen(server, requestedPort);
71
+ const address = server.address();
72
+ const port = address.port;
73
+ const authority = `${LOOPBACK_HOST}:${port}`;
74
+ const httpEndpoint = `http://${authority}`;
75
+ async function handleHttp(request, response) {
76
+ setCommonHeaders(response);
77
+ if (stopping)
78
+ return respond(response, 503, "stopping\n");
79
+ if (!isTrustedRequest(request, authority))
80
+ return respond(response, 403, "forbidden\n");
81
+ if (request.method !== "GET") {
82
+ response.setHeader("allow", "GET");
83
+ return respond(response, 405, "method not allowed\n");
84
+ }
85
+ const url = requestUrl(request);
86
+ if (!url)
87
+ return respond(response, 400, "bad request\n");
88
+ if (url.pathname === "/json/version") {
89
+ return respondJson(response, 200, {
90
+ Browser: "Chrome/145.0.0.0",
91
+ "Protocol-Version": "1.3",
92
+ "User-Agent": "Rynx Chrome Inspection Gateway",
93
+ });
94
+ }
95
+ if (url.pathname === "/json" || url.pathname === "/json/list") {
96
+ const targets = await refreshTargets();
97
+ return respondJson(response, 200, targets.map(({ token, target }) => {
98
+ const path = `${PAGE_ROUTE_PREFIX}${token}`;
99
+ const ws = `ws://${authority}${path}`;
100
+ return {
101
+ description: target.description,
102
+ devtoolsFrontendUrl: `/devtools/inspector.html?ws=${authority}${path}`,
103
+ id: token,
104
+ title: target.title,
105
+ type: "page",
106
+ url: target.url,
107
+ webSocketDebuggerUrl: ws,
108
+ };
109
+ }));
110
+ }
111
+ const action = actionRoute(url.pathname);
112
+ if (action) {
113
+ const registered = byToken.get(action.token);
114
+ if (!registered)
115
+ return respond(response, 404, "no such target\n");
116
+ if (action.action === "activate") {
117
+ if (!options.source.activate)
118
+ return respond(response, 405, "activation unsupported\n");
119
+ await options.source.activate(registered.target);
120
+ return respond(response, 200, "target activated\n");
121
+ }
122
+ if (!options.source.close)
123
+ return respond(response, 405, "close unsupported\n");
124
+ await options.source.close(registered.target);
125
+ return respond(response, 200, "target is closing\n");
126
+ }
127
+ return respond(response, 404, "not found\n");
128
+ }
129
+ async function handleUpgrade(request, socket, head) {
130
+ socket.pause();
131
+ if (stopping)
132
+ return rejectUpgrade(socket, 503, "Chrome inspection is stopping");
133
+ if (!isTrustedRequest(request, authority)) {
134
+ return rejectUpgrade(socket, 403, "Chrome inspection request is forbidden");
135
+ }
136
+ if (!isValidWebSocketHandshake(request)) {
137
+ return rejectUpgrade(socket, 400, "Invalid Chrome inspection WebSocket upgrade");
138
+ }
139
+ const url = requestUrl(request);
140
+ const token = url && exactPageRouteToken(url);
141
+ const registered = token ? byToken.get(token) : undefined;
142
+ if (!registered)
143
+ return rejectUpgrade(socket, 404, "Chrome inspection target was not found");
144
+ if (pendingOpenings.size >= maxPendingOpenings ||
145
+ peerByClient.size + tunnelPeerByClient.size + pendingOpenings.size >= maxConnections) {
146
+ return rejectUpgrade(socket, 503, "Chrome inspection connection capacity reached");
147
+ }
148
+ if (pendingOpenings.has(token) ||
149
+ [...tokenByClient.values()].some((activeToken) => activeToken === token) ||
150
+ [...tunnelTokenByClient.values()].some((activeToken) => activeToken === token)) {
151
+ return rejectUpgrade(socket, 409, "Chrome inspection target is already inspected");
152
+ }
153
+ const opening = new AbortController();
154
+ const abortOpening = () => {
155
+ opening.abort(new Error("Chrome inspection client disconnected during opening"));
156
+ };
157
+ socket.once("close", abortOpening);
158
+ socket.once("end", abortOpening);
159
+ socket.once("error", abortOpening);
160
+ pendingOpenings.set(token, opening);
161
+ let upstream;
162
+ let claimed = false;
163
+ try {
164
+ upstream = await openTargetConnection(options.source, registered.target, request, openTimeoutMs, opening.signal);
165
+ if (stopping || socket.destroyed || byToken.get(token) !== registered) {
166
+ return rejectUpgrade(socket, 404, "Chrome inspection target became stale");
167
+ }
168
+ if (upstream.kind === "tunnel") {
169
+ bridgeRawTunnel(socket, upstream.socket, head, upstream.handshake, token);
170
+ claimed = true;
171
+ return;
172
+ }
173
+ const connection = upstream.connection;
174
+ const closeUpstream = upstream.close;
175
+ webSocketServer.handleUpgrade(request, socket, head, (client) => {
176
+ claimed = true;
177
+ tokenByClient.set(client, token);
178
+ const placeholder = {
179
+ close: async () => {
180
+ if (client.readyState !== WebSocket.CLOSED)
181
+ client.terminate();
182
+ peerByClient.delete(client);
183
+ tokenByClient.delete(client);
184
+ await closeUpstream().catch(() => undefined);
185
+ },
186
+ };
187
+ peerByClient.set(client, placeholder);
188
+ const peer = bridgeConnection(client, connection);
189
+ if (peerByClient.get(client) === placeholder) {
190
+ peerByClient.set(client, peer);
191
+ }
192
+ else {
193
+ void peer.close("Chrome inspection bridge closed during registration").catch(() => undefined);
194
+ }
195
+ webSocketServer.emit("connection", client, request);
196
+ socket.resume();
197
+ });
198
+ }
199
+ finally {
200
+ socket.removeListener("close", abortOpening);
201
+ socket.removeListener("end", abortOpening);
202
+ socket.removeListener("error", abortOpening);
203
+ if (pendingOpenings.get(token) === opening) {
204
+ pendingOpenings.delete(token);
205
+ }
206
+ if (!claimed && upstream) {
207
+ await upstream.close().catch(() => undefined);
208
+ }
209
+ }
210
+ }
211
+ async function refreshTargets() {
212
+ const projected = await options.source.listTargets();
213
+ const seen = new Set();
214
+ const registered = [];
215
+ for (const target of projected) {
216
+ validateTarget(target);
217
+ if (seen.has(target.key))
218
+ throw new Error("Chrome inspection source returned duplicate target keys");
219
+ seen.add(target.key);
220
+ let entry = byKey.get(target.key);
221
+ if (!entry) {
222
+ const token = uniqueToken(randomToken, byToken);
223
+ entry = { token, target: { ...target } };
224
+ byKey.set(target.key, entry);
225
+ byToken.set(token, entry);
226
+ }
227
+ else {
228
+ entry.target = { ...target };
229
+ }
230
+ registered.push(entry);
231
+ }
232
+ for (const [key, entry] of byKey) {
233
+ if (seen.has(key))
234
+ continue;
235
+ byKey.delete(key);
236
+ byToken.delete(entry.token);
237
+ // Discovery controls only new routes. An established DevTools socket
238
+ // stays attached until either endpoint closes its actual transport.
239
+ pendingOpenings.get(entry.token)?.abort(new Error("Chrome inspection target disappeared"));
240
+ }
241
+ return registered;
242
+ }
243
+ function bridgeRawTunnel(client, upstream, clientHead, upstreamHandshake, token) {
244
+ if (client.destroyed || upstream.destroyed) {
245
+ throw new Error("Chrome inspection tunnel closed during handoff");
246
+ }
247
+ let closed = false;
248
+ let peer;
249
+ const finish = () => {
250
+ if (closed)
251
+ return;
252
+ closed = true;
253
+ if (tunnelPeerByClient.get(client) === peer) {
254
+ tunnelPeerByClient.delete(client);
255
+ tunnelTokenByClient.delete(client);
256
+ }
257
+ client.unpipe(upstream);
258
+ upstream.unpipe(client);
259
+ if (!client.destroyed)
260
+ client.destroy();
261
+ if (!upstream.destroyed)
262
+ upstream.destroy();
263
+ };
264
+ peer = {
265
+ close: async () => {
266
+ finish();
267
+ },
268
+ };
269
+ tunnelPeerByClient.set(client, peer);
270
+ tunnelTokenByClient.set(client, token);
271
+ client.once("close", finish);
272
+ client.once("error", finish);
273
+ upstream.once("close", finish);
274
+ upstream.once("error", finish);
275
+ try {
276
+ client.write(upstreamHandshake);
277
+ if (clientHead.byteLength > 0)
278
+ upstream.write(clientHead);
279
+ client.pipe(upstream);
280
+ upstream.pipe(client);
281
+ client.resume();
282
+ upstream.resume();
283
+ }
284
+ catch (error) {
285
+ finish();
286
+ throw error;
287
+ }
288
+ return peer;
289
+ }
290
+ function bridgeConnection(client, connection) {
291
+ let closed = false;
292
+ let connectionClosePromise;
293
+ const closeConnection = () => {
294
+ if (connectionClosePromise)
295
+ return connectionClosePromise;
296
+ const closing = Promise.resolve()
297
+ .then(() => connection.close())
298
+ .catch(() => undefined);
299
+ connectionClosePromise = closing;
300
+ closingPeers.add(closing);
301
+ void closing.finally(() => closingPeers.delete(closing)).catch(() => undefined);
302
+ return closing;
303
+ };
304
+ const finish = (code, reason, terminateClient = false) => {
305
+ if (closed)
306
+ return;
307
+ closed = true;
308
+ peerByClient.delete(client);
309
+ tokenByClient.delete(client);
310
+ void closeConnection();
311
+ if (terminateClient && client.readyState !== WebSocket.CLOSED) {
312
+ client.terminate();
313
+ }
314
+ else if (client.readyState === WebSocket.OPEN) {
315
+ client.close(code, reason.slice(0, 120));
316
+ }
317
+ else if (client.readyState !== WebSocket.CLOSED) {
318
+ client.terminate();
319
+ }
320
+ };
321
+ client.on("message", (data, isBinary) => {
322
+ if (closed)
323
+ return;
324
+ void connection.send(isBinary
325
+ ? { binary: true, data: rawDataBytes(data) }
326
+ : { binary: false, data: rawDataText(data) }).catch(() => {
327
+ finish(1_011, "Chrome inspection upstream failed", true);
328
+ });
329
+ });
330
+ client.on("close", () => finish(1_001, "Chrome inspection client closed"));
331
+ client.on("error", () => finish(1_011, "Chrome inspection client error"));
332
+ void (async () => {
333
+ try {
334
+ for await (const message of connection) {
335
+ if (closed)
336
+ break;
337
+ await forwardFrame(client, message.data, message.binary);
338
+ }
339
+ finish(1_001, "Chrome inspection upstream closed");
340
+ }
341
+ catch {
342
+ finish(1_011, "Chrome inspection upstream failed");
343
+ }
344
+ })();
345
+ return {
346
+ close: async (reason) => {
347
+ finish(1_001, reason, true);
348
+ await closeConnection();
349
+ },
350
+ };
351
+ }
352
+ let shutdownPromise;
353
+ const shutdown = () => {
354
+ if (shutdownPromise)
355
+ return shutdownPromise;
356
+ stopping = true;
357
+ shutdownPromise = (async () => {
358
+ for (const opening of pendingOpenings.values())
359
+ opening.abort();
360
+ pendingOpenings.clear();
361
+ await Promise.allSettled([...upgradeTasks]);
362
+ const activeClosures = [
363
+ ...peerByClient.values(),
364
+ ...tunnelPeerByClient.values(),
365
+ ].map((peer) => peer.close("Chrome inspection gateway stopped"));
366
+ peerByClient.clear();
367
+ tokenByClient.clear();
368
+ tunnelPeerByClient.clear();
369
+ tunnelTokenByClient.clear();
370
+ await Promise.allSettled(activeClosures);
371
+ await Promise.allSettled([...closingPeers]);
372
+ await Promise.all([
373
+ closeWebSocketServer(webSocketServer),
374
+ closeHttpServer(server),
375
+ ]);
376
+ })();
377
+ return shutdownPromise;
378
+ };
379
+ return {
380
+ host: LOOPBACK_HOST,
381
+ port,
382
+ httpEndpoint,
383
+ shutdown,
384
+ };
385
+ }
386
+ function isTrustedRequest(request, authority) {
387
+ return request.headers.host === authority && isTrustedOrigin(request.headers.origin);
388
+ }
389
+ function isValidWebSocketHandshake(request) {
390
+ const key = request.headers["sec-websocket-key"];
391
+ return request.method === "GET" &&
392
+ request.httpVersion === "1.1" &&
393
+ headerHasToken(request.headers.connection, "upgrade") &&
394
+ request.headers.upgrade?.toLowerCase() === "websocket" &&
395
+ request.headers["sec-websocket-version"] === "13" &&
396
+ typeof key === "string" &&
397
+ /^[+/0-9A-Za-z]{22}==$/.test(key) &&
398
+ Buffer.from(key, "base64").byteLength === 16;
399
+ }
400
+ function isTrustedOrigin(origin) {
401
+ return origin === undefined ||
402
+ origin === "devtools://devtools" ||
403
+ origin === "chrome-devtools://devtools";
404
+ }
405
+ function requestUrl(request) {
406
+ try {
407
+ return new URL(request.url ?? "", "http://127.0.0.1");
408
+ }
409
+ catch {
410
+ return undefined;
411
+ }
412
+ }
413
+ function exactPageRouteToken(url) {
414
+ if (url.search || url.hash || !url.pathname.startsWith(PAGE_ROUTE_PREFIX))
415
+ return undefined;
416
+ const token = url.pathname.slice(PAGE_ROUTE_PREFIX.length);
417
+ return ROUTE_TOKEN_PATTERN.test(token) ? token : undefined;
418
+ }
419
+ function actionRoute(pathname) {
420
+ const match = /^\/json\/(activate|close)\/([A-Za-z0-9_-]{32,128})$/.exec(pathname);
421
+ if (!match)
422
+ return undefined;
423
+ return { action: match[1], token: match[2] };
424
+ }
425
+ function uniqueToken(factory, existing) {
426
+ for (let attempt = 0; attempt < 64; attempt += 1) {
427
+ const token = factory();
428
+ if (ROUTE_TOKEN_PATTERN.test(token) && !existing.has(token))
429
+ return token;
430
+ }
431
+ throw new Error("Chrome inspection route token factory did not produce a unique token");
432
+ }
433
+ function validateTarget(target) {
434
+ if (!target.key || target.key.length > 2_048) {
435
+ throw new Error("Chrome inspection target key is invalid");
436
+ }
437
+ for (const [field, value, limit] of [
438
+ ["title", target.title, 4_096],
439
+ ["url", target.url, 16 * 1_024],
440
+ ["description", target.description, 4_096],
441
+ ["engineVersion", target.engineVersion, 256],
442
+ ]) {
443
+ if (typeof value !== "string" || Buffer.byteLength(value, "utf8") > limit) {
444
+ throw new Error(`Chrome inspection target ${field} is invalid`);
445
+ }
446
+ }
447
+ }
448
+ function parseLoopbackWebSocketEndpoint(value) {
449
+ let url;
450
+ try {
451
+ url = new URL(value);
452
+ }
453
+ catch {
454
+ throw new Error("Chrome inspection upstream endpoint is invalid");
455
+ }
456
+ if (url.protocol !== "ws:" ||
457
+ url.hostname !== LOOPBACK_HOST ||
458
+ !url.port ||
459
+ url.username ||
460
+ url.password ||
461
+ url.search ||
462
+ url.hash ||
463
+ !url.pathname.startsWith("/devtools/")) {
464
+ throw new Error("Chrome inspection upstream endpoint must be a loopback CDP WebSocket URL");
465
+ }
466
+ return url;
467
+ }
468
+ async function openTargetConnection(source, target, request, openTimeoutMs, signal) {
469
+ const timeout = new AbortController();
470
+ const timeoutHandle = setTimeout(() => {
471
+ timeout.abort(new Error("Chrome inspection attachment opening timed out"));
472
+ }, openTimeoutMs);
473
+ timeoutHandle.unref?.();
474
+ const openingSignal = AbortSignal.any([signal, timeout.signal]);
475
+ try {
476
+ if (source.openConnection) {
477
+ const opening = source.openConnection(target, { signal: openingSignal });
478
+ let connection;
479
+ try {
480
+ connection = await waitForValue(opening, openTimeoutMs, openingSignal, "Chrome inspection attachment opening timed out");
481
+ }
482
+ catch (error) {
483
+ void opening.then((late) => late?.close()).catch(() => undefined);
484
+ throw error;
485
+ }
486
+ if (connection) {
487
+ return {
488
+ kind: "connection",
489
+ connection,
490
+ close: async () => {
491
+ await connection.close();
492
+ },
493
+ };
494
+ }
495
+ }
496
+ if (!source.resolveWebSocketEndpoint) {
497
+ throw new Error("Chrome inspection target source cannot open attachments");
498
+ }
499
+ const endpoint = parseLoopbackWebSocketEndpoint(await waitForValue(source.resolveWebSocketEndpoint(target), openTimeoutMs, openingSignal, "Chrome inspection endpoint resolution timed out"));
500
+ return await openRawWebSocketUpstream(request, endpoint, openingSignal);
501
+ }
502
+ finally {
503
+ clearTimeout(timeoutHandle);
504
+ }
505
+ }
506
+ function openRawWebSocketUpstream(request, endpoint, signal) {
507
+ const key = request.headers["sec-websocket-key"];
508
+ if (typeof key !== "string") {
509
+ return Promise.reject(new Error("Chrome inspection WebSocket key is missing"));
510
+ }
511
+ const expectedAccept = createHash("sha1")
512
+ .update(`${key}${WEBSOCKET_GUID}`)
513
+ .digest("base64");
514
+ const socket = createConnection({
515
+ host: endpoint.hostname,
516
+ port: Number(endpoint.port),
517
+ });
518
+ socket.setNoDelay(true);
519
+ // Keep errors handled after the opening listeners are removed and before the
520
+ // active tunnel installs its own lifecycle listeners.
521
+ socket.on("error", () => undefined);
522
+ return new Promise((resolve, reject) => {
523
+ let settled = false;
524
+ let response = Buffer.alloc(0);
525
+ const cleanup = () => {
526
+ signal.removeEventListener("abort", onAbort);
527
+ socket.removeListener("connect", onConnect);
528
+ socket.removeListener("data", onData);
529
+ socket.removeListener("close", onClose);
530
+ };
531
+ const fail = (error) => {
532
+ if (settled)
533
+ return;
534
+ settled = true;
535
+ cleanup();
536
+ socket.destroy();
537
+ reject(error);
538
+ };
539
+ const onAbort = () => {
540
+ fail(signal.reason instanceof Error
541
+ ? signal.reason
542
+ : new Error("Chrome inspection opening was cancelled"));
543
+ };
544
+ const onConnect = () => {
545
+ socket.write(serializeUpstreamHandshake(request, endpoint));
546
+ };
547
+ const onData = (chunk) => {
548
+ response = Buffer.concat([response, chunk]);
549
+ const headerEnd = response.indexOf("\r\n\r\n");
550
+ if (headerEnd < 0) {
551
+ if (response.byteLength > MAX_HANDSHAKE_BYTES) {
552
+ fail(new Error("Chrome inspection upstream handshake is too large"));
553
+ }
554
+ return;
555
+ }
556
+ if (headerEnd + 4 > MAX_HANDSHAKE_BYTES) {
557
+ fail(new Error("Chrome inspection upstream handshake is too large"));
558
+ return;
559
+ }
560
+ try {
561
+ validateUpstreamHandshake(response.subarray(0, headerEnd + 4), expectedAccept);
562
+ }
563
+ catch (error) {
564
+ fail(asError(error));
565
+ return;
566
+ }
567
+ settled = true;
568
+ socket.pause();
569
+ cleanup();
570
+ resolve({
571
+ kind: "tunnel",
572
+ socket,
573
+ handshake: response,
574
+ close: async () => {
575
+ if (!socket.destroyed)
576
+ socket.destroy();
577
+ },
578
+ });
579
+ };
580
+ const onClose = () => {
581
+ fail(new Error("Chrome inspection upstream closed during handshake"));
582
+ };
583
+ if (signal.aborted) {
584
+ onAbort();
585
+ return;
586
+ }
587
+ signal.addEventListener("abort", onAbort, { once: true });
588
+ socket.once("connect", onConnect);
589
+ socket.on("data", onData);
590
+ socket.once("close", onClose);
591
+ });
592
+ }
593
+ function serializeUpstreamHandshake(request, endpoint) {
594
+ const lines = [`GET ${endpoint.pathname} HTTP/1.1`];
595
+ let hostRewritten = false;
596
+ for (let index = 0; index < request.rawHeaders.length; index += 2) {
597
+ const name = request.rawHeaders[index];
598
+ const value = request.rawHeaders[index + 1];
599
+ const lowerName = name.toLowerCase();
600
+ if (lowerName === "origin" || lowerName === "sec-websocket-origin") {
601
+ // The gateway validates the browser Origin. Forwarding it would make
602
+ // Chromium's loopback DevTools endpoint reject this trusted proxy hop.
603
+ continue;
604
+ }
605
+ if (lowerName === "host") {
606
+ if (!hostRewritten) {
607
+ lines.push(`${name}: ${endpoint.host}`);
608
+ hostRewritten = true;
609
+ }
610
+ continue;
611
+ }
612
+ lines.push(`${name}: ${value}`);
613
+ }
614
+ if (!hostRewritten)
615
+ lines.push(`Host: ${endpoint.host}`);
616
+ lines.push("", "");
617
+ return Buffer.from(lines.join("\r\n"), "latin1");
618
+ }
619
+ function validateUpstreamHandshake(handshake, expectedAccept) {
620
+ const lines = handshake.toString("latin1").split("\r\n");
621
+ if (!/^HTTP\/1\.1 101(?:[ \t].*)?$/.test(lines.shift() ?? "")) {
622
+ throw new Error("Chrome inspection upstream did not switch protocols");
623
+ }
624
+ const headers = new Map();
625
+ for (const line of lines) {
626
+ if (line === "")
627
+ break;
628
+ const separator = line.indexOf(":");
629
+ if (separator <= 0) {
630
+ throw new Error("Chrome inspection upstream returned malformed headers");
631
+ }
632
+ const name = line.slice(0, separator).trim().toLowerCase();
633
+ const value = line.slice(separator + 1).trim();
634
+ const values = headers.get(name);
635
+ if (values)
636
+ values.push(value);
637
+ else
638
+ headers.set(name, [value]);
639
+ }
640
+ if (!headerHasToken(headers.get("connection"), "upgrade") ||
641
+ !headerHasToken(headers.get("upgrade"), "websocket") ||
642
+ headers.get("sec-websocket-accept")?.length !== 1 ||
643
+ headers.get("sec-websocket-accept")?.[0] !== expectedAccept) {
644
+ throw new Error("Chrome inspection upstream returned an invalid WebSocket handshake");
645
+ }
646
+ }
647
+ function headerHasToken(value, expected) {
648
+ const values = Array.isArray(value) ? value : value === undefined ? [] : [value];
649
+ return values.some((entry) => entry.split(",").some((token) => token.trim().toLowerCase() === expected));
650
+ }
651
+ async function forwardFrame(destination, data, isBinary) {
652
+ if (destination.readyState !== WebSocket.OPEN)
653
+ throw new Error("destination is not open");
654
+ const bytes = rawDataBytes(data);
655
+ if (bytes.byteLength > MAX_CDP_MESSAGE_BYTES ||
656
+ destination.bufferedAmount + bytes.byteLength > MAX_BUFFERED_BYTES) {
657
+ throw new Error("Chrome inspection forwarding capacity exceeded");
658
+ }
659
+ await new Promise((resolve, reject) => {
660
+ destination.send(bytes, { binary: isBinary, compress: false }, (error) => {
661
+ if (error)
662
+ reject(error);
663
+ else
664
+ resolve();
665
+ });
666
+ });
667
+ }
668
+ function rawDataBytes(data) {
669
+ if (typeof data === "string")
670
+ return Buffer.from(data, "utf8");
671
+ if (data instanceof ArrayBuffer)
672
+ return new Uint8Array(data);
673
+ if (Array.isArray(data))
674
+ return Buffer.concat(data);
675
+ return new Uint8Array(data.buffer, data.byteOffset, data.byteLength);
676
+ }
677
+ function rawDataText(data) {
678
+ return Buffer.from(rawDataBytes(data)).toString("utf8");
679
+ }
680
+ function waitForValue(promise, timeoutMs, signal, timeoutMessage) {
681
+ if (signal.aborted) {
682
+ return Promise.reject(signal.reason ?? new Error("Chrome inspection opening was cancelled"));
683
+ }
684
+ return new Promise((resolve, reject) => {
685
+ let settled = false;
686
+ const finish = (callback) => {
687
+ if (settled)
688
+ return;
689
+ settled = true;
690
+ clearTimeout(timer);
691
+ signal.removeEventListener("abort", onAbort);
692
+ callback();
693
+ };
694
+ const timer = setTimeout(() => {
695
+ finish(() => reject(new Error(timeoutMessage)));
696
+ }, timeoutMs);
697
+ timer.unref?.();
698
+ const onAbort = () => {
699
+ finish(() => reject(signal.reason ?? new Error("Chrome inspection opening was cancelled")));
700
+ };
701
+ signal.addEventListener("abort", onAbort, { once: true });
702
+ promise.then((value) => finish(() => resolve(value)), (error) => finish(() => reject(error)));
703
+ });
704
+ }
705
+ function setCommonHeaders(response) {
706
+ response.setHeader("cache-control", "no-store");
707
+ response.setHeader("x-content-type-options", "nosniff");
708
+ }
709
+ function respond(response, status, body) {
710
+ response.statusCode = status;
711
+ response.setHeader("content-type", "text/plain; charset=utf-8");
712
+ response.end(body);
713
+ }
714
+ function respondJson(response, status, value) {
715
+ response.statusCode = status;
716
+ response.setHeader("content-type", "application/json; charset=utf-8");
717
+ response.end(`${JSON.stringify(value)}\n`);
718
+ }
719
+ function rejectUpgrade(socket, status, reason) {
720
+ if (socket.destroyed)
721
+ return;
722
+ socket.once("error", () => undefined);
723
+ socket.once("finish", () => socket.destroy());
724
+ socket.end(`HTTP/1.1 ${status} ${httpStatusText(status)}\r\n` +
725
+ "Connection: close\r\n" +
726
+ "Content-Type: text/plain; charset=utf-8\r\n" +
727
+ "Cache-Control: no-store\r\n" +
728
+ `Content-Length: ${Buffer.byteLength(reason, "utf8")}\r\n\r\n${reason}`);
729
+ }
730
+ function httpStatusText(status) {
731
+ if (status === 400)
732
+ return "Bad Request";
733
+ if (status === 403)
734
+ return "Forbidden";
735
+ if (status === 404)
736
+ return "Not Found";
737
+ if (status === 409)
738
+ return "Conflict";
739
+ if (status === 502)
740
+ return "Bad Gateway";
741
+ if (status === 503)
742
+ return "Service Unavailable";
743
+ return "Error";
744
+ }
745
+ function listen(server, port) {
746
+ return new Promise((resolve, reject) => {
747
+ const onError = (error) => {
748
+ server.removeListener("listening", onListening);
749
+ reject(error);
750
+ };
751
+ const onListening = () => {
752
+ server.removeListener("error", onError);
753
+ resolve();
754
+ };
755
+ server.once("error", onError);
756
+ server.once("listening", onListening);
757
+ server.listen(port, LOOPBACK_HOST);
758
+ });
759
+ }
760
+ function closeHttpServer(server) {
761
+ return new Promise((resolve) => {
762
+ let settled = false;
763
+ const finish = () => {
764
+ if (settled)
765
+ return;
766
+ settled = true;
767
+ clearTimeout(timer);
768
+ resolve();
769
+ };
770
+ const timer = setTimeout(() => {
771
+ server.closeAllConnections?.();
772
+ finish();
773
+ }, SHUTDOWN_TIMEOUT_MS);
774
+ timer.unref?.();
775
+ server.close(() => finish());
776
+ server.closeIdleConnections?.();
777
+ });
778
+ }
779
+ function closeWebSocketServer(server) {
780
+ for (const client of server.clients) {
781
+ if (client.readyState !== WebSocket.CLOSED)
782
+ client.terminate();
783
+ }
784
+ return new Promise((resolve) => {
785
+ let settled = false;
786
+ const finish = () => {
787
+ if (settled)
788
+ return;
789
+ settled = true;
790
+ clearTimeout(timer);
791
+ resolve();
792
+ };
793
+ const timer = setTimeout(finish, SHUTDOWN_TIMEOUT_MS);
794
+ timer.unref?.();
795
+ server.close(() => finish());
796
+ });
797
+ }
798
+ function boundedPositiveInteger(value, field) {
799
+ if (!Number.isInteger(value) || value < 1 || value > 120_000) {
800
+ throw new Error(`${field} must be an integer between 1 and 120000`);
801
+ }
802
+ return value;
803
+ }
804
+ function asError(error) {
805
+ return error instanceof Error ? error : new Error(String(error));
806
+ }