@ait-co/devtools 0.1.128 → 0.1.131
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/{attach-orchestrator-CPMQWkGV.js → attach-orchestrator-BiaYiNOr.js} +34 -107
- package/dist/attach-orchestrator-BiaYiNOr.js.map +1 -0
- package/dist/{attach-orchestrator-RhOdO3DS.js → attach-orchestrator-Cjdhir2U.js} +31 -13
- package/dist/{attach-orchestrator-RhOdO3DS.js.map → attach-orchestrator-Cjdhir2U.js.map} +1 -1
- package/dist/{attach-orchestrator-CpE0dMew.js → attach-orchestrator-ca34r46y.js} +33 -106
- package/dist/attach-orchestrator-ca34r46y.js.map +1 -0
- package/dist/{bundle-Cb3GFuw6.d.ts → bundle-BKqyhEK9.d.ts} +1 -1
- package/dist/{bundle-Cb3GFuw6.d.ts.map → bundle-BKqyhEK9.d.ts.map} +1 -1
- package/dist/{capture-B1zfGyTo.d.ts → capture-ltuV0gZa.d.ts} +1 -1
- package/dist/{capture-B1zfGyTo.d.ts.map → capture-ltuV0gZa.d.ts.map} +1 -1
- package/dist/{cdp-connection-Dxw_EY8s.d.ts → cdp-connection-D7AYQck2.d.ts} +1 -1
- package/dist/{cdp-connection-Dxw_EY8s.d.ts.map → cdp-connection-D7AYQck2.d.ts.map} +1 -1
- package/dist/{cell-D0dJSGHi.js → cell-Bi8MAPA8.js} +11 -7
- package/dist/cell-Bi8MAPA8.js.map +1 -0
- package/dist/{cell-Ba9lCI3B.js → cell-Cw4fVOl2.js} +11 -7
- package/dist/cell-Cw4fVOl2.js.map +1 -0
- package/dist/{cell-CX92lDI0.js → cell-rr6gL9AT.js} +11 -7
- package/dist/cell-rr6gL9AT.js.map +1 -0
- package/dist/debug-server-BSbfnFR5.js +436 -0
- package/dist/debug-server-BSbfnFR5.js.map +1 -0
- package/dist/debug-server-CD4zYBq4.js +379 -0
- package/dist/{debug-server-BzzNkETg.js.map → debug-server-CD4zYBq4.js.map} +1 -1
- package/dist/{debug-server-Dctc9GAh.js → debug-server-qRSRG7YZ.js} +68 -65
- package/dist/debug-server-qRSRG7YZ.js.map +1 -0
- package/dist/in-app/auto.js +15 -0
- package/dist/in-app/auto.js.map +1 -1
- package/dist/in-app/index.d.ts +27 -0
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +15 -0
- package/dist/in-app/index.js.map +1 -1
- package/dist/log-CUik_tHp.js +95 -0
- package/dist/log-CUik_tHp.js.map +1 -0
- package/dist/mcp/cli.js +1272 -1111
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/panel/index.js +11 -1
- package/dist/panel/index.js.map +1 -1
- package/dist/{pool-DBLKNeRC.d.ts → pool-BaSCZALv.d.ts} +12 -4
- package/dist/{pool-DBLKNeRC.d.ts.map → pool-BaSCZALv.d.ts.map} +1 -1
- package/dist/{qr-http-server-Bo8LMOJQ.js → qr-http-server-BdNGPRoL.js} +76 -6
- package/dist/qr-http-server-BdNGPRoL.js.map +1 -0
- package/dist/{qr-http-server-CrGohOwC.js → qr-http-server-Bq9zR7-z.js} +76 -6
- package/dist/qr-http-server-Bq9zR7-z.js.map +1 -0
- package/dist/{qr-http-server-Bt_RAxFn.cjs → qr-http-server-BxoRp2w7.cjs} +76 -6
- package/dist/qr-http-server-BxoRp2w7.cjs.map +1 -0
- package/dist/{qr-http-server-DM2R25jP.cjs → qr-http-server-CV6TA9ZN.cjs} +76 -6
- package/dist/qr-http-server-CV6TA9ZN.cjs.map +1 -0
- package/dist/{qr-http-server-s1iW_5VQ.js → qr-http-server-DCbjORFa.js} +76 -6
- package/dist/qr-http-server-DCbjORFa.js.map +1 -0
- package/dist/{qr-http-server-BH0UFeUn.js → qr-http-server-cKbciY5k.js} +76 -6
- package/dist/qr-http-server-cKbciY5k.js.map +1 -0
- package/dist/{relay-factory-Dug7W6Ao.js → relay-factory-Cloog1jb.js} +34 -14
- package/dist/relay-factory-Cloog1jb.js.map +1 -0
- package/dist/{relay-secret-store-DKxs7zwq.js → relay-secret-store-DzxDsK6S.js} +1 -1
- package/dist/{relay-secret-store-DKxs7zwq.js.map → relay-secret-store-DzxDsK6S.js.map} +1 -1
- package/dist/{relay-url-store-xmUuTjXA.js → relay-url-store-D3rY-GJJ.js} +2 -2
- package/dist/{relay-url-store-xmUuTjXA.js.map → relay-url-store-D3rY-GJJ.js.map} +1 -1
- package/dist/{debug-server-BzzNkETg.js → relay-worker-CCAHLC4a.js} +220 -350
- package/dist/relay-worker-CCAHLC4a.js.map +1 -0
- package/dist/{relay-worker-TReZtzGl.d.ts → relay-worker-o0M2ndyJ.d.ts} +7 -7
- package/dist/relay-worker-o0M2ndyJ.d.ts.map +1 -0
- package/dist/{runtime-ozyPnRbU.d.ts → runtime-9xhN9pr8.d.ts} +1 -1
- package/dist/{runtime-ozyPnRbU.d.ts.map → runtime-9xhN9pr8.d.ts.map} +1 -1
- package/dist/test-runner/bin.js +744 -25
- package/dist/test-runner/bin.js.map +1 -1
- package/dist/test-runner/bundle.d.ts +1 -1
- package/dist/test-runner/capture.d.ts +1 -1
- package/dist/test-runner/config.d.ts +15 -9
- package/dist/test-runner/config.d.ts.map +1 -1
- package/dist/test-runner/config.js +1 -1
- package/dist/test-runner/pool.d.ts +1 -1
- package/dist/test-runner/pool.js +1 -1
- package/dist/test-runner/pool.js.map +1 -1
- package/dist/test-runner/relay-factory.d.ts +23 -9
- package/dist/test-runner/relay-factory.d.ts.map +1 -1
- package/dist/test-runner/relay-factory.js +32 -12
- package/dist/test-runner/relay-factory.js.map +1 -1
- package/dist/test-runner/relay-worker.d.ts +1 -1
- package/dist/test-runner/relay-worker.js +1 -187
- package/dist/test-runner/report.d.ts +3 -3
- package/dist/test-runner/rpc.d.ts +2 -2
- package/dist/test-runner/runtime.d.ts +1 -1
- package/dist/test-runner/task-graph.d.ts +1 -1
- package/dist/{tunnel-DyEV6WwL.cjs → tunnel-B2UCKAc7.cjs} +4 -3
- package/dist/{tunnel-DyEV6WwL.cjs.map → tunnel-B2UCKAc7.cjs.map} +1 -1
- package/dist/{tunnel-BFwOSwT4.js → tunnel-C-rQ34dM.js} +4 -3
- package/dist/{tunnel-BFwOSwT4.js.map → tunnel-C-rQ34dM.js.map} +1 -1
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +3 -2
- package/dist/unplugin/tunnel.cjs.map +1 -1
- package/dist/unplugin/tunnel.d.cts.map +1 -1
- package/dist/unplugin/tunnel.d.ts.map +1 -1
- package/dist/unplugin/tunnel.js +3 -2
- package/dist/unplugin/tunnel.js.map +1 -1
- package/package.json +1 -1
- package/dist/attach-orchestrator-CPMQWkGV.js.map +0 -1
- package/dist/attach-orchestrator-CpE0dMew.js.map +0 -1
- package/dist/cell-Ba9lCI3B.js.map +0 -1
- package/dist/cell-CX92lDI0.js.map +0 -1
- package/dist/cell-D0dJSGHi.js.map +0 -1
- package/dist/debug-server-1dhDA3aw.js +0 -980
- package/dist/debug-server-1dhDA3aw.js.map +0 -1
- package/dist/debug-server-Dctc9GAh.js.map +0 -1
- package/dist/qr-http-server-BH0UFeUn.js.map +0 -1
- package/dist/qr-http-server-Bo8LMOJQ.js.map +0 -1
- package/dist/qr-http-server-Bt_RAxFn.cjs.map +0 -1
- package/dist/qr-http-server-CrGohOwC.js.map +0 -1
- package/dist/qr-http-server-DM2R25jP.cjs.map +0 -1
- package/dist/qr-http-server-s1iW_5VQ.js.map +0 -1
- package/dist/relay-factory-Dug7W6Ao.js.map +0 -1
- package/dist/relay-worker-TReZtzGl.d.ts.map +0 -1
- package/dist/test-runner/relay-worker.js.map +0 -1
|
@@ -1,17 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
import { n as
|
|
3
|
-
import "./test-runner/
|
|
4
|
-
import {
|
|
5
|
-
import "./
|
|
6
|
-
import "./qr-http-server-s1iW_5VQ.js";
|
|
7
|
-
import "./relay-secret-store-CFc9n0OA.js";
|
|
8
|
-
import { createRequire } from "node:module";
|
|
9
|
-
import "@modelcontextprotocol/sdk/server/index.js";
|
|
10
|
-
import "@modelcontextprotocol/sdk/server/stdio.js";
|
|
11
|
-
import "@modelcontextprotocol/sdk/types.js";
|
|
1
|
+
import { r as generateTotp } from "./totp-DfekTBk3.js";
|
|
2
|
+
import { n as logInfo } from "./log-CUik_tHp.js";
|
|
3
|
+
import { bundleTestFile } from "./test-runner/bundle.js";
|
|
4
|
+
import { parseCaptureLines } from "./test-runner/capture.js";
|
|
5
|
+
import { injectAndRunBundle } from "./test-runner/rpc.js";
|
|
12
6
|
import { EventEmitter } from "node:events";
|
|
13
|
-
import { WebSocket
|
|
14
|
-
import { createServer } from "node:http";
|
|
7
|
+
import { WebSocket } from "ws";
|
|
15
8
|
//#region src/shared/relay-auth-close.ts
|
|
16
9
|
/**
|
|
17
10
|
* Shared constants for the relay's named TOTP-auth rejection (issue #478).
|
|
@@ -75,6 +68,25 @@ const RELAY_AUTH_REJECT_REASON = "totp-rejected";
|
|
|
75
68
|
*/
|
|
76
69
|
/** Max events retained per domain ring buffer. */
|
|
77
70
|
const DEFAULT_BUFFER_SIZE = 500;
|
|
71
|
+
/**
|
|
72
|
+
* Substrings that mark a "relay websocket is dead" class error, as produced by
|
|
73
|
+
* `handleDisconnect('relay WebSocket 연결이 끊겼습니다')` (ws close handler) and
|
|
74
|
+
* the fail-fast `sendCommand` rejection (`relay에 연결되어 있지 않습니다 (...)`).
|
|
75
|
+
*
|
|
76
|
+
* Exported so callers outside this module (e.g. `test-runner/relay-worker.ts`)
|
|
77
|
+
* can detect the same error class without hardcoding a fragile full-sentence
|
|
78
|
+
* match — mirrors the `EVALUATE_TIMEOUT_MARKER` precedent in
|
|
79
|
+
* `test-runner/relay-worker.ts`. Kept in sync with `classifyToolError`'s
|
|
80
|
+
* `relayDisconnectError` branch in `mcp/errors.ts`.
|
|
81
|
+
*/
|
|
82
|
+
const RELAY_DISCONNECT_MARKERS = ["relay WebSocket", "relay에 연결되어 있지 않습니다"];
|
|
83
|
+
/**
|
|
84
|
+
* Returns true when `message` matches the relay-websocket-dead error class
|
|
85
|
+
* (socket closed, socket errored, or fail-fast "not connected" rejection).
|
|
86
|
+
*/
|
|
87
|
+
function isRelayDisconnectMessage(message) {
|
|
88
|
+
return RELAY_DISCONNECT_MARKERS.some((marker) => message.includes(marker));
|
|
89
|
+
}
|
|
78
90
|
function isObject(value) {
|
|
79
91
|
return typeof value === "object" && value !== null;
|
|
80
92
|
}
|
|
@@ -225,7 +237,11 @@ var ChiiCdpConnection = class {
|
|
|
225
237
|
* EventEmitter is missed (defensive belt-and-suspenders).
|
|
226
238
|
*
|
|
227
239
|
* @param filterFn - Predicate that the returned targets must satisfy.
|
|
228
|
-
* @param timeoutMs - Reject after this many ms (default 90 000).
|
|
240
|
+
* @param timeoutMs - Reject after this many ms (default 90 000). Pass a
|
|
241
|
+
* non-finite value (`Infinity`) to disable the rejection timer entirely —
|
|
242
|
+
* used by the test-runner's unbounded QR-attach wait (devtools#735). Node
|
|
243
|
+
* clamps `setTimeout(fn, Infinity)` to ~1ms, so a non-finite value MUST
|
|
244
|
+
* skip arming the timer rather than pass it through.
|
|
229
245
|
* @param pollIntervalMs - Fallback poll interval (default 500ms).
|
|
230
246
|
*/
|
|
231
247
|
waitForFirstTarget(filterFn, timeoutMs = 9e4, pollIntervalMs = 500) {
|
|
@@ -237,7 +253,7 @@ var ChiiCdpConnection = class {
|
|
|
237
253
|
const settle = (targets) => {
|
|
238
254
|
if (settled) return;
|
|
239
255
|
settled = true;
|
|
240
|
-
clearTimeout(timeoutHandle);
|
|
256
|
+
if (timeoutHandle !== null) clearTimeout(timeoutHandle);
|
|
241
257
|
if (pollHandle !== null) {
|
|
242
258
|
clearInterval(pollHandle);
|
|
243
259
|
pollHandle = null;
|
|
@@ -248,7 +264,7 @@ var ChiiCdpConnection = class {
|
|
|
248
264
|
const onAttach = (targets) => {
|
|
249
265
|
if (filterFn(targets)) settle(targets);
|
|
250
266
|
};
|
|
251
|
-
const timeoutHandle = setTimeout(() => {
|
|
267
|
+
const timeoutHandle = Number.isFinite(timeoutMs) ? setTimeout(() => {
|
|
252
268
|
if (settled) return;
|
|
253
269
|
settled = true;
|
|
254
270
|
if (pollHandle !== null) {
|
|
@@ -257,7 +273,7 @@ var ChiiCdpConnection = class {
|
|
|
257
273
|
}
|
|
258
274
|
this.emitter.off("target:attached", onAttach);
|
|
259
275
|
reject(/* @__PURE__ */ new Error(`waitForFirstTarget: 타임아웃 (${timeoutMs}ms) — 폰이 relay에 attach되지 않았습니다.`));
|
|
260
|
-
}, timeoutMs);
|
|
276
|
+
}, timeoutMs) : null;
|
|
261
277
|
this.emitter.on("target:attached", onAttach);
|
|
262
278
|
pollHandle = setInterval(() => {
|
|
263
279
|
this.refreshTargets().then((targets) => {
|
|
@@ -557,368 +573,222 @@ var ChiiCdpConnection = class {
|
|
|
557
573
|
}
|
|
558
574
|
};
|
|
559
575
|
//#endregion
|
|
560
|
-
//#region src/
|
|
576
|
+
//#region src/test-runner/relay-worker.ts
|
|
561
577
|
/**
|
|
562
|
-
*
|
|
563
|
-
*
|
|
564
|
-
*
|
|
565
|
-
* WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.
|
|
566
|
-
* The relay accepts a `target` websocket from the phone's injected `target.js`
|
|
567
|
-
* and `client` websockets from CDP frontends (our MCP connection).
|
|
568
|
-
*
|
|
569
|
-
* Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app
|
|
570
|
-
* entries.
|
|
571
|
-
*
|
|
572
|
-
* TOTP auth (relay-side, authoritative gate):
|
|
573
|
-
* When `verifyAuth` is provided, this module gates both inbound surfaces:
|
|
574
|
-
*
|
|
575
|
-
* - HTTP 'request': a listener registered BEFORE `chii.start({server})`.
|
|
576
|
-
* Node's `http.Server` calls listeners in registration order; the first
|
|
577
|
-
* to call `res.end()` wins. Invalid auth → 401 + CORS header + a tiny
|
|
578
|
-
* JSON body (`{"error":"totp-rejected"}`) so a cross-origin script
|
|
579
|
-
* `fetch()` probe can READ the status (issue #478). Valid auth → return
|
|
580
|
-
* without side-effect (chii's Koa handler serves it).
|
|
578
|
+
* Sentinel string embedded in the error message by `injectAndRunBundle` when
|
|
579
|
+
* the per-file evaluate race hits the timeout. Used by the retry guard so only
|
|
580
|
+
* genuine timeouts get a second attempt — not bundle errors or parse failures.
|
|
581
581
|
*
|
|
582
|
-
*
|
|
583
|
-
* listener, we take over the upgrade chain (remove chii's listeners,
|
|
584
|
-
* re-dispatch manually). Invalid auth → accept-then-close: complete the
|
|
585
|
-
* handshake via a `noServer` WebSocketServer, then immediately close
|
|
586
|
-
* with code 4401 reason 'totp-rejected' (issue #478). A raw 401 +
|
|
587
|
-
* `socket.destroy()` only ever surfaced as close code 1006 in the
|
|
588
|
-
* browser — indistinguishable from a tunnel failure, which left the
|
|
589
|
-
* env-2 phone UI silent. The explicit dispatch (not listener ordering)
|
|
590
|
-
* is what keeps chii away from rejected sockets: accept-then-close
|
|
591
|
-
* leaves the socket alive, so an order-based early-return would let
|
|
592
|
-
* chii's later listener complete a SECOND handshake on the same socket
|
|
593
|
-
* — an auth bypass. Valid auth → forward to chii's captured listeners.
|
|
594
|
-
*
|
|
595
|
-
* TOTP code transports (issue #466) — two equivalent ways to carry the code:
|
|
596
|
-
* 1. Query param `at=<code>` — used by the daemon-side `/client` connection
|
|
597
|
-
* (`chii-connection.ts` appends it; it holds the secret).
|
|
598
|
-
* 2. Path prefix `/at/<code>/…` — used by the phone-side target. Chii's
|
|
599
|
-
* stock `target.js` derives its WS endpoint from the script `src`
|
|
600
|
-
* (`scriptEl.src.replace('target.js','')`), so the only way for the
|
|
601
|
-
* phone to carry a code is to embed it in the script URL path. The
|
|
602
|
-
* in-app attach injects `https://<host>/at/<code>/target.js`; both the
|
|
603
|
-
* script fetch and the derived `wss://<host>/at/<code>/target/<id>` WS
|
|
604
|
-
* dial then carry the prefix. The listeners below rewrite the prefix
|
|
605
|
-
* into the query form (`rewriteAtPathPrefix`) and MUTATE `req.url`
|
|
606
|
-
* before chii's own handlers (registered later) parse it — chii only
|
|
607
|
-
* ever sees the stripped URL.
|
|
608
|
-
*
|
|
609
|
-
* Threat model: "URL leak" — someone obtains the tunnel URL (Slack paste, QR
|
|
610
|
-
* screenshot, shoulder-surfing) but does not have the shared TOTP secret.
|
|
611
|
-
* Rotating 6-digit code makes the URL stale after 30 s.
|
|
612
|
-
* A determined attacker who extracts the secret from the dogfood bundle can
|
|
613
|
-
* still compute valid codes; that is out of scope (see umbrella CLAUDE.md §4).
|
|
614
|
-
*
|
|
615
|
-
* SECRET-HANDLING: The secret value and computed TOTP codes MUST NOT appear
|
|
616
|
-
* in any log, error message, or process output. `verifyAuth` is a black-box
|
|
617
|
-
* predicate from the caller's perspective; this module only forwards pass/fail.
|
|
582
|
+
* Exported for unit tests that assert the retry path is taken.
|
|
618
583
|
*/
|
|
619
|
-
const
|
|
584
|
+
const EVALUATE_TIMEOUT_MARKER = "rpc: evaluate timed out after";
|
|
620
585
|
/**
|
|
621
|
-
*
|
|
586
|
+
* Runs all `files` sequentially over the given CDP `connection`.
|
|
622
587
|
*
|
|
623
|
-
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
/**
|
|
629
|
-
* Loads chii's internal WebSocketServer class and returns it together with a
|
|
630
|
-
* flag indicating whether the real class was found.
|
|
588
|
+
* For each file:
|
|
589
|
+
* 1. Bundle with esbuild (includes SDK shim + runtime).
|
|
590
|
+
* 2. Inject into the attached page via `Runtime.evaluate`.
|
|
591
|
+
* 3. Await the `RunReport` JSON response.
|
|
592
|
+
* 4. Accumulate results.
|
|
631
593
|
*
|
|
632
|
-
* Returns `
|
|
633
|
-
* changes the layout) — callers skip keepalive gracefully.
|
|
634
|
-
*/
|
|
635
|
-
function tryLoadChiiWssClass() {
|
|
636
|
-
try {
|
|
637
|
-
const mod = require("chii/server/lib/WebSocketServer");
|
|
638
|
-
if (typeof mod === "function") return mod;
|
|
639
|
-
} catch {}
|
|
640
|
-
return null;
|
|
641
|
-
}
|
|
642
|
-
/**
|
|
643
|
-
* Calls `chii.start()` and returns the chii `WebSocketServer` instance that
|
|
644
|
-
* was constructed during the call.
|
|
594
|
+
* Returns a `RelayRunReport` with per-file results and flattened totals.
|
|
645
595
|
*
|
|
646
|
-
*
|
|
647
|
-
*
|
|
648
|
-
* at module load time. The class reference is stable, so we can temporarily
|
|
649
|
-
* patch `ChiiWssClass.prototype.start` — which runs *on the instance* —
|
|
650
|
-
* to record `this` before the original `start` runs.
|
|
596
|
+
* This function does NOT open or manage the relay connection — the caller
|
|
597
|
+
* is responsible for attaching and closing it.
|
|
651
598
|
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
654
|
-
* call's patch overrides the previous in the prototype chain for the duration
|
|
655
|
-
* of its own `chii.start()` call, restoring the prior descriptor on exit.
|
|
599
|
+
* TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here
|
|
600
|
+
* so that `runTestFilesOverRelay` can be used as a Vitest pool entry.
|
|
656
601
|
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
659
|
-
*
|
|
602
|
+
* @param connection - Active CDP connection (relay or local kind).
|
|
603
|
+
* @param files - Absolute paths to test files, run in order.
|
|
604
|
+
* @param opts - Optional per-run overrides.
|
|
660
605
|
*/
|
|
661
|
-
async function
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
let captured = null;
|
|
667
|
-
const proto = ChiiWssClass.prototype;
|
|
668
|
-
const originalStart = proto.start;
|
|
669
|
-
proto.start = function(server) {
|
|
670
|
-
captured = this;
|
|
671
|
-
return originalStart.call(this, server);
|
|
672
|
-
};
|
|
606
|
+
async function runTestFilesOverRelay(connection, files, opts) {
|
|
607
|
+
const wallStart = Date.now();
|
|
608
|
+
const startedAt = new Date(wallStart).toISOString();
|
|
609
|
+
const fileResults = [];
|
|
610
|
+
let domainsEnabled = false;
|
|
673
611
|
try {
|
|
674
|
-
await
|
|
675
|
-
|
|
676
|
-
|
|
612
|
+
await connection.enableDomains();
|
|
613
|
+
domainsEnabled = true;
|
|
614
|
+
} catch (e) {
|
|
615
|
+
process.stderr.write(`relay-worker: enableDomains() failed before run — console capture may be empty (${e instanceof Error ? e.message : String(e)})\n`);
|
|
677
616
|
}
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
/**
|
|
686
|
-
*
|
|
687
|
-
*
|
|
688
|
-
*
|
|
689
|
-
*
|
|
690
|
-
*
|
|
691
|
-
*
|
|
692
|
-
*
|
|
693
|
-
*
|
|
694
|
-
*
|
|
695
|
-
*
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
*
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
* needed (TOTP codes are 6 digits and never percent-encoded in practice).
|
|
703
|
-
*/
|
|
704
|
-
function rewriteAtPathPrefix(rawUrl) {
|
|
705
|
-
const match = /^\/at\/([^/?]+)(\/[^?]*)?(\?.*)?$/.exec(rawUrl);
|
|
706
|
-
if (match === null) return null;
|
|
707
|
-
const code = match[1];
|
|
708
|
-
const path = match[2] === void 0 || match[2] === "" ? "/" : match[2];
|
|
709
|
-
const query = match[3] ?? "";
|
|
710
|
-
return `${path}${query}${query === "" ? "?" : "&"}at=${code}`;
|
|
711
|
-
}
|
|
712
|
-
/**
|
|
713
|
-
* Starts the Chii relay and resolves once listening.
|
|
714
|
-
*
|
|
715
|
-
* Default port is 0 (OS-assigned). With port 0 the OS picks a free ephemeral
|
|
716
|
-
* port on every start, so a stale cloudflared orphan holding any particular
|
|
717
|
-
* port cannot cause EADDRINUSE. The resolved `ChiiRelay.port` and `baseUrl`
|
|
718
|
-
* always reflect the actual bound port.
|
|
719
|
-
*
|
|
720
|
-
* chii.start() is called with `server` (our pre-created httpServer) BEFORE
|
|
721
|
-
* httpServer.listen(). This is intentional: chii attaches its Koa handler and
|
|
722
|
-
* WS upgrade listener to the server object, but the actual TCP bind is
|
|
723
|
-
* performed by our httpServer.listen() call below. The `port`/`domain` values
|
|
724
|
-
* passed to chii.start() are used for display/banner purposes inside chii and
|
|
725
|
-
* do not affect which port the server binds. The connection path (clients
|
|
726
|
-
* connecting to `relay.baseUrl`) always uses the post-listen confirmed port.
|
|
727
|
-
*/
|
|
728
|
-
async function startChiiRelay(options = {}) {
|
|
729
|
-
const requestedPort = options.port ?? 0;
|
|
730
|
-
const host = options.host ?? "127.0.0.1";
|
|
731
|
-
const { verifyAuth, onAuthReject } = options;
|
|
732
|
-
const keepaliveIntervalMs = options.keepaliveIntervalMs !== void 0 ? options.keepaliveIntervalMs : DEFAULT_KEEPALIVE_INTERVAL_MS;
|
|
733
|
-
const httpServer = createServer();
|
|
734
|
-
const notifyAuthReject = (kind) => {
|
|
735
|
-
if (onAuthReject === void 0) return;
|
|
617
|
+
const collectCaptures = opts?.collectCaptures === true;
|
|
618
|
+
const liveConsole = [];
|
|
619
|
+
let unsubscribeConsole;
|
|
620
|
+
if (collectCaptures && domainsEnabled) unsubscribeConsole = connection.on("Runtime.consoleAPICalled", (event) => {
|
|
621
|
+
liveConsole.push(event);
|
|
622
|
+
});
|
|
623
|
+
let pendingReconnectCheck = false;
|
|
624
|
+
/**
|
|
625
|
+
* Attempts one `enableDomains()` reconnect. Logs the attempt and outcome to
|
|
626
|
+
* stderr. Never throws — a failed reconnect just means the caller proceeds
|
|
627
|
+
* as today (each remaining file / the retry will fail-fast on the still-dead
|
|
628
|
+
* socket).
|
|
629
|
+
*
|
|
630
|
+
* `context` selects the fixed log literal: 'next-file' for the between-files
|
|
631
|
+
* case (a file's FINAL result was a WS-dead-class error — the primary #731
|
|
632
|
+
* scenario), 'retry-precheck' for the defensive reconnect before retrying a
|
|
633
|
+
* timed-out file (cheap no-op via `enableDomains()`'s idempotency when the
|
|
634
|
+
* socket never actually died).
|
|
635
|
+
*
|
|
636
|
+
* SECRET-HANDLING: fixed literals only — no relay wss URL, TOTP code, or
|
|
637
|
+
* tunnel host.
|
|
638
|
+
*/
|
|
639
|
+
const attemptReconnect = async (context) => {
|
|
640
|
+
process.stderr.write(context === "next-file" ? "relay-worker: relay connection lost — attempting reconnect before next file\n" : "relay-worker: evaluate timed out — attempting reconnect before retry\n");
|
|
736
641
|
try {
|
|
737
|
-
|
|
738
|
-
|
|
642
|
+
await connection.enableDomains();
|
|
643
|
+
process.stderr.write("relay-worker: reconnect succeeded\n");
|
|
644
|
+
} catch (e) {
|
|
645
|
+
process.stderr.write(`relay-worker: reconnect failed (${e instanceof Error ? e.message : String(e)}) — continuing, remaining files may fail\n`);
|
|
646
|
+
}
|
|
739
647
|
};
|
|
740
|
-
|
|
741
|
-
const
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
res.statusCode = 401;
|
|
746
|
-
res.setHeader("Access-Control-Allow-Origin", "*");
|
|
747
|
-
res.setHeader("Content-Type", "application/json");
|
|
748
|
-
res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
|
|
749
|
-
notifyAuthReject("http-request");
|
|
648
|
+
try {
|
|
649
|
+
for (const file of files) {
|
|
650
|
+
if (pendingReconnectCheck) {
|
|
651
|
+
await attemptReconnect("next-file");
|
|
652
|
+
pendingReconnectCheck = false;
|
|
750
653
|
}
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
654
|
+
let fileEntry;
|
|
655
|
+
try {
|
|
656
|
+
const { code } = await bundleTestFile(file, opts?.bundleOptions);
|
|
657
|
+
/**
|
|
658
|
+
* Runs one evaluate attempt and returns a FileResult, or `null` when the
|
|
659
|
+
* result is a genuine timeout and the caller should retry.
|
|
660
|
+
*
|
|
661
|
+
* We need to distinguish:
|
|
662
|
+
* - `rpcResult.ok = false` + timeout error → retry candidate (`return null`)
|
|
663
|
+
* - `rpcResult.ok = false` + other error → final error, no retry
|
|
664
|
+
* - `injectAndRunBundle` throws → CDP exceptionDetails OR a
|
|
665
|
+
* relay-disconnect rejection from `connection.send()` (page engine
|
|
666
|
+
* threw, or the ws died mid-evaluate); treated as a final
|
|
667
|
+
* (non-retryable) error either way.
|
|
668
|
+
*
|
|
669
|
+
* The Promise.race timeout in rpc.ts RETURNS `{ok:false, error: '…'}` (it
|
|
670
|
+
* does NOT throw/reject). Only genuine CDP `exceptionDetails` (or a dead
|
|
671
|
+
* `connection.send()`) cause a throw. This distinction is what makes the
|
|
672
|
+
* EVALUATE_TIMEOUT_MARKER gate below reachable — the timeout result
|
|
673
|
+
* surfaces as `rpcResult.ok=false` with the marker string, not as a
|
|
674
|
+
* caught exception.
|
|
675
|
+
*/
|
|
676
|
+
const attempt = async () => {
|
|
677
|
+
let rpcResult;
|
|
678
|
+
try {
|
|
679
|
+
rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);
|
|
680
|
+
} catch (e) {
|
|
681
|
+
return {
|
|
682
|
+
file,
|
|
683
|
+
result: { error: e instanceof Error ? e.message : String(e) }
|
|
684
|
+
};
|
|
685
|
+
}
|
|
686
|
+
if (rpcResult.ok) return {
|
|
687
|
+
file,
|
|
688
|
+
result: rpcResult.report
|
|
689
|
+
};
|
|
690
|
+
if (rpcResult.error.includes("rpc: evaluate timed out after")) return null;
|
|
691
|
+
return {
|
|
692
|
+
file,
|
|
693
|
+
result: { error: rpcResult.error }
|
|
694
|
+
};
|
|
695
|
+
};
|
|
696
|
+
const firstResult = await attempt();
|
|
697
|
+
if (firstResult !== null) fileEntry = firstResult;
|
|
698
|
+
else {
|
|
699
|
+
await attemptReconnect("retry-precheck");
|
|
700
|
+
process.stderr.write(`relay-worker: evaluate timed out for ${file} — retrying once\n`);
|
|
701
|
+
const retryResult = await attempt();
|
|
702
|
+
if (retryResult !== null) fileEntry = retryResult;
|
|
703
|
+
else fileEntry = {
|
|
704
|
+
file,
|
|
705
|
+
result: { error: `${EVALUATE_TIMEOUT_MARKER} ${opts?.timeoutMs ?? 6e4}ms (after retry)` }
|
|
706
|
+
};
|
|
707
|
+
}
|
|
708
|
+
} catch (e) {
|
|
709
|
+
fileEntry = {
|
|
710
|
+
file,
|
|
711
|
+
result: { error: e instanceof Error ? e.message : String(e) }
|
|
712
|
+
};
|
|
762
713
|
}
|
|
763
|
-
|
|
714
|
+
if ("error" in fileEntry.result && isRelayDisconnectMessage(fileEntry.result.error)) pendingReconnectCheck = true;
|
|
715
|
+
fileResults.push(fileEntry);
|
|
764
716
|
}
|
|
765
|
-
}
|
|
766
|
-
|
|
767
|
-
const capturedChiiWss = await startChiiWithCapture(loadChiiServer(), {
|
|
768
|
-
server: httpServer,
|
|
769
|
-
domain: `${host}:${requestedPort}`,
|
|
770
|
-
port: requestedPort
|
|
771
|
-
}, chiiWssClass);
|
|
772
|
-
if (verifyAuth) {
|
|
773
|
-
const chiiUpgradeListeners = httpServer.listeners("upgrade");
|
|
774
|
-
httpServer.removeAllListeners("upgrade");
|
|
775
|
-
const rejectWss = new WebSocketServer({ noServer: true });
|
|
776
|
-
httpServer.on("upgrade", (req, socket, head) => {
|
|
777
|
-
const rewritten = rewriteAtPathPrefix(req.url ?? "");
|
|
778
|
-
if (rewritten !== null) req.url = rewritten;
|
|
779
|
-
if (!verifyAuth(req)) {
|
|
780
|
-
rejectWss.handleUpgrade(req, socket, head, (ws) => {
|
|
781
|
-
ws.close(RELAY_AUTH_REJECT_CLOSE_CODE, RELAY_AUTH_REJECT_REASON);
|
|
782
|
-
});
|
|
783
|
-
notifyAuthReject("ws-upgrade");
|
|
784
|
-
return;
|
|
785
|
-
}
|
|
786
|
-
for (const listener of chiiUpgradeListeners) listener(req, socket, head);
|
|
787
|
-
});
|
|
717
|
+
} finally {
|
|
718
|
+
unsubscribeConsole?.();
|
|
788
719
|
}
|
|
789
|
-
const
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
}
|
|
720
|
+
const captures = collectCaptures ? parseCaptureLines(liveConsole.map((e) => ({ text: renderConsoleLineText(e) }))) : [];
|
|
721
|
+
const totals = fileResults.reduce((acc, { result }) => {
|
|
722
|
+
if ("error" in result) {
|
|
723
|
+
acc.failed += 1;
|
|
724
|
+
acc.total += 1;
|
|
725
|
+
} else {
|
|
726
|
+
acc.passed += result.passed;
|
|
727
|
+
acc.failed += result.failed;
|
|
728
|
+
acc.skipped += result.skipped;
|
|
729
|
+
acc.total += result.passed + result.failed + result.skipped;
|
|
730
|
+
}
|
|
731
|
+
return acc;
|
|
732
|
+
}, {
|
|
733
|
+
passed: 0,
|
|
734
|
+
failed: 0,
|
|
735
|
+
skipped: 0,
|
|
736
|
+
total: 0
|
|
795
737
|
});
|
|
796
|
-
let keepaliveHandle = null;
|
|
797
|
-
if (keepaliveIntervalMs > 0 && capturedChiiWss !== null) {
|
|
798
|
-
const chiiWss = capturedChiiWss;
|
|
799
|
-
keepaliveHandle = setInterval(() => {
|
|
800
|
-
for (const client of chiiWss._wss.clients) if (client.readyState === 1) client.ping();
|
|
801
|
-
}, keepaliveIntervalMs);
|
|
802
|
-
}
|
|
803
738
|
return {
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
keepaliveHandle = null;
|
|
810
|
-
}
|
|
811
|
-
httpServer.close(() => resolve());
|
|
812
|
-
})
|
|
739
|
+
startedAt,
|
|
740
|
+
duration: Date.now() - wallStart,
|
|
741
|
+
files: fileResults,
|
|
742
|
+
totals,
|
|
743
|
+
captures
|
|
813
744
|
};
|
|
814
745
|
}
|
|
815
|
-
//#endregion
|
|
816
|
-
//#region src/mcp/debug-server.ts
|
|
817
746
|
/**
|
|
818
|
-
*
|
|
819
|
-
*
|
|
820
|
-
*
|
|
821
|
-
*
|
|
822
|
-
*
|
|
823
|
-
*
|
|
824
|
-
*
|
|
825
|
-
*
|
|
826
|
-
*
|
|
747
|
+
* Renders one `Runtime.consoleAPICalled` event to a single line of text, the
|
|
748
|
+
* same way `tools.ts#normalizeConsoleMessage` does (args rendered + space-
|
|
749
|
+
* joined). Inlined here (≈8 lines) so this module avoids importing `tools.ts`,
|
|
750
|
+
* which would drag the heavy MCP/Node graph (server-lock, parent-watcher, …)
|
|
751
|
+
* onto the test-runner entry.
|
|
752
|
+
*
|
|
753
|
+
* SECRET-HANDLING: this only stringifies console args; the caller's
|
|
754
|
+
* allowlist-prefix parser then discards everything that is not a genuine
|
|
755
|
+
* `__AIT_CAPTURE__` line.
|
|
827
756
|
*/
|
|
828
|
-
function
|
|
829
|
-
return
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
757
|
+
function renderConsoleLineText(event) {
|
|
758
|
+
return event.args.map((arg) => {
|
|
759
|
+
if (arg.value !== void 0) {
|
|
760
|
+
if (typeof arg.value === "string") return arg.value;
|
|
761
|
+
try {
|
|
762
|
+
return JSON.stringify(arg.value);
|
|
763
|
+
} catch {
|
|
764
|
+
return String(arg.value);
|
|
765
|
+
}
|
|
766
|
+
}
|
|
767
|
+
if (arg.description !== void 0) return arg.description;
|
|
768
|
+
if (arg.className !== void 0) return arg.className;
|
|
769
|
+
return arg.subtype ?? arg.type;
|
|
770
|
+
}).join(" ");
|
|
833
771
|
}
|
|
834
772
|
/**
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
* to the relay's confirmed port in the background, prints the attach banner,
|
|
838
|
-
* and arms the tunnel health probe. Returns a {@link BootedFamily} whose
|
|
839
|
-
* `getTunnelStatus()` reflects the live tunnel (it flips up once the background
|
|
840
|
-
* tunnel resolves and follows reissues).
|
|
841
|
-
*
|
|
842
|
-
* Booted lazily via the dual router's `bootLazyFor('relay-intoss')` callback
|
|
843
|
-
* (symmetry with {@link bootLocalFamily}), at most once on the first
|
|
844
|
-
* `start_debug({ mode: 'relay-staging' })` (all-lazy, #396 — every relay boot now
|
|
845
|
-
* flows through `switchMode` after the project-local secret load). `relay-live`
|
|
846
|
-
* removed (#665).
|
|
847
|
-
*
|
|
848
|
-
* The relay base URL is only known after `startChiiRelay()` resolves, so the
|
|
849
|
-
* `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside
|
|
850
|
-
* this function, after the relay port is confirmed.
|
|
851
|
-
*
|
|
852
|
-
* SECRET-HANDLING: the TOTP secret rides only inside `verifyAuth`; the wssUrl
|
|
853
|
-
* (relay host) is never logged here directly.
|
|
773
|
+
* Flattens all test results from a `RelayRunReport` into a single array.
|
|
774
|
+
* Files that errored during bundle/inject produce a synthetic failed entry.
|
|
854
775
|
*/
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
const
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
776
|
+
function flattenResults(report) {
|
|
777
|
+
const out = [];
|
|
778
|
+
for (const { file, result } of report.files) if ("error" in result) out.push({
|
|
779
|
+
file,
|
|
780
|
+
name: `<bundle/inject error>`,
|
|
781
|
+
status: "fail",
|
|
782
|
+
duration: 0,
|
|
783
|
+
error: result.error
|
|
863
784
|
});
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
785
|
+
else for (const t of result.tests) out.push({
|
|
786
|
+
...t,
|
|
787
|
+
file
|
|
867
788
|
});
|
|
868
|
-
|
|
869
|
-
let tunnelStatus = makeTunnelStatus(false, null);
|
|
870
|
-
let tunnelProbe = null;
|
|
871
|
-
generateAttachToken();
|
|
872
|
-
startQuickTunnel(relay.port).then((t) => {
|
|
873
|
-
tunnel = t;
|
|
874
|
-
tunnelStatus = makeTunnelStatus(true, t.wssUrl);
|
|
875
|
-
options.onWssUrl?.(t.wssUrl);
|
|
876
|
-
if (t.childPid !== void 0) options.onTunnelChildPid?.(t.childPid);
|
|
877
|
-
logInfo("tunnel.up", { totpEnabled });
|
|
878
|
-
tunnelProbe = startTunnelHealthProbe(t, relay.port, {
|
|
879
|
-
onReissue: (newTunnel) => {
|
|
880
|
-
tunnel = newTunnel;
|
|
881
|
-
tunnelStatus = makeTunnelStatus(true, newTunnel.wssUrl, null, 0);
|
|
882
|
-
options.onWssUrl?.(newTunnel.wssUrl);
|
|
883
|
-
if (newTunnel.childPid !== void 0) options.onTunnelChildPid?.(newTunnel.childPid);
|
|
884
|
-
printAttachBanner({
|
|
885
|
-
wssUrl: newTunnel.wssUrl,
|
|
886
|
-
totpEnabled
|
|
887
|
-
}).then(() => {
|
|
888
|
-
logInfo("tunnel.up", {
|
|
889
|
-
totpEnabled,
|
|
890
|
-
reissued: true
|
|
891
|
-
});
|
|
892
|
-
});
|
|
893
|
-
},
|
|
894
|
-
onPermanentDrop: (droppedAt) => {
|
|
895
|
-
tunnelStatus = makeTunnelStatus(false, null, droppedAt, 3);
|
|
896
|
-
logError("tunnel.down", { msg: `tunnel permanently dropped (${droppedAt}). Restart: npx @ait-co/devtools devtools-mcp` });
|
|
897
|
-
options.onTunnelDown?.();
|
|
898
|
-
}
|
|
899
|
-
});
|
|
900
|
-
return printAttachBanner({
|
|
901
|
-
wssUrl: t.wssUrl,
|
|
902
|
-
totpEnabled
|
|
903
|
-
});
|
|
904
|
-
}, (err) => {
|
|
905
|
-
logError("tunnel.down", { msg: `Failed to open cloudflared quick tunnel: ${err instanceof Error ? err.message : String(err)}. The relay is up locally; attach over the public URL is unavailable until the tunnel starts.` });
|
|
906
|
-
});
|
|
907
|
-
const connection = createRelayConnection(relay.baseUrl);
|
|
908
|
-
return {
|
|
909
|
-
connection,
|
|
910
|
-
relayOrigin: "intoss-webview",
|
|
911
|
-
relayHttpUrl: relay.baseUrl,
|
|
912
|
-
getTunnelStatus: () => tunnelStatus,
|
|
913
|
-
stop() {
|
|
914
|
-
tunnelProbe?.stop();
|
|
915
|
-
tunnel?.stop();
|
|
916
|
-
connection.close();
|
|
917
|
-
relay.close();
|
|
918
|
-
}
|
|
919
|
-
};
|
|
789
|
+
return out;
|
|
920
790
|
}
|
|
921
791
|
//#endregion
|
|
922
|
-
export {
|
|
792
|
+
export { RELAY_AUTH_REJECT_CLOSE_CODE as a, ChiiCdpConnection as i, flattenResults as n, RELAY_AUTH_REJECT_REASON as o, runTestFilesOverRelay as r, EVALUATE_TIMEOUT_MARKER as t };
|
|
923
793
|
|
|
924
|
-
//# sourceMappingURL=
|
|
794
|
+
//# sourceMappingURL=relay-worker-CCAHLC4a.js.map
|