@ait-co/devtools 0.1.137 → 0.1.139
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-Cjdhir2U.js → attach-orchestrator-6kz_ZdWL.js} +5 -3
- package/dist/{attach-orchestrator-Cjdhir2U.js.map → attach-orchestrator-6kz_ZdWL.js.map} +1 -1
- package/dist/{attach-orchestrator-CE0S09YU.js → attach-orchestrator-BEiz3wAx.js} +5 -3
- package/dist/{attach-orchestrator-CMoDuG2A.js.map → attach-orchestrator-BEiz3wAx.js.map} +1 -1
- package/dist/{attach-orchestrator-CMoDuG2A.js → attach-orchestrator-BG8TGJOZ.js} +5 -3
- package/dist/{attach-orchestrator-CE0S09YU.js.map → attach-orchestrator-BG8TGJOZ.js.map} +1 -1
- package/dist/{bundle-BKqyhEK9.d.ts → bundle-C796JIwG.d.ts} +65 -2
- package/dist/bundle-C796JIwG.d.ts.map +1 -0
- package/dist/{capture-ltuV0gZa.d.ts → capture-DsP525OZ.d.ts} +1 -1
- package/dist/{capture-ltuV0gZa.d.ts.map → capture-DsP525OZ.d.ts.map} +1 -1
- package/dist/{cdp-connection-BYE9meXe.d.ts → cdp-connection-rP1WdnH5.d.ts} +1 -1
- package/dist/{cdp-connection-BYE9meXe.d.ts.map → cdp-connection-rP1WdnH5.d.ts.map} +1 -1
- package/dist/{cell-D1y4shoV.js → cell-BMlFh08P.js} +2 -2
- package/dist/cell-BMlFh08P.js.map +1 -0
- package/dist/{cell-DHA578lX.js → cell-Dtgtusog.js} +90 -16
- package/dist/cell-Dtgtusog.js.map +1 -0
- package/dist/{cell-BIdSQb9T.js → cell-KsRXFdDs.js} +122 -16
- package/dist/cell-KsRXFdDs.js.map +1 -0
- package/dist/{debug-server-CBK7dn8O.js → debug-server-6c22qSWa.js} +132 -9
- package/dist/debug-server-6c22qSWa.js.map +1 -0
- package/dist/{debug-server-BQplCzf8.js → debug-server-BQHbaKPT.js} +134 -11
- package/dist/debug-server-BQHbaKPT.js.map +1 -0
- package/dist/{debug-server-D2wpPK3_.js → debug-server-DWat7d-9.js} +82 -6
- package/dist/debug-server-DWat7d-9.js.map +1 -0
- package/dist/devtools-opener-3Drge_RJ.js +75 -0
- package/dist/devtools-opener-3Drge_RJ.js.map +1 -0
- package/dist/devtools-opener-CJpEsXXQ.js +76 -0
- package/dist/devtools-opener-CJpEsXXQ.js.map +1 -0
- package/dist/devtools-opener-CxtryS8c.js +75 -0
- package/dist/devtools-opener-CxtryS8c.js.map +1 -0
- package/dist/in-app/auto.js +59 -12
- package/dist/in-app/auto.js.map +1 -1
- package/dist/in-app/index.d.ts +21 -2
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +60 -13
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.js +299 -60
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/mock/index.d.ts +79 -1
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +218 -14
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +171 -10
- package/dist/panel/index.js.map +1 -1
- package/dist/{pool-CmfvXnwh.d.ts → pool-CrP5CPvU.d.ts} +3 -3
- package/dist/{pool-CmfvXnwh.d.ts.map → pool-CrP5CPvU.d.ts.map} +1 -1
- package/dist/{qr-http-server-ZkT6F7is.js → qr-http-server-D1hpoyBF.js} +1 -1
- package/dist/{qr-http-server-ZkT6F7is.js.map → qr-http-server-D1hpoyBF.js.map} +1 -1
- package/dist/qr-http-server-D4rGz8M7.js +1640 -0
- package/dist/qr-http-server-D4rGz8M7.js.map +1 -0
- package/dist/{qr-http-server-C7q8Gn04.js → qr-http-server-DIjCV7h_.js} +1 -1
- package/dist/{qr-http-server-C7q8Gn04.js.map → qr-http-server-DIjCV7h_.js.map} +1 -1
- package/dist/{relay-factory-DVzpBRxz.js → relay-factory-C1X3G3WY.js} +71 -15
- package/dist/relay-factory-C1X3G3WY.js.map +1 -0
- package/dist/{relay-secret-store-CLEGyHou.js → relay-secret-store-Bmyleu0A.js} +1 -1
- package/dist/{relay-secret-store-CLEGyHou.js.map → relay-secret-store-Bmyleu0A.js.map} +1 -1
- package/dist/{relay-secret-store-DhzAnnj-.js → relay-secret-store-CQenfcSL.js} +2 -2
- package/dist/{relay-secret-store-DhzAnnj-.js.map → relay-secret-store-CQenfcSL.js.map} +1 -1
- package/dist/{relay-secret-store-BcVrWwTq.js → relay-secret-store-DKxs7zwq.js} +1 -1
- package/dist/{relay-secret-store-BcVrWwTq.js.map → relay-secret-store-DKxs7zwq.js.map} +1 -1
- package/dist/{relay-secret-store-DGduVJhs.js → relay-secret-store-WJ8EGkIl.js} +1 -1
- package/dist/{relay-secret-store-DGduVJhs.js.map → relay-secret-store-WJ8EGkIl.js.map} +1 -1
- package/dist/{relay-url-store-CKW8RQzf.js → relay-url-store-BR2XodiO.js} +2 -2
- package/dist/{relay-url-store-CKW8RQzf.js.map → relay-url-store-BR2XodiO.js.map} +1 -1
- package/dist/{relay-url-store-CdA58fgw.js → relay-url-store-CH63fVCm.js} +2 -2
- package/dist/{relay-url-store-B0X8TsGr.js.map → relay-url-store-CH63fVCm.js.map} +1 -1
- package/dist/{relay-url-store-CwKT7i04.js → relay-url-store-DaY1QPes.js} +2 -2
- package/dist/{relay-url-store-CwKT7i04.js.map → relay-url-store-DaY1QPes.js.map} +1 -1
- package/dist/{relay-url-store-B0X8TsGr.js → relay-url-store-xmUuTjXA.js} +2 -2
- package/dist/{relay-url-store-CdA58fgw.js.map → relay-url-store-xmUuTjXA.js.map} +1 -1
- package/dist/{relay-worker-6fy1eaIo.js → relay-worker-BUXI0K0b.js} +8 -4
- package/dist/{relay-worker-6fy1eaIo.js.map → relay-worker-BUXI0K0b.js.map} +1 -1
- package/dist/{relay-worker-DJnZkXza.d.ts → relay-worker-DcyboK43.d.ts} +30 -5
- package/dist/relay-worker-DcyboK43.d.ts.map +1 -0
- package/dist/{runtime-DfHHZms2.d.ts → runtime-BiigOuvb.d.ts} +30 -3
- package/dist/runtime-BiigOuvb.d.ts.map +1 -0
- package/dist/test-runner/bin.js +376 -38
- package/dist/test-runner/bin.js.map +1 -1
- package/dist/test-runner/bundle.d.ts +2 -2
- package/dist/test-runner/bundle.js +66 -13
- package/dist/test-runner/bundle.js.map +1 -1
- package/dist/test-runner/capture.d.ts +1 -1
- package/dist/test-runner/config.d.ts +58 -5
- package/dist/test-runner/config.d.ts.map +1 -1
- package/dist/test-runner/config.js +1 -1
- package/dist/test-runner/method-pace.d.ts +82 -0
- package/dist/test-runner/method-pace.d.ts.map +1 -0
- package/dist/test-runner/method-pace.js +120 -0
- package/dist/test-runner/method-pace.js.map +1 -0
- package/dist/test-runner/pool.d.ts +1 -1
- package/dist/test-runner/pool.js +1 -1
- package/dist/test-runner/relay-factory.d.ts +57 -4
- package/dist/test-runner/relay-factory.d.ts.map +1 -1
- package/dist/test-runner/relay-factory.js +70 -14
- 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 -1
- 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 +2 -2
- package/dist/test-runner/runtime.js +83 -6
- package/dist/test-runner/runtime.js.map +1 -1
- package/dist/test-runner/task-graph.d.ts +1 -1
- package/dist/throttle-DKKzX1qC.js +59 -0
- package/dist/throttle-DKKzX1qC.js.map +1 -0
- package/dist/totp-Dwft0Kz7.js +3 -0
- package/package.json +1 -1
- package/dist/bundle-BKqyhEK9.d.ts.map +0 -1
- package/dist/cell-BIdSQb9T.js.map +0 -1
- package/dist/cell-D1y4shoV.js.map +0 -1
- package/dist/cell-DHA578lX.js.map +0 -1
- package/dist/debug-server-BQplCzf8.js.map +0 -1
- package/dist/debug-server-CBK7dn8O.js.map +0 -1
- package/dist/debug-server-D2wpPK3_.js.map +0 -1
- package/dist/relay-factory-DVzpBRxz.js.map +0 -1
- package/dist/relay-worker-DJnZkXza.d.ts.map +0 -1
- package/dist/runtime-DfHHZms2.d.ts.map +0 -1
- package/dist/totp-D1pulXLa.js +0 -3
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/mcp/devtools-opener.ts
|
|
2
|
+
/**
|
|
3
|
+
* Assembles the Chii self-hosted DevTools inspector URL for a given relay
|
|
4
|
+
* and target.
|
|
5
|
+
*
|
|
6
|
+
* Chii serves its own DevTools frontend at
|
|
7
|
+
* `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)
|
|
8
|
+
* or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form
|
|
9
|
+
* `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used
|
|
10
|
+
* by Chii's own target list page (derived from `chii/public/index.js`).
|
|
11
|
+
*
|
|
12
|
+
* The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid
|
|
13
|
+
* for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =
|
|
14
|
+
* 180–210 s). The developer must open the returned URL within that window.
|
|
15
|
+
* If the window expires before the browser connects, the relay will reject the
|
|
16
|
+
* WebSocket upgrade with close code 4401.
|
|
17
|
+
*
|
|
18
|
+
* FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.
|
|
19
|
+
* `undefined`), this function returns `null` — the caller must treat `null` as
|
|
20
|
+
* "inspector not yet available" and show a waiting hint instead of a broken
|
|
21
|
+
* link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built
|
|
22
|
+
* without `at=` would be rejected with WS 4401 immediately — there is no
|
|
23
|
+
* non-TOTP relay path in production. Returning `null` surfaces this cleanly as
|
|
24
|
+
* a "TOTP not yet configured" state rather than silently producing a URL that
|
|
25
|
+
* will always fail at the WS handshake.
|
|
26
|
+
*
|
|
27
|
+
* SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is
|
|
28
|
+
* embedded in the `wss=` parameter (inside the `at=` param) of the returned
|
|
29
|
+
* URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is
|
|
30
|
+
* the intended fallback surface for the developer to copy the URL).
|
|
31
|
+
*
|
|
32
|
+
* @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.
|
|
33
|
+
* `http://127.0.0.1:9100`. No trailing slash.
|
|
34
|
+
* @param targetId - Chii target id (from `GET <relay>/targets`).
|
|
35
|
+
* @param mintTotp - Function that returns a fresh 6-digit TOTP code string.
|
|
36
|
+
* Called at most once. **Required** — when `undefined`, the function returns
|
|
37
|
+
* `null` (fail-closed: no `at=` param means the relay WS gate rejects the
|
|
38
|
+
* handshake, so a null result is safer than a URL that always 404s).
|
|
39
|
+
* @param panel - Initial panel. Defaults to `"console"`.
|
|
40
|
+
*
|
|
41
|
+
* @returns The inspector URL string, or `null` when `mintTotp` is absent.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* buildChiiInspectorUrl(
|
|
45
|
+
* 'http://127.0.0.1:9100',
|
|
46
|
+
* 'abc123',
|
|
47
|
+
* () => generateTotp(secret),
|
|
48
|
+
* )
|
|
49
|
+
* // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'
|
|
50
|
+
*/
|
|
51
|
+
function buildChiiInspectorUrl(relayHttpBaseUrl, targetId, mintTotp, panel = "console") {
|
|
52
|
+
if (!mintTotp) return null;
|
|
53
|
+
let relayHost;
|
|
54
|
+
let wsParamName;
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new URL(relayHttpBaseUrl);
|
|
57
|
+
relayHost = parsed.host;
|
|
58
|
+
wsParamName = parsed.protocol === "https:" ? "wss" : "ws";
|
|
59
|
+
} catch {
|
|
60
|
+
relayHost = relayHttpBaseUrl.replace(/^https?:\/\//i, "");
|
|
61
|
+
wsParamName = /^https:/i.test(relayHttpBaseUrl) ? "wss" : "ws";
|
|
62
|
+
}
|
|
63
|
+
const clientId = `devtools-opener-${Date.now().toString(36)}`;
|
|
64
|
+
const code = mintTotp();
|
|
65
|
+
const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;
|
|
66
|
+
const params = new URLSearchParams({
|
|
67
|
+
[wsParamName]: wsPath,
|
|
68
|
+
panel
|
|
69
|
+
});
|
|
70
|
+
return `${relayHttpBaseUrl.replace(/\/$/, "")}/front_end/chii_app.html?${params.toString()}`;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { buildChiiInspectorUrl };
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=devtools-opener-3Drge_RJ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools-opener-3Drge_RJ.js","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for ~3 minutes (the relay gate\n * accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). If the developer\n * does not open the URL within that window the WebSocket upgrade will be\n * rejected with 4401. In practice the browser opens immediately after the OS\n * `open` command; if needed the developer can copy the wss= param, replace\n * `at=`, and reload. This is documented in the JSDoc below.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chii self-hosted DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chii self-hosted DevTools inspector URL for a given relay\n * and target.\n *\n * Chii serves its own DevTools frontend at\n * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)\n * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form\n * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used\n * by Chii's own target list page (derived from `chii/public/index.js`).\n *\n * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid\n * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =\n * 180–210 s). The developer must open the returned URL within that window.\n * If the window expires before the browser connects, the relay will reject the\n * WebSocket upgrade with close code 4401.\n *\n * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.\n * `undefined`), this function returns `null` — the caller must treat `null` as\n * \"inspector not yet available\" and show a waiting hint instead of a broken\n * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built\n * without `at=` would be rejected with WS 4401 immediately — there is no\n * non-TOTP relay path in production. Returning `null` surfaces this cleanly as\n * a \"TOTP not yet configured\" state rather than silently producing a URL that\n * will always fail at the WS handshake.\n *\n * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is\n * embedded in the `wss=` parameter (inside the `at=` param) of the returned\n * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is\n * the intended fallback surface for the developer to copy the URL).\n *\n * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.\n * `http://127.0.0.1:9100`. No trailing slash.\n * @param targetId - Chii target id (from `GET <relay>/targets`).\n * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.\n * Called at most once. **Required** — when `undefined`, the function returns\n * `null` (fail-closed: no `at=` param means the relay WS gate rejects the\n * handshake, so a null result is safer than a URL that always 404s).\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @returns The inspector URL string, or `null` when `mintTotp` is absent.\n *\n * @example\n * buildChiiInspectorUrl(\n * 'http://127.0.0.1:9100',\n * 'abc123',\n * () => generateTotp(secret),\n * )\n * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'\n */\nexport function buildChiiInspectorUrl(\n relayHttpBaseUrl: string,\n targetId: string,\n mintTotp?: () => string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string | null {\n // FAIL-CLOSED (#509): relay sessions require TOTP for every WS upgrade.\n // Without a mintTotp function we cannot produce a valid at= code, so we\n // return null rather than a URL that will always be rejected by the relay gate\n // with WS 4401 / HTTP 404. Callers show a \"waiting\" hint when they get null.\n if (!mintTotp) {\n return null;\n }\n\n // Extract the host (and port) from the relay HTTP base URL, and pick the\n // query param name chii_app.html expects: `ws=` dials `ws://` (plain-HTTP\n // relay — env 3/4 local 127.0.0.1) while `wss=` dials `wss://` (HTTPS\n // tunnel — env 2). chii/public/index.js does the same scheme branch:\n // `location.protocol === 'https:' ? 'wss' : 'ws'`. Always sending `wss=`\n // would make the frontend attempt TLS against the plain-HTTP local relay.\n let relayHost: string;\n let wsParamName: 'ws' | 'wss';\n try {\n const parsed = new URL(relayHttpBaseUrl);\n relayHost = parsed.host; // e.g. \"127.0.0.1:9100\"\n wsParamName = parsed.protocol === 'https:' ? 'wss' : 'ws';\n } catch {\n // Fallback: strip the scheme prefix manually if URL parsing fails.\n relayHost = relayHttpBaseUrl.replace(/^https?:\\/\\//i, '');\n wsParamName = /^https:/i.test(relayHttpBaseUrl) ? 'wss' : 'ws';\n }\n\n // Generate a client UUID that matches the format Chii's index.js uses\n // (6 random alphanumeric characters).\n const clientId = `devtools-opener-${Date.now().toString(36)}`;\n\n // Build the ws=/wss= value: \"<relay-host>/client/<uuid>?target=<id>&at=<code>\"\n // This mirrors the format from chii/public/index.js:\n // `${domain}${basePath}client/${randomId(6)}?target=${targetId}`\n // SECRET-HANDLING: mintTotp() returns a code (not a secret). The code\n // rides only in the URL's at= param. Callers must not log the URL.\n const code = mintTotp();\n const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;\n\n const params = new URLSearchParams({ [wsParamName]: wsPath, panel });\n return `${relayHttpBaseUrl.replace(/\\/$/, '')}/front_end/chii_app.html?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled**.\n *\n * Default (env var absent or any value other than `\"1\"`) is **disabled** —\n * the developer uses the \"디버그 툴 열기\" button on the /attach or dashboard\n * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic\n * browser-open behaviour on device attach.\n *\n * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward\n * compatibility (same effect as absent).\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS !== '1';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link AutoDevtoolsOpener.open}.\n *\n * The `relayHttpBaseUrl` and `targetId` fields are required to build a working\n * Chii self-hosted inspector URL. When `relayHttpBaseUrl` is absent the open\n * is skipped (no relay available yet).\n */\nexport interface DevtoolsOpenOptions {\n /**\n * Stable local inspector URL (`http://127.0.0.1:<port>/inspector`) from the\n * QR HTTP server (issue #530). When provided this URL is opened in the browser\n * instead of building a direct `front_end/chii_app.html?wss=…` URL. The\n * `/inspector` endpoint mints a fresh TOTP at click time and redirects, so\n * there is no TOTP-expiry race. Safe to log (no tunnel host, no TOTP code).\n *\n * When absent, falls back to building a direct inspector URL from\n * `relayHttpBaseUrl` + `mintTotp` (legacy path, kept for backward compat).\n */\n inspectorStableUrl?: string | null;\n /**\n * Local HTTP base URL of the Chii relay, e.g. `http://127.0.0.1:9100`.\n * Used to build the `<relay-base>/front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is not available.\n *\n * For env 3/4 (intoss relay) this is `http://127.0.0.1:<port>`.\n * For env 2 (external PWA relay) this is the relay's external HTTP URL\n * (e.g. `https://<host>.trycloudflare.com`).\n *\n * When absent or empty, `open()` is a no-op.\n *\n * SECRET-HANDLING: this value contains the relay host. Callers MUST NOT\n * log it to stdout; stderr is the intended surface.\n */\n relayHttpBaseUrl: string | null | undefined;\n /**\n * Chii target id of the attached page, from `listTargets()[0].id`.\n * When absent or empty, `open()` is a no-op.\n */\n targetId: string | null | undefined;\n /**\n * Function that mints a fresh TOTP code when called. Called at most once per\n * `open()` invocation, immediately before building the inspector URL.\n * Only used when `inspectorStableUrl` is absent.\n *\n * Pass `undefined` when TOTP is disabled (no `at=` param is added).\n *\n * SECRET-HANDLING: the function MUST return only the code (6 digits), not\n * the secret. The code rides in the URL's `at=` param only.\n */\n mintTotp?: () => string;\n /** Current MCP environment (`mock` | `relay`). `open()` no-ops on `mock`. */\n env: McpEnvironment;\n}\n\n/**\n * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).\n *\n * The open fires for each NEW `targetId` — subsequent notifications for the\n * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after\n * page reload on the phone) fires a new open. The URL opened is the stable\n * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —\n * it mints a fresh TOTP at click time so there is no expiry race. Falls back to\n * building a direct `front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is absent.\n *\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */\n private readonly _openedTargets = new Set<string>();\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Opens when:\n * - `options.targetId` is a NEW target (not yet in `_openedTargets`).\n *\n * No-op when any of the following conditions hold:\n * 1. `targetId` has already been opened (`_openedTargets` has it).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. `options.env` is `mock` (env 1 — F12 is already available).\n * 4. `options.targetId` is null/undefined/empty (no page attached yet).\n * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.\n *\n * When `inspectorStableUrl` is provided (issue #530 stable URL): opens\n * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.\n * The URL contains no tunnel host or TOTP code — safe to log anywhere.\n *\n * Legacy path (no `inspectorStableUrl`): builds a direct\n * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`\n * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).\n *\n * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay\n * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or\n * persistent logs.\n */\n open(options: DevtoolsOpenOptions): void {\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.targetId) return;\n\n // Target-unit de-dupe (issue #530): re-attach with a new targetId fires again.\n const targetId = options.targetId;\n if (this._openedTargets.has(targetId)) return;\n\n // Use stable /inspector URL when available (issue #530) — secret-free, no expiry.\n if (options.inspectorStableUrl) {\n this._openedTargets.add(targetId);\n const stableUrl = options.inspectorStableUrl;\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace('/inspector', '')})의 \"디버그 툴 열기\" 버튼을 눌러 DevTools를 여세요.\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n',\n );\n const opened = openUrlInBrowser(stableUrl);\n if (!opened) {\n process.stderr.write(\n `[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\\n`,\n );\n }\n return;\n }\n\n // Legacy path: build direct inspector URL from relayHttpBaseUrl + mintTotp.\n if (!options.relayHttpBaseUrl) return;\n\n this._openedTargets.add(targetId);\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n targetId,\n options.mintTotp,\n );\n\n // FAIL-CLOSED (#509): buildChiiInspectorUrl returns null when mintTotp is\n // absent (no valid at= code → relay WS gate would reject the connection).\n // Record targetId in set so this guard fires, but skip browser open.\n if (inspectorUrl === null) {\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\\n' +\n '[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\\n',\n );\n return;\n }\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n' +\n '[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.\\n',\n );\n\n const opened = openUrlInBrowser(inspectorUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /**\n * Returns `true` if `open()` has been called for at least one target.\n * (Replaces the old once-per-session `_opened` flag; kept for interface\n * compatibility with tests that read `opener.opened`.)\n */\n get opened(): boolean {\n return this._openedTargets.size > 0;\n }\n\n /** Returns the set of target IDs that have already been auto-opened. */\n get openedTargets(): ReadonlySet<string> {\n return this._openedTargets;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4GA,SAAgB,sBACd,kBACA,UACA,UACA,QAAwD,WACzC;AAKf,KAAI,CAAC,SACH,QAAO;CAST,IAAI;CACJ,IAAI;AACJ,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,iBAAiB;AACxC,cAAY,OAAO;AACnB,gBAAc,OAAO,aAAa,WAAW,QAAQ;SAC/C;AAEN,cAAY,iBAAiB,QAAQ,iBAAiB,GAAG;AACzD,gBAAc,WAAW,KAAK,iBAAiB,GAAG,QAAQ;;CAK5D,MAAM,WAAW,mBAAmB,KAAK,KAAK,CAAC,SAAS,GAAG;CAO3D,MAAM,OAAO,UAAU;CACvB,MAAM,SAAS,GAAG,UAAU,UAAU,SAAS,UAAU,mBAAmB,SAAS,CAAC,MAAM,mBAAmB,KAAK;CAEpH,MAAM,SAAS,IAAI,gBAAgB;GAAG,cAAc;EAAQ;EAAO,CAAC;AACpE,QAAO,GAAG,iBAAiB,QAAQ,OAAO,GAAG,CAAC,2BAA2B,OAAO,UAAU"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
//#region src/mcp/devtools-opener.ts
|
|
3
|
+
/**
|
|
4
|
+
* Assembles the Chii self-hosted DevTools inspector URL for a given relay
|
|
5
|
+
* and target.
|
|
6
|
+
*
|
|
7
|
+
* Chii serves its own DevTools frontend at
|
|
8
|
+
* `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)
|
|
9
|
+
* or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form
|
|
10
|
+
* `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used
|
|
11
|
+
* by Chii's own target list page (derived from `chii/public/index.js`).
|
|
12
|
+
*
|
|
13
|
+
* The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid
|
|
14
|
+
* for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =
|
|
15
|
+
* 180–210 s). The developer must open the returned URL within that window.
|
|
16
|
+
* If the window expires before the browser connects, the relay will reject the
|
|
17
|
+
* WebSocket upgrade with close code 4401.
|
|
18
|
+
*
|
|
19
|
+
* FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.
|
|
20
|
+
* `undefined`), this function returns `null` — the caller must treat `null` as
|
|
21
|
+
* "inspector not yet available" and show a waiting hint instead of a broken
|
|
22
|
+
* link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built
|
|
23
|
+
* without `at=` would be rejected with WS 4401 immediately — there is no
|
|
24
|
+
* non-TOTP relay path in production. Returning `null` surfaces this cleanly as
|
|
25
|
+
* a "TOTP not yet configured" state rather than silently producing a URL that
|
|
26
|
+
* will always fail at the WS handshake.
|
|
27
|
+
*
|
|
28
|
+
* SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is
|
|
29
|
+
* embedded in the `wss=` parameter (inside the `at=` param) of the returned
|
|
30
|
+
* URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is
|
|
31
|
+
* the intended fallback surface for the developer to copy the URL).
|
|
32
|
+
*
|
|
33
|
+
* @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.
|
|
34
|
+
* `http://127.0.0.1:9100`. No trailing slash.
|
|
35
|
+
* @param targetId - Chii target id (from `GET <relay>/targets`).
|
|
36
|
+
* @param mintTotp - Function that returns a fresh 6-digit TOTP code string.
|
|
37
|
+
* Called at most once. **Required** — when `undefined`, the function returns
|
|
38
|
+
* `null` (fail-closed: no `at=` param means the relay WS gate rejects the
|
|
39
|
+
* handshake, so a null result is safer than a URL that always 404s).
|
|
40
|
+
* @param panel - Initial panel. Defaults to `"console"`.
|
|
41
|
+
*
|
|
42
|
+
* @returns The inspector URL string, or `null` when `mintTotp` is absent.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* buildChiiInspectorUrl(
|
|
46
|
+
* 'http://127.0.0.1:9100',
|
|
47
|
+
* 'abc123',
|
|
48
|
+
* () => generateTotp(secret),
|
|
49
|
+
* )
|
|
50
|
+
* // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'
|
|
51
|
+
*/
|
|
52
|
+
function buildChiiInspectorUrl(relayHttpBaseUrl, targetId, mintTotp, panel = "console") {
|
|
53
|
+
if (!mintTotp) return null;
|
|
54
|
+
let relayHost;
|
|
55
|
+
let wsParamName;
|
|
56
|
+
try {
|
|
57
|
+
const parsed = new URL(relayHttpBaseUrl);
|
|
58
|
+
relayHost = parsed.host;
|
|
59
|
+
wsParamName = parsed.protocol === "https:" ? "wss" : "ws";
|
|
60
|
+
} catch {
|
|
61
|
+
relayHost = relayHttpBaseUrl.replace(/^https?:\/\//i, "");
|
|
62
|
+
wsParamName = /^https:/i.test(relayHttpBaseUrl) ? "wss" : "ws";
|
|
63
|
+
}
|
|
64
|
+
const clientId = `devtools-opener-${Date.now().toString(36)}`;
|
|
65
|
+
const code = mintTotp();
|
|
66
|
+
const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;
|
|
67
|
+
const params = new URLSearchParams({
|
|
68
|
+
[wsParamName]: wsPath,
|
|
69
|
+
panel
|
|
70
|
+
});
|
|
71
|
+
return `${relayHttpBaseUrl.replace(/\/$/, "")}/front_end/chii_app.html?${params.toString()}`;
|
|
72
|
+
}
|
|
73
|
+
//#endregion
|
|
74
|
+
export { buildChiiInspectorUrl };
|
|
75
|
+
|
|
76
|
+
//# sourceMappingURL=devtools-opener-CJpEsXXQ.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools-opener-CJpEsXXQ.js","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for ~3 minutes (the relay gate\n * accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). If the developer\n * does not open the URL within that window the WebSocket upgrade will be\n * rejected with 4401. In practice the browser opens immediately after the OS\n * `open` command; if needed the developer can copy the wss= param, replace\n * `at=`, and reload. This is documented in the JSDoc below.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chii self-hosted DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chii self-hosted DevTools inspector URL for a given relay\n * and target.\n *\n * Chii serves its own DevTools frontend at\n * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)\n * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form\n * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used\n * by Chii's own target list page (derived from `chii/public/index.js`).\n *\n * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid\n * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =\n * 180–210 s). The developer must open the returned URL within that window.\n * If the window expires before the browser connects, the relay will reject the\n * WebSocket upgrade with close code 4401.\n *\n * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.\n * `undefined`), this function returns `null` — the caller must treat `null` as\n * \"inspector not yet available\" and show a waiting hint instead of a broken\n * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built\n * without `at=` would be rejected with WS 4401 immediately — there is no\n * non-TOTP relay path in production. Returning `null` surfaces this cleanly as\n * a \"TOTP not yet configured\" state rather than silently producing a URL that\n * will always fail at the WS handshake.\n *\n * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is\n * embedded in the `wss=` parameter (inside the `at=` param) of the returned\n * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is\n * the intended fallback surface for the developer to copy the URL).\n *\n * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.\n * `http://127.0.0.1:9100`. No trailing slash.\n * @param targetId - Chii target id (from `GET <relay>/targets`).\n * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.\n * Called at most once. **Required** — when `undefined`, the function returns\n * `null` (fail-closed: no `at=` param means the relay WS gate rejects the\n * handshake, so a null result is safer than a URL that always 404s).\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @returns The inspector URL string, or `null` when `mintTotp` is absent.\n *\n * @example\n * buildChiiInspectorUrl(\n * 'http://127.0.0.1:9100',\n * 'abc123',\n * () => generateTotp(secret),\n * )\n * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'\n */\nexport function buildChiiInspectorUrl(\n relayHttpBaseUrl: string,\n targetId: string,\n mintTotp?: () => string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string | null {\n // FAIL-CLOSED (#509): relay sessions require TOTP for every WS upgrade.\n // Without a mintTotp function we cannot produce a valid at= code, so we\n // return null rather than a URL that will always be rejected by the relay gate\n // with WS 4401 / HTTP 404. Callers show a \"waiting\" hint when they get null.\n if (!mintTotp) {\n return null;\n }\n\n // Extract the host (and port) from the relay HTTP base URL, and pick the\n // query param name chii_app.html expects: `ws=` dials `ws://` (plain-HTTP\n // relay — env 3/4 local 127.0.0.1) while `wss=` dials `wss://` (HTTPS\n // tunnel — env 2). chii/public/index.js does the same scheme branch:\n // `location.protocol === 'https:' ? 'wss' : 'ws'`. Always sending `wss=`\n // would make the frontend attempt TLS against the plain-HTTP local relay.\n let relayHost: string;\n let wsParamName: 'ws' | 'wss';\n try {\n const parsed = new URL(relayHttpBaseUrl);\n relayHost = parsed.host; // e.g. \"127.0.0.1:9100\"\n wsParamName = parsed.protocol === 'https:' ? 'wss' : 'ws';\n } catch {\n // Fallback: strip the scheme prefix manually if URL parsing fails.\n relayHost = relayHttpBaseUrl.replace(/^https?:\\/\\//i, '');\n wsParamName = /^https:/i.test(relayHttpBaseUrl) ? 'wss' : 'ws';\n }\n\n // Generate a client UUID that matches the format Chii's index.js uses\n // (6 random alphanumeric characters).\n const clientId = `devtools-opener-${Date.now().toString(36)}`;\n\n // Build the ws=/wss= value: \"<relay-host>/client/<uuid>?target=<id>&at=<code>\"\n // This mirrors the format from chii/public/index.js:\n // `${domain}${basePath}client/${randomId(6)}?target=${targetId}`\n // SECRET-HANDLING: mintTotp() returns a code (not a secret). The code\n // rides only in the URL's at= param. Callers must not log the URL.\n const code = mintTotp();\n const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;\n\n const params = new URLSearchParams({ [wsParamName]: wsPath, panel });\n return `${relayHttpBaseUrl.replace(/\\/$/, '')}/front_end/chii_app.html?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled**.\n *\n * Default (env var absent or any value other than `\"1\"`) is **disabled** —\n * the developer uses the \"디버그 툴 열기\" button on the /attach or dashboard\n * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic\n * browser-open behaviour on device attach.\n *\n * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward\n * compatibility (same effect as absent).\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS !== '1';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link AutoDevtoolsOpener.open}.\n *\n * The `relayHttpBaseUrl` and `targetId` fields are required to build a working\n * Chii self-hosted inspector URL. When `relayHttpBaseUrl` is absent the open\n * is skipped (no relay available yet).\n */\nexport interface DevtoolsOpenOptions {\n /**\n * Stable local inspector URL (`http://127.0.0.1:<port>/inspector`) from the\n * QR HTTP server (issue #530). When provided this URL is opened in the browser\n * instead of building a direct `front_end/chii_app.html?wss=…` URL. The\n * `/inspector` endpoint mints a fresh TOTP at click time and redirects, so\n * there is no TOTP-expiry race. Safe to log (no tunnel host, no TOTP code).\n *\n * When absent, falls back to building a direct inspector URL from\n * `relayHttpBaseUrl` + `mintTotp` (legacy path, kept for backward compat).\n */\n inspectorStableUrl?: string | null;\n /**\n * Local HTTP base URL of the Chii relay, e.g. `http://127.0.0.1:9100`.\n * Used to build the `<relay-base>/front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is not available.\n *\n * For env 3/4 (intoss relay) this is `http://127.0.0.1:<port>`.\n * For env 2 (external PWA relay) this is the relay's external HTTP URL\n * (e.g. `https://<host>.trycloudflare.com`).\n *\n * When absent or empty, `open()` is a no-op.\n *\n * SECRET-HANDLING: this value contains the relay host. Callers MUST NOT\n * log it to stdout; stderr is the intended surface.\n */\n relayHttpBaseUrl: string | null | undefined;\n /**\n * Chii target id of the attached page, from `listTargets()[0].id`.\n * When absent or empty, `open()` is a no-op.\n */\n targetId: string | null | undefined;\n /**\n * Function that mints a fresh TOTP code when called. Called at most once per\n * `open()` invocation, immediately before building the inspector URL.\n * Only used when `inspectorStableUrl` is absent.\n *\n * Pass `undefined` when TOTP is disabled (no `at=` param is added).\n *\n * SECRET-HANDLING: the function MUST return only the code (6 digits), not\n * the secret. The code rides in the URL's `at=` param only.\n */\n mintTotp?: () => string;\n /** Current MCP environment (`mock` | `relay`). `open()` no-ops on `mock`. */\n env: McpEnvironment;\n}\n\n/**\n * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).\n *\n * The open fires for each NEW `targetId` — subsequent notifications for the\n * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after\n * page reload on the phone) fires a new open. The URL opened is the stable\n * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —\n * it mints a fresh TOTP at click time so there is no expiry race. Falls back to\n * building a direct `front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is absent.\n *\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */\n private readonly _openedTargets = new Set<string>();\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Opens when:\n * - `options.targetId` is a NEW target (not yet in `_openedTargets`).\n *\n * No-op when any of the following conditions hold:\n * 1. `targetId` has already been opened (`_openedTargets` has it).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. `options.env` is `mock` (env 1 — F12 is already available).\n * 4. `options.targetId` is null/undefined/empty (no page attached yet).\n * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.\n *\n * When `inspectorStableUrl` is provided (issue #530 stable URL): opens\n * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.\n * The URL contains no tunnel host or TOTP code — safe to log anywhere.\n *\n * Legacy path (no `inspectorStableUrl`): builds a direct\n * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`\n * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).\n *\n * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay\n * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or\n * persistent logs.\n */\n open(options: DevtoolsOpenOptions): void {\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.targetId) return;\n\n // Target-unit de-dupe (issue #530): re-attach with a new targetId fires again.\n const targetId = options.targetId;\n if (this._openedTargets.has(targetId)) return;\n\n // Use stable /inspector URL when available (issue #530) — secret-free, no expiry.\n if (options.inspectorStableUrl) {\n this._openedTargets.add(targetId);\n const stableUrl = options.inspectorStableUrl;\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace('/inspector', '')})의 \"디버그 툴 열기\" 버튼을 눌러 DevTools를 여세요.\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n',\n );\n const opened = openUrlInBrowser(stableUrl);\n if (!opened) {\n process.stderr.write(\n `[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\\n`,\n );\n }\n return;\n }\n\n // Legacy path: build direct inspector URL from relayHttpBaseUrl + mintTotp.\n if (!options.relayHttpBaseUrl) return;\n\n this._openedTargets.add(targetId);\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n targetId,\n options.mintTotp,\n );\n\n // FAIL-CLOSED (#509): buildChiiInspectorUrl returns null when mintTotp is\n // absent (no valid at= code → relay WS gate would reject the connection).\n // Record targetId in set so this guard fires, but skip browser open.\n if (inspectorUrl === null) {\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\\n' +\n '[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\\n',\n );\n return;\n }\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n' +\n '[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.\\n',\n );\n\n const opened = openUrlInBrowser(inspectorUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /**\n * Returns `true` if `open()` has been called for at least one target.\n * (Replaces the old once-per-session `_opened` flag; kept for interface\n * compatibility with tests that read `opener.opened`.)\n */\n get opened(): boolean {\n return this._openedTargets.size > 0;\n }\n\n /** Returns the set of target IDs that have already been auto-opened. */\n get openedTargets(): ReadonlySet<string> {\n return this._openedTargets;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4GA,SAAgB,sBACd,kBACA,UACA,UACA,QAAwD,WACzC;AAKf,KAAI,CAAC,SACH,QAAO;CAST,IAAI;CACJ,IAAI;AACJ,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,iBAAiB;AACxC,cAAY,OAAO;AACnB,gBAAc,OAAO,aAAa,WAAW,QAAQ;SAC/C;AAEN,cAAY,iBAAiB,QAAQ,iBAAiB,GAAG;AACzD,gBAAc,WAAW,KAAK,iBAAiB,GAAG,QAAQ;;CAK5D,MAAM,WAAW,mBAAmB,KAAK,KAAK,CAAC,SAAS,GAAG;CAO3D,MAAM,OAAO,UAAU;CACvB,MAAM,SAAS,GAAG,UAAU,UAAU,SAAS,UAAU,mBAAmB,SAAS,CAAC,MAAM,mBAAmB,KAAK;CAEpH,MAAM,SAAS,IAAI,gBAAgB;GAAG,cAAc;EAAQ;EAAO,CAAC;AACpE,QAAO,GAAG,iBAAiB,QAAQ,OAAO,GAAG,CAAC,2BAA2B,OAAO,UAAU"}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
//#region src/mcp/devtools-opener.ts
|
|
2
|
+
/**
|
|
3
|
+
* Assembles the Chii self-hosted DevTools inspector URL for a given relay
|
|
4
|
+
* and target.
|
|
5
|
+
*
|
|
6
|
+
* Chii serves its own DevTools frontend at
|
|
7
|
+
* `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)
|
|
8
|
+
* or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form
|
|
9
|
+
* `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used
|
|
10
|
+
* by Chii's own target list page (derived from `chii/public/index.js`).
|
|
11
|
+
*
|
|
12
|
+
* The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid
|
|
13
|
+
* for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =
|
|
14
|
+
* 180–210 s). The developer must open the returned URL within that window.
|
|
15
|
+
* If the window expires before the browser connects, the relay will reject the
|
|
16
|
+
* WebSocket upgrade with close code 4401.
|
|
17
|
+
*
|
|
18
|
+
* FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.
|
|
19
|
+
* `undefined`), this function returns `null` — the caller must treat `null` as
|
|
20
|
+
* "inspector not yet available" and show a waiting hint instead of a broken
|
|
21
|
+
* link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built
|
|
22
|
+
* without `at=` would be rejected with WS 4401 immediately — there is no
|
|
23
|
+
* non-TOTP relay path in production. Returning `null` surfaces this cleanly as
|
|
24
|
+
* a "TOTP not yet configured" state rather than silently producing a URL that
|
|
25
|
+
* will always fail at the WS handshake.
|
|
26
|
+
*
|
|
27
|
+
* SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is
|
|
28
|
+
* embedded in the `wss=` parameter (inside the `at=` param) of the returned
|
|
29
|
+
* URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is
|
|
30
|
+
* the intended fallback surface for the developer to copy the URL).
|
|
31
|
+
*
|
|
32
|
+
* @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.
|
|
33
|
+
* `http://127.0.0.1:9100`. No trailing slash.
|
|
34
|
+
* @param targetId - Chii target id (from `GET <relay>/targets`).
|
|
35
|
+
* @param mintTotp - Function that returns a fresh 6-digit TOTP code string.
|
|
36
|
+
* Called at most once. **Required** — when `undefined`, the function returns
|
|
37
|
+
* `null` (fail-closed: no `at=` param means the relay WS gate rejects the
|
|
38
|
+
* handshake, so a null result is safer than a URL that always 404s).
|
|
39
|
+
* @param panel - Initial panel. Defaults to `"console"`.
|
|
40
|
+
*
|
|
41
|
+
* @returns The inspector URL string, or `null` when `mintTotp` is absent.
|
|
42
|
+
*
|
|
43
|
+
* @example
|
|
44
|
+
* buildChiiInspectorUrl(
|
|
45
|
+
* 'http://127.0.0.1:9100',
|
|
46
|
+
* 'abc123',
|
|
47
|
+
* () => generateTotp(secret),
|
|
48
|
+
* )
|
|
49
|
+
* // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'
|
|
50
|
+
*/
|
|
51
|
+
function buildChiiInspectorUrl(relayHttpBaseUrl, targetId, mintTotp, panel = "console") {
|
|
52
|
+
if (!mintTotp) return null;
|
|
53
|
+
let relayHost;
|
|
54
|
+
let wsParamName;
|
|
55
|
+
try {
|
|
56
|
+
const parsed = new URL(relayHttpBaseUrl);
|
|
57
|
+
relayHost = parsed.host;
|
|
58
|
+
wsParamName = parsed.protocol === "https:" ? "wss" : "ws";
|
|
59
|
+
} catch {
|
|
60
|
+
relayHost = relayHttpBaseUrl.replace(/^https?:\/\//i, "");
|
|
61
|
+
wsParamName = /^https:/i.test(relayHttpBaseUrl) ? "wss" : "ws";
|
|
62
|
+
}
|
|
63
|
+
const clientId = `devtools-opener-${Date.now().toString(36)}`;
|
|
64
|
+
const code = mintTotp();
|
|
65
|
+
const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;
|
|
66
|
+
const params = new URLSearchParams({
|
|
67
|
+
[wsParamName]: wsPath,
|
|
68
|
+
panel
|
|
69
|
+
});
|
|
70
|
+
return `${relayHttpBaseUrl.replace(/\/$/, "")}/front_end/chii_app.html?${params.toString()}`;
|
|
71
|
+
}
|
|
72
|
+
//#endregion
|
|
73
|
+
export { buildChiiInspectorUrl };
|
|
74
|
+
|
|
75
|
+
//# sourceMappingURL=devtools-opener-CxtryS8c.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"devtools-opener-CxtryS8c.js","names":[],"sources":["../src/mcp/devtools-opener.ts"],"sourcesContent":["/**\n * Auto-opens Chrome DevTools when a page attaches over the Chii relay.\n *\n * When a real device attaches (env 2 / 3 / 4 in the 4-environments fidelity\n * ladder), the Chii relay exposes a standard CDP WebSocket endpoint. Chii\n * also self-hosts its DevTools frontend at:\n *\n * <relay-base>/front_end/chii_app.html\n * ?ws|wss=<encodeURIComponent(\"<relay-host>/client/<uuid>?target=<targetId>&at=<totp>\")>\n *\n * The param name follows the relay base scheme — `ws=` for plain HTTP\n * (env 3/4 local relay), `wss=` for HTTPS (env 2 tunnel) — matching the\n * scheme branch in chii/public/index.js.\n *\n * This is the same URL format that Chii's own index-page inspect-links use\n * (derived from `chii/public/index.js` — the JS that powers the target list\n * page at `<relay-base>/`). Opening this URL in the developer's local browser\n * gives a full Chrome DevTools UI connected to the phone via the relay.\n *\n * IMPORTANT — environment guard:\n * Auto-open only fires in relay environments (env 2 / 3 / 4). In env 1\n * (local browser + mock SDK) the developer already has F12 available; opening\n * a DevTools window pointing at the mock relay would be confusing and useless.\n * The caller (`startAttachWatcher` in `debug-server.ts`) passes the current\n * environment and this module bails out when it is `mock`.\n *\n * Opt-out: set `AIT_AUTO_DEVTOOLS=0` in the environment to suppress auto-open\n * entirely. Any other value (or absent) enables the default behaviour.\n *\n * Duplicate-open guard:\n * `AutoDevtoolsOpener` tracks whether open was already triggered for the\n * current session. The open fires at most once per instance — typically one\n * per `runDebugServer` call.\n *\n * TOTP expiry caveat:\n * The `at=` TOTP code embedded in the `wss=` parameter is minted fresh at the\n * moment `open()` is called. The code is valid for ~3 minutes (the relay gate\n * accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps = 180–210 s). If the developer\n * does not open the URL within that window the WebSocket upgrade will be\n * rejected with 4401. In practice the browser opens immediately after the OS\n * `open` command; if needed the developer can copy the wss= param, replace\n * `at=`, and reload. This is documented in the JSDoc below.\n *\n * PWA (WebKit) caveat:\n * The Chii relay injects a chobitsu CDP shim into WebKit-based runtimes (env 2\n * AITC Sandbox PWA). The DevTools frontend will connect and most panels work.\n * However, WebKit does not expose the full CDP domain set that V8/Blink does,\n * so some panels (Network, Layers) may appear empty or show limited data.\n * This is a WebKit runtime constraint, not a relay or devtools-opener issue.\n *\n * Node-only: uses `child_process.spawnSync` to invoke the OS open command.\n */\n\nimport type { McpEnvironment } from './environment.js';\n\n// ---------------------------------------------------------------------------\n// Chii self-hosted DevTools frontend URL\n// ---------------------------------------------------------------------------\n\n/**\n * Assembles the Chii self-hosted DevTools inspector URL for a given relay\n * and target.\n *\n * Chii serves its own DevTools frontend at\n * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)\n * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form\n * `<relay-host>/client/<uuid>?target=<id>&at=<totp>` — the same format used\n * by Chii's own target list page (derived from `chii/public/index.js`).\n *\n * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid\n * for ~3 minutes (relay gate accepts ±RELAY_VERIFY_SKEW_STEPS=6 steps =\n * 180–210 s). The developer must open the returned URL within that window.\n * If the window expires before the browser connects, the relay will reject the\n * WebSocket upgrade with close code 4401.\n *\n * FAIL-CLOSED (issue #509): `mintTotp` is REQUIRED. When omitted (i.e.\n * `undefined`), this function returns `null` — the caller must treat `null` as\n * \"inspector not yet available\" and show a waiting hint instead of a broken\n * link. Relay sessions gate every WS upgrade with TOTP (#452), so a URL built\n * without `at=` would be rejected with WS 4401 immediately — there is no\n * non-TOTP relay path in production. Returning `null` surfaces this cleanly as\n * a \"TOTP not yet configured\" state rather than silently producing a URL that\n * will always fail at the WS handshake.\n *\n * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is\n * embedded in the `wss=` parameter (inside the `at=` param) of the returned\n * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is\n * the intended fallback surface for the developer to copy the URL).\n *\n * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.\n * `http://127.0.0.1:9100`. No trailing slash.\n * @param targetId - Chii target id (from `GET <relay>/targets`).\n * @param mintTotp - Function that returns a fresh 6-digit TOTP code string.\n * Called at most once. **Required** — when `undefined`, the function returns\n * `null` (fail-closed: no `at=` param means the relay WS gate rejects the\n * handshake, so a null result is safer than a URL that always 404s).\n * @param panel - Initial panel. Defaults to `\"console\"`.\n *\n * @returns The inspector URL string, or `null` when `mintTotp` is absent.\n *\n * @example\n * buildChiiInspectorUrl(\n * 'http://127.0.0.1:9100',\n * 'abc123',\n * () => generateTotp(secret),\n * )\n * // → 'http://127.0.0.1:9100/front_end/chii_app.html?ws=127.0.0.1%3A9100%2Fclient%2F<uuid>%3Ftarget%3Dabc123%26at%3D<code>'\n */\nexport function buildChiiInspectorUrl(\n relayHttpBaseUrl: string,\n targetId: string,\n mintTotp?: () => string,\n panel: 'elements' | 'console' | 'sources' | 'network' = 'console',\n): string | null {\n // FAIL-CLOSED (#509): relay sessions require TOTP for every WS upgrade.\n // Without a mintTotp function we cannot produce a valid at= code, so we\n // return null rather than a URL that will always be rejected by the relay gate\n // with WS 4401 / HTTP 404. Callers show a \"waiting\" hint when they get null.\n if (!mintTotp) {\n return null;\n }\n\n // Extract the host (and port) from the relay HTTP base URL, and pick the\n // query param name chii_app.html expects: `ws=` dials `ws://` (plain-HTTP\n // relay — env 3/4 local 127.0.0.1) while `wss=` dials `wss://` (HTTPS\n // tunnel — env 2). chii/public/index.js does the same scheme branch:\n // `location.protocol === 'https:' ? 'wss' : 'ws'`. Always sending `wss=`\n // would make the frontend attempt TLS against the plain-HTTP local relay.\n let relayHost: string;\n let wsParamName: 'ws' | 'wss';\n try {\n const parsed = new URL(relayHttpBaseUrl);\n relayHost = parsed.host; // e.g. \"127.0.0.1:9100\"\n wsParamName = parsed.protocol === 'https:' ? 'wss' : 'ws';\n } catch {\n // Fallback: strip the scheme prefix manually if URL parsing fails.\n relayHost = relayHttpBaseUrl.replace(/^https?:\\/\\//i, '');\n wsParamName = /^https:/i.test(relayHttpBaseUrl) ? 'wss' : 'ws';\n }\n\n // Generate a client UUID that matches the format Chii's index.js uses\n // (6 random alphanumeric characters).\n const clientId = `devtools-opener-${Date.now().toString(36)}`;\n\n // Build the ws=/wss= value: \"<relay-host>/client/<uuid>?target=<id>&at=<code>\"\n // This mirrors the format from chii/public/index.js:\n // `${domain}${basePath}client/${randomId(6)}?target=${targetId}`\n // SECRET-HANDLING: mintTotp() returns a code (not a secret). The code\n // rides only in the URL's at= param. Callers must not log the URL.\n const code = mintTotp();\n const wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}&at=${encodeURIComponent(code)}`;\n\n const params = new URLSearchParams({ [wsParamName]: wsPath, panel });\n return `${relayHttpBaseUrl.replace(/\\/$/, '')}/front_end/chii_app.html?${params.toString()}`;\n}\n\n// ---------------------------------------------------------------------------\n// Opt-out check\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when auto-open is **disabled**.\n *\n * Default (env var absent or any value other than `\"1\"`) is **disabled** —\n * the developer uses the \"디버그 툴 열기\" button on the /attach or dashboard\n * page instead. Set `AIT_AUTO_DEVTOOLS=1` to restore the old automatic\n * browser-open behaviour on device attach.\n *\n * `AIT_AUTO_DEVTOOLS=0` retains its explicit opt-out meaning for backward\n * compatibility (same effect as absent).\n */\nexport function isAutoDevtoolsDisabled(): boolean {\n return process.env.AIT_AUTO_DEVTOOLS !== '1';\n}\n\n// ---------------------------------------------------------------------------\n// Browser open (Node-only, sync)\n// ---------------------------------------------------------------------------\n\n/**\n * Opens the given URL in the OS default browser using a platform-appropriate\n * command. Returns `true` on success.\n *\n * Failures are silent from the caller's perspective — the caller should log\n * the URL to stderr as a fallback before calling this function.\n */\nexport function openUrlInBrowser(url: string): boolean {\n // Test hook: skip actual spawn when running in vitest / CI where the OS open\n // command may hang or be absent. Production code never sets this.\n if (process.env.AIT_AUTO_DEVTOOLS_TEST_SKIP_SPAWN === '1') return false;\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const { spawnSync } = require('node:child_process') as typeof import('node:child_process');\n const platform = process.platform;\n\n type Candidate = { cmd: string; args: string[] };\n let candidates: Candidate[];\n if (platform === 'darwin') {\n candidates = [{ cmd: 'open', args: [url] }];\n } else if (platform === 'win32') {\n candidates = [{ cmd: 'cmd', args: ['/c', 'start', '', url] }];\n } else {\n // Linux + fallback\n candidates = [\n { cmd: 'xdg-open', args: [url] },\n { cmd: 'sensible-browser', args: [url] },\n { cmd: 'x-www-browser', args: [url] },\n ];\n }\n\n for (const { cmd, args } of candidates) {\n try {\n const result = spawnSync(cmd, args, { encoding: 'utf8', timeout: 5_000 });\n if (!result.error && result.status === 0) return true;\n } catch {\n // Try next candidate.\n }\n }\n return false;\n}\n\n// ---------------------------------------------------------------------------\n// AutoDevtoolsOpener — stateful once-per-session open guard\n// ---------------------------------------------------------------------------\n\n/**\n * Options for {@link AutoDevtoolsOpener.open}.\n *\n * The `relayHttpBaseUrl` and `targetId` fields are required to build a working\n * Chii self-hosted inspector URL. When `relayHttpBaseUrl` is absent the open\n * is skipped (no relay available yet).\n */\nexport interface DevtoolsOpenOptions {\n /**\n * Stable local inspector URL (`http://127.0.0.1:<port>/inspector`) from the\n * QR HTTP server (issue #530). When provided this URL is opened in the browser\n * instead of building a direct `front_end/chii_app.html?wss=…` URL. The\n * `/inspector` endpoint mints a fresh TOTP at click time and redirects, so\n * there is no TOTP-expiry race. Safe to log (no tunnel host, no TOTP code).\n *\n * When absent, falls back to building a direct inspector URL from\n * `relayHttpBaseUrl` + `mintTotp` (legacy path, kept for backward compat).\n */\n inspectorStableUrl?: string | null;\n /**\n * Local HTTP base URL of the Chii relay, e.g. `http://127.0.0.1:9100`.\n * Used to build the `<relay-base>/front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is not available.\n *\n * For env 3/4 (intoss relay) this is `http://127.0.0.1:<port>`.\n * For env 2 (external PWA relay) this is the relay's external HTTP URL\n * (e.g. `https://<host>.trycloudflare.com`).\n *\n * When absent or empty, `open()` is a no-op.\n *\n * SECRET-HANDLING: this value contains the relay host. Callers MUST NOT\n * log it to stdout; stderr is the intended surface.\n */\n relayHttpBaseUrl: string | null | undefined;\n /**\n * Chii target id of the attached page, from `listTargets()[0].id`.\n * When absent or empty, `open()` is a no-op.\n */\n targetId: string | null | undefined;\n /**\n * Function that mints a fresh TOTP code when called. Called at most once per\n * `open()` invocation, immediately before building the inspector URL.\n * Only used when `inspectorStableUrl` is absent.\n *\n * Pass `undefined` when TOTP is disabled (no `at=` param is added).\n *\n * SECRET-HANDLING: the function MUST return only the code (6 digits), not\n * the secret. The code rides in the URL's `at=` param only.\n */\n mintTotp?: () => string;\n /** Current MCP environment (`mock` | `relay`). `open()` no-ops on `mock`. */\n env: McpEnvironment;\n}\n\n/**\n * Manages auto-opening Chrome DevTools on every NEW target attach (issue #530).\n *\n * Create one instance per `runDebugServer` call and pass its `open()` method\n * as the `onAttach` callback to the attach watcher (via `DualConnectionRouter`).\n *\n * The open fires for each NEW `targetId` — subsequent notifications for the\n * same target are de-duplicated. Re-attach with a fresh targetId (e.g. after\n * page reload on the phone) fires a new open. The URL opened is the stable\n * `/inspector` endpoint (issue #530) when `inspectorStableUrl` is provided —\n * it mints a fresh TOTP at click time so there is no expiry race. Falls back to\n * building a direct `front_end/chii_app.html?wss=…` URL when\n * `inspectorStableUrl` is absent.\n *\n * Opt-out and mock-environment guard are checked at call time.\n */\nexport class AutoDevtoolsOpener {\n /** Per-target de-dupe set (issue #530 — target-unit guard replaces once-per-daemon). */\n private readonly _openedTargets = new Set<string>();\n\n /**\n * Attempts to auto-open Chii DevTools in the developer's browser.\n *\n * Opens when:\n * - `options.targetId` is a NEW target (not yet in `_openedTargets`).\n *\n * No-op when any of the following conditions hold:\n * 1. `targetId` has already been opened (`_openedTargets` has it).\n * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.\n * 3. `options.env` is `mock` (env 1 — F12 is already available).\n * 4. `options.targetId` is null/undefined/empty (no page attached yet).\n * 5. Neither `inspectorStableUrl` nor `relayHttpBaseUrl` is available.\n *\n * When `inspectorStableUrl` is provided (issue #530 stable URL): opens\n * `http://127.0.0.1:<port>/inspector` directly and writes it to stderr.\n * The URL contains no tunnel host or TOTP code — safe to log anywhere.\n *\n * Legacy path (no `inspectorStableUrl`): builds a direct\n * `<relay-base>/front_end/chii_app.html?wss=…` URL from `relayHttpBaseUrl`\n * + `mintTotp`, writes to stderr. TOTP expiry caveat applies (~3 min window).\n *\n * SECRET-HANDLING: direct inspector URL (written to stderr) may contain relay\n * host and TOTP code. Stable URL is secret-free. Neither must go to stdout or\n * persistent logs.\n */\n open(options: DevtoolsOpenOptions): void {\n if (isAutoDevtoolsDisabled()) return;\n if (options.env === 'mock') return;\n if (!options.targetId) return;\n\n // Target-unit de-dupe (issue #530): re-attach with a new targetId fires again.\n const targetId = options.targetId;\n if (this._openedTargets.has(targetId)) return;\n\n // Use stable /inspector URL when available (issue #530) — secret-free, no expiry.\n if (options.inspectorStableUrl) {\n this._openedTargets.add(targetId);\n const stableUrl = options.inspectorStableUrl;\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] QR 페이지 또는 대시보드(${stableUrl.replace('/inspector', '')})의 \"디버그 툴 열기\" 버튼을 눌러 DevTools를 여세요.\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n',\n );\n const opened = openUrlInBrowser(stableUrl);\n if (!opened) {\n process.stderr.write(\n `[ait-debug] 브라우저 자동 열기 실패 — ${stableUrl} 을 브라우저에서 직접 여세요.\\n`,\n );\n }\n return;\n }\n\n // Legacy path: build direct inspector URL from relayHttpBaseUrl + mintTotp.\n if (!options.relayHttpBaseUrl) return;\n\n this._openedTargets.add(targetId);\n\n const inspectorUrl = buildChiiInspectorUrl(\n options.relayHttpBaseUrl,\n targetId,\n options.mintTotp,\n );\n\n // FAIL-CLOSED (#509): buildChiiInspectorUrl returns null when mintTotp is\n // absent (no valid at= code → relay WS gate would reject the connection).\n // Record targetId in set so this guard fires, but skip browser open.\n if (inspectorUrl === null) {\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다 — TOTP secret 미설정으로 인스펙터 URL을 생성할 수 없습니다.\\n' +\n '[ait-debug] relay 세션은 AIT_DEBUG_TOTP_SECRET 설정이 필요합니다.\\n',\n );\n return;\n }\n\n process.stderr.write(\n '[ait-debug] 기기가 연결됐습니다.\\n' +\n `[ait-debug] DevTools URL: ${inspectorUrl}\\n` +\n '[ait-debug] (AIT_AUTO_DEVTOOLS=1 로 설정하면 연결 시 자동으로 열립니다)\\n' +\n '[ait-debug] 주의: URL의 at= 코드는 ~3분 안에서만 유효합니다.\\n',\n );\n\n const opened = openUrlInBrowser(inspectorUrl);\n if (!opened) {\n process.stderr.write(\n '[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\\n',\n );\n }\n }\n\n /**\n * Returns `true` if `open()` has been called for at least one target.\n * (Replaces the old once-per-session `_opened` flag; kept for interface\n * compatibility with tests that read `opener.opened`.)\n */\n get opened(): boolean {\n return this._openedTargets.size > 0;\n }\n\n /** Returns the set of target IDs that have already been auto-opened. */\n get openedTargets(): ReadonlySet<string> {\n return this._openedTargets;\n }\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA4GA,SAAgB,sBACd,kBACA,UACA,UACA,QAAwD,WACzC;AAKf,KAAI,CAAC,SACH,QAAO;CAST,IAAI;CACJ,IAAI;AACJ,KAAI;EACF,MAAM,SAAS,IAAI,IAAI,iBAAiB;AACxC,cAAY,OAAO;AACnB,gBAAc,OAAO,aAAa,WAAW,QAAQ;SAC/C;AAEN,cAAY,iBAAiB,QAAQ,iBAAiB,GAAG;AACzD,gBAAc,WAAW,KAAK,iBAAiB,GAAG,QAAQ;;CAK5D,MAAM,WAAW,mBAAmB,KAAK,KAAK,CAAC,SAAS,GAAG;CAO3D,MAAM,OAAO,UAAU;CACvB,MAAM,SAAS,GAAG,UAAU,UAAU,SAAS,UAAU,mBAAmB,SAAS,CAAC,MAAM,mBAAmB,KAAK;CAEpH,MAAM,SAAS,IAAI,gBAAgB;GAAG,cAAc;EAAQ;EAAO,CAAC;AACpE,QAAO,GAAG,iBAAiB,QAAQ,OAAO,GAAG,CAAC,2BAA2B,OAAO,UAAU"}
|
package/dist/in-app/auto.js
CHANGED
|
@@ -129,6 +129,41 @@ function isPrivateAppsHost(hostname) {
|
|
|
129
129
|
return hostname.endsWith(PRIVATE_APPS_HOST_SUFFIX);
|
|
130
130
|
}
|
|
131
131
|
/**
|
|
132
|
+
* The parent host suffix for the whole Toss mini-app serving family.
|
|
133
|
+
*
|
|
134
|
+
* The 3.0 runtime loader serves mini-app pages from tossmini.com hosts that
|
|
135
|
+
* are NOT `*.private-apps.tossmini.com` (observed live 2026-07-08 on mini-app
|
|
136
|
+
* 31146 with a 3.0-beta bundle: a 4-label host ending in `.tossmini.com`
|
|
137
|
+
* whose middle label is not `private-apps`, with `_deploymentId` consumed by
|
|
138
|
+
* the native loader and not propagated to the page URL — devtools#760).
|
|
139
|
+
*
|
|
140
|
+
* Under 3.0 the hostname therefore no longer distinguishes a dogfood
|
|
141
|
+
* candidate from a production entry, so for these hosts Layer B is demoted
|
|
142
|
+
* from a stage discriminator (#665) to a "Toss-owned host family" filter,
|
|
143
|
+
* and the effective boundary moves to Layer C: explicit `debug=1`, a valid
|
|
144
|
+
* `wss:` relay, and a MANDATORY `at=` TOTP code (see Layer C3 in
|
|
145
|
+
* {@link evaluateDebugGate}). A production user's entry URL carries none of
|
|
146
|
+
* those params, so an accidentally-shipped debug build stays dormant exactly
|
|
147
|
+
* as #665 intended; what changes is that a deliberate operator holding the
|
|
148
|
+
* TOTP secret can now attach on a 3.0-family host.
|
|
149
|
+
*
|
|
150
|
+
* The match is the same exact-suffix `endsWith` check as
|
|
151
|
+
* {@link isPrivateAppsHost} — never a substring `.includes()`, which would
|
|
152
|
+
* accept an attacker-controlled `x.tossmini.com.evil.example`. The leading
|
|
153
|
+
* `.` forces at least one subdomain label, so a bare `tossmini.com` does not
|
|
154
|
+
* match.
|
|
155
|
+
*/
|
|
156
|
+
const TOSSMINI_HOST_SUFFIX = ".tossmini.com";
|
|
157
|
+
/**
|
|
158
|
+
* Returns whether `hostname` is any `*.tossmini.com` subdomain — the host
|
|
159
|
+
* family the Toss app serves mini-app pages from. Includes the 2.x
|
|
160
|
+
* `*.private-apps.tossmini.com` dogfood hosts and the 3.0 unified serving
|
|
161
|
+
* hosts (devtools#760).
|
|
162
|
+
*/
|
|
163
|
+
function isTossminiHost(hostname) {
|
|
164
|
+
return hostname.endsWith(TOSSMINI_HOST_SUFFIX);
|
|
165
|
+
}
|
|
166
|
+
/**
|
|
132
167
|
* The host suffix Cloudflare quick-tunnels use — the env 2 (PWA) entry.
|
|
133
168
|
*
|
|
134
169
|
* Env 2 serves the local Vite dev server through a `*.trycloudflare.com` quick
|
|
@@ -173,18 +208,27 @@ function isLocalhostHost(hostname) {
|
|
|
173
208
|
* known debug-allowed host. The debug surface is ONLY active on:
|
|
174
209
|
* - localhost / loopback (env 1 desktop dev)
|
|
175
210
|
* - *.trycloudflare.com (env 2 PWA tunnel)
|
|
176
|
-
* - *.
|
|
177
|
-
*
|
|
178
|
-
*
|
|
179
|
-
* is silently blocked. This is a positive allowlist —
|
|
180
|
-
* had debug surface regardless, but this function makes
|
|
181
|
-
* auditable in a single place.
|
|
211
|
+
* - *.tossmini.com (env 3 dog-food — 2.x private-apps hosts AND the 3.0
|
|
212
|
+
* unified serving family, devtools#760)
|
|
213
|
+
*
|
|
214
|
+
* Any other host is silently blocked. This is a positive allowlist —
|
|
215
|
+
* unlisted hosts never had debug surface regardless, but this function makes
|
|
216
|
+
* it explicit and auditable in a single place.
|
|
217
|
+
*
|
|
218
|
+
* #760 note on the #665 boundary: the former env 4 LIVE host family
|
|
219
|
+
* (`*.apps.tossmini.com`) now passes this coarse filter because the 3.0
|
|
220
|
+
* loader serves dogfood candidates and production entries from the same
|
|
221
|
+
* host family — the hostname alone can no longer separate them. The #665
|
|
222
|
+
* invariant ("no naked attach on a production-family host") is preserved
|
|
223
|
+
* one layer down: on tossmini hosts that are not `*.private-apps.*`, Layer
|
|
224
|
+
* C3 makes the TOTP `at=` code MANDATORY, and production entry URLs carry
|
|
225
|
+
* no debug/relay/at params at all.
|
|
182
226
|
*
|
|
183
227
|
* SECRET-HANDLING: the hostname value MUST NOT be logged or included in any
|
|
184
228
|
* error reason string — only benign labels ('host not in allowlist') are safe.
|
|
185
229
|
*/
|
|
186
230
|
function isDebugAllowedHost(hostname) {
|
|
187
|
-
return isLocalhostHost(hostname) || isTrycloudflareHost(hostname) ||
|
|
231
|
+
return isLocalhostHost(hostname) || isTrycloudflareHost(hostname) || isTossminiHost(hostname);
|
|
188
232
|
}
|
|
189
233
|
/**
|
|
190
234
|
* Pure function that evaluates the runtime debug activation layers (B and C).
|
|
@@ -215,13 +259,13 @@ function evaluateDebugGate(input) {
|
|
|
215
259
|
reason: "host"
|
|
216
260
|
};
|
|
217
261
|
let deploymentId = "";
|
|
218
|
-
if (
|
|
262
|
+
if (isPrivateAppsHost(input.hostname)) {
|
|
219
263
|
deploymentId = input.searchParams.get("_deploymentId") ?? "";
|
|
220
264
|
if (deploymentId === "") return {
|
|
221
265
|
attach: false,
|
|
222
266
|
reason: "entry"
|
|
223
267
|
};
|
|
224
|
-
}
|
|
268
|
+
} else if (!isTunnel && !isLocal) deploymentId = input.searchParams.get("_deploymentId") ?? "";
|
|
225
269
|
if (input.searchParams.get("debug") !== "1") return {
|
|
226
270
|
attach: false,
|
|
227
271
|
reason: "opt-in"
|
|
@@ -244,13 +288,16 @@ function evaluateDebugGate(input) {
|
|
|
244
288
|
attach: false,
|
|
245
289
|
reason: "invalid-relay"
|
|
246
290
|
};
|
|
291
|
+
const atCode = input.searchParams.get("at") ?? "";
|
|
247
292
|
if (input.verifyTotpCode !== void 0) {
|
|
248
|
-
|
|
249
|
-
if (!input.verifyTotpCode(code)) return {
|
|
293
|
+
if (!input.verifyTotpCode(atCode)) return {
|
|
250
294
|
attach: false,
|
|
251
295
|
reason: "auth"
|
|
252
296
|
};
|
|
253
|
-
}
|
|
297
|
+
} else if (isTossminiHost(input.hostname) && !isPrivateAppsHost(input.hostname) && atCode === "") return {
|
|
298
|
+
attach: false,
|
|
299
|
+
reason: "auth"
|
|
300
|
+
};
|
|
254
301
|
return {
|
|
255
302
|
attach: true,
|
|
256
303
|
relayUrl: relayUrl.href,
|