@ait-co/devtools 0.1.69 → 0.1.71

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/dist/chii-relay-BcnVJBqm.cjs +289 -0
  2. package/dist/chii-relay-BcnVJBqm.cjs.map +1 -0
  3. package/dist/chii-relay-DSVG4Ui1.js +289 -0
  4. package/dist/chii-relay-DSVG4Ui1.js.map +1 -0
  5. package/dist/devtools-opener-BbUXBzgA.js.map +1 -1
  6. package/dist/devtools-opener-Bp671YXu.cjs.map +1 -1
  7. package/dist/devtools-opener-D84kZFtR.js.map +1 -1
  8. package/dist/devtools-opener-h6A-UjzC.cjs.map +1 -1
  9. package/dist/in-app/index.d.ts.map +1 -1
  10. package/dist/in-app/index.js +179 -0
  11. package/dist/in-app/index.js.map +1 -1
  12. package/dist/mcp/cli.js +263 -67
  13. package/dist/mcp/cli.js.map +1 -1
  14. package/dist/mcp/server.js +1 -1
  15. package/dist/mock/index.d.ts +24 -1
  16. package/dist/mock/index.d.ts.map +1 -1
  17. package/dist/mock/index.js +89 -1
  18. package/dist/mock/index.js.map +1 -1
  19. package/dist/panel/index.js +4 -2
  20. package/dist/panel/index.js.map +1 -1
  21. package/dist/{qr-http-server-DR__VNnX.cjs → qr-http-server-BIIMOcuU.cjs} +3 -1
  22. package/dist/qr-http-server-BIIMOcuU.cjs.map +1 -0
  23. package/dist/{qr-http-server-CyVQphTM.js → qr-http-server-CeEzLS3g.js} +3 -1
  24. package/dist/qr-http-server-CeEzLS3g.js.map +1 -0
  25. package/dist/{qr-http-server-DnQSQ3hC.cjs → qr-http-server-ClakYBO9.cjs} +3 -1
  26. package/dist/qr-http-server-ClakYBO9.cjs.map +1 -0
  27. package/dist/{qr-http-server-DKEca8J3.js → qr-http-server-JjGU81q7.js} +3 -1
  28. package/dist/qr-http-server-JjGU81q7.js.map +1 -0
  29. package/dist/{tunnel-BMY7KgO5.cjs → tunnel-DwVrcZ56.cjs} +2 -2
  30. package/dist/{tunnel-BMY7KgO5.cjs.map → tunnel-DwVrcZ56.cjs.map} +1 -1
  31. package/dist/{tunnel-DIN5Vvbo.js → tunnel-aIy_7nWm.js} +2 -2
  32. package/dist/{tunnel-DIN5Vvbo.js.map → tunnel-aIy_7nWm.js.map} +1 -1
  33. package/dist/unplugin/index.cjs +2 -2
  34. package/dist/unplugin/index.js +2 -2
  35. package/dist/unplugin/tunnel.cjs +1 -1
  36. package/dist/unplugin/tunnel.js +1 -1
  37. package/package.json +1 -1
  38. package/dist/chii-relay-BNd3G3UG.js +0 -152
  39. package/dist/chii-relay-BNd3G3UG.js.map +0 -1
  40. package/dist/chii-relay-DngjQ2_A.cjs +0 -151
  41. package/dist/chii-relay-DngjQ2_A.cjs.map +0 -1
  42. package/dist/qr-http-server-CyVQphTM.js.map +0 -1
  43. package/dist/qr-http-server-DKEca8J3.js.map +0 -1
  44. package/dist/qr-http-server-DR__VNnX.cjs.map +0 -1
  45. package/dist/qr-http-server-DnQSQ3hC.cjs.map +0 -1
package/dist/mcp/cli.js CHANGED
@@ -9,7 +9,7 @@ import { Server } from "@modelcontextprotocol/sdk/server/index.js";
9
9
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
10
10
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
11
11
  import { EventEmitter } from "node:events";
12
- import { WebSocket } from "ws";
12
+ import { WebSocket, WebSocketServer } from "ws";
13
13
  import { randomBytes } from "node:crypto";
14
14
  import { createServer } from "node:http";
15
15
  import { spawn } from "node:child_process";
@@ -120,6 +120,47 @@ var ChiiAitSource = class {
120
120
  }
121
121
  };
122
122
  //#endregion
123
+ //#region src/shared/relay-auth-close.ts
124
+ /**
125
+ * Shared constants for the relay's named TOTP-auth rejection (issue #478).
126
+ *
127
+ * Before #478 the relay rejected an unauthenticated WebSocket upgrade with a
128
+ * raw `HTTP/1.1 401` + `socket.destroy()`. A handshake aborted that way is
129
+ * indistinguishable from a network failure on the browser side — the
130
+ * WebSocket only ever sees close code 1006, so the phone (env-2 launcher PWA)
131
+ * could not tell "stale TOTP code" apart from "tunnel down" and stayed
132
+ * silent. The fix is accept-then-close: complete the handshake, then close
133
+ * with an application close code that NAMES the rejection.
134
+ *
135
+ * Three parties share this contract:
136
+ * - `src/mcp/chii-relay.ts` (Node) sends the close frame / HTTP error body;
137
+ * - `src/in-app/attach.ts` (browser) observes relay-bound WebSockets and
138
+ * surfaces the code to the launcher shell;
139
+ * - `src/mcp/chii-connection.ts` (Node daemon client) recognises the code
140
+ * as an auth failure on its own `/client` dial (defensive — #439's fresh
141
+ * code mint means it should not normally hit this).
142
+ *
143
+ * This module is intentionally dependency-free (no Node, no DOM) so it is
144
+ * safe to import from both the browser in-app bundle and the MCP daemon
145
+ * bundle.
146
+ *
147
+ * SECRET-HANDLING: these are fixed enum values. The close reason / error body
148
+ * must never grow to carry a secret, a TOTP code, or a host.
149
+ */
150
+ /**
151
+ * WebSocket close code sent by the relay when TOTP auth is rejected.
152
+ *
153
+ * 4000–4999 is the application-reserved range (RFC 6455 §7.4.2); 4401 mirrors
154
+ * HTTP 401 so it reads as "unauthorized" at a glance.
155
+ */
156
+ const RELAY_AUTH_REJECT_CLOSE_CODE = 4401;
157
+ /**
158
+ * Close reason string accompanying {@link RELAY_AUTH_REJECT_CLOSE_CODE}, and
159
+ * the `error` value of the relay's HTTP 401 JSON body. Enum string only —
160
+ * never interpolated with request data.
161
+ */
162
+ const RELAY_AUTH_REJECT_REASON = "totp-rejected";
163
+ //#endregion
123
164
  //#region src/mcp/log.ts
124
165
  /**
125
166
  * Allowed field keys that may pass through to a log line.
@@ -467,12 +508,15 @@ var ChiiCdpConnection = class {
467
508
  await new Promise((resolve, reject) => {
468
509
  ws.once("open", () => resolve());
469
510
  ws.once("error", (err) => reject(err));
511
+ ws.once("close", (code) => {
512
+ if (code === 4401) reject(/* @__PURE__ */ new Error("relay 인증(TOTP)이 거부됐습니다 (close 4401). 코드가 만료됐을 수 있습니다 — 재연결 시 새 코드가 발급됩니다."));
513
+ });
470
514
  });
471
515
  this.lastCrashDetectedAt = null;
472
516
  this.targetLastSeenAt.clear();
473
517
  this.connectionState = "connected";
474
518
  ws.on("message", (data) => this.handleMessage(data.toString()));
475
- ws.on("close", () => this.handleDisconnect("relay WebSocket 연결이 끊겼습니다"));
519
+ ws.on("close", (code) => this.handleDisconnect(code === 4401 ? "relay 인증(TOTP)이 거부돼 연결이 종료됐습니다 (close 4401)" : "relay WebSocket 연결이 끊겼습니다"));
476
520
  ws.on("error", (err) => this.handleDisconnect(`relay WebSocket 오류: ${err.message}`));
477
521
  this.sendFireAndForget("Runtime.enable");
478
522
  this.sendFireAndForget("Network.enable");
@@ -721,12 +765,27 @@ var ChiiCdpConnection = class {
721
765
  * entries.
722
766
  *
723
767
  * TOTP auth (relay-side, authoritative gate):
724
- * When `verifyAuth` is provided, this module registers HTTP 'upgrade' and
725
- * 'request' listeners on the server BEFORE calling `chii.start({server})`.
726
- * Node's `http.Server` calls listeners in registration order; the first to
727
- * call `socket.destroy()` (upgrade) or `res.end()` (request) wins. Invalid
728
- * auth → 401 + destroy (chii never sees the connection). Valid auth →
729
- * return without side-effect (chii handles it).
768
+ * When `verifyAuth` is provided, this module gates both inbound surfaces:
769
+ *
770
+ * - HTTP 'request': a listener registered BEFORE `chii.start({server})`.
771
+ * Node's `http.Server` calls listeners in registration order; the first
772
+ * to call `res.end()` wins. Invalid auth → 401 + CORS header + a tiny
773
+ * JSON body (`{"error":"totp-rejected"}`) so a cross-origin script
774
+ * `fetch()` probe can READ the status (issue #478). Valid auth → return
775
+ * without side-effect (chii's Koa handler serves it).
776
+ *
777
+ * - WS 'upgrade': after `chii.start()` has registered chii's own upgrade
778
+ * listener, we take over the upgrade chain (remove chii's listeners,
779
+ * re-dispatch manually). Invalid auth → accept-then-close: complete the
780
+ * handshake via a `noServer` WebSocketServer, then immediately close
781
+ * with code 4401 reason 'totp-rejected' (issue #478). A raw 401 +
782
+ * `socket.destroy()` only ever surfaced as close code 1006 in the
783
+ * browser — indistinguishable from a tunnel failure, which left the
784
+ * env-2 phone UI silent. The explicit dispatch (not listener ordering)
785
+ * is what keeps chii away from rejected sockets: accept-then-close
786
+ * leaves the socket alive, so an order-based early-return would let
787
+ * chii's later listener complete a SECOND handshake on the same socket
788
+ * — an auth bypass. Valid auth → forward to chii's captured listeners.
730
789
  *
731
790
  * TOTP code transports (issue #466) — two equivalent ways to carry the code:
732
791
  * 1. Query param `at=<code>` — used by the daemon-side `/client` connection
@@ -753,6 +812,66 @@ var ChiiCdpConnection = class {
753
812
  * predicate from the caller's perspective; this module only forwards pass/fail.
754
813
  */
755
814
  const require$1 = createRequire(import.meta.url);
815
+ /**
816
+ * WS keepalive ping interval (ms).
817
+ *
818
+ * Cloudflare proxied connections are dropped after ~100 s of no traffic.
819
+ * 45 s comfortably fits inside that window and lets both the phone-target leg
820
+ * and the daemon-client leg survive idle CDP sessions.
821
+ */
822
+ const DEFAULT_KEEPALIVE_INTERVAL_MS = 45e3;
823
+ /**
824
+ * Loads chii's internal WebSocketServer class and returns it together with a
825
+ * flag indicating whether the real class was found.
826
+ *
827
+ * Returns `null` if the internal path is not resolvable (future chii release
828
+ * changes the layout) — callers skip keepalive gracefully.
829
+ */
830
+ function tryLoadChiiWssClass() {
831
+ try {
832
+ const mod = require$1("chii/server/lib/WebSocketServer");
833
+ if (typeof mod === "function") return mod;
834
+ } catch {}
835
+ return null;
836
+ }
837
+ /**
838
+ * Calls `chii.start()` and returns the chii `WebSocketServer` instance that
839
+ * was constructed during the call.
840
+ *
841
+ * How: `chii/server/index.js`'s `start()` creates `new WebSocketServer()`
842
+ * where `WebSocketServer` is captured from `require('./lib/WebSocketServer')`
843
+ * at module load time. The class reference is stable, so we can temporarily
844
+ * patch `ChiiWssClass.prototype.start` — which runs *on the instance* —
845
+ * to record `this` before the original `start` runs.
846
+ *
847
+ * The patch is installed before `chii.start()` and removed (via `finally`)
848
+ * immediately after, so concurrent `startChiiRelay` calls nest correctly: each
849
+ * call's patch overrides the previous in the prototype chain for the duration
850
+ * of its own `chii.start()` call, restoring the prior descriptor on exit.
851
+ *
852
+ * If `ChiiWssClass` is null (internal path changed in a future chii release),
853
+ * `chii.start()` runs unpatched and the function returns null — callers skip
854
+ * keepalive gracefully without affecting relay correctness.
855
+ */
856
+ async function startChiiWithCapture(chii, startOptions, ChiiWssClass) {
857
+ if (ChiiWssClass === null) {
858
+ await chii.start(startOptions);
859
+ return null;
860
+ }
861
+ let captured = null;
862
+ const proto = ChiiWssClass.prototype;
863
+ const originalStart = proto.start;
864
+ proto.start = function(server) {
865
+ captured = this;
866
+ return originalStart.call(this, server);
867
+ };
868
+ try {
869
+ await chii.start(startOptions);
870
+ } finally {
871
+ proto.start = originalStart;
872
+ }
873
+ return captured;
874
+ }
756
875
  function loadChiiServer() {
757
876
  const mod = require$1("chii");
758
877
  if (typeof mod === "object" && mod !== null && "start" in mod && typeof mod.start === "function") return mod;
@@ -805,6 +924,7 @@ async function startChiiRelay(options = {}) {
805
924
  const requestedPort = options.port ?? 0;
806
925
  const host = options.host ?? "127.0.0.1";
807
926
  const { verifyAuth, onAuthReject } = options;
927
+ const keepaliveIntervalMs = options.keepaliveIntervalMs !== void 0 ? options.keepaliveIntervalMs : DEFAULT_KEEPALIVE_INTERVAL_MS;
808
928
  const httpServer = createServer();
809
929
  const notifyAuthReject = (kind) => {
810
930
  if (onAuthReject === void 0) return;
@@ -812,33 +932,41 @@ async function startChiiRelay(options = {}) {
812
932
  onAuthReject({ kind });
813
933
  } catch {}
814
934
  };
935
+ if (verifyAuth) httpServer.on("request", (req, res) => {
936
+ const rewritten = rewriteAtPathPrefix(req.url ?? "");
937
+ if (rewritten === null) return;
938
+ req.url = rewritten;
939
+ if (!verifyAuth(req)) {
940
+ res.statusCode = 401;
941
+ res.setHeader("Access-Control-Allow-Origin", "*");
942
+ res.setHeader("Content-Type", "application/json");
943
+ res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
944
+ notifyAuthReject("http-request");
945
+ }
946
+ });
947
+ const chiiWssClass = keepaliveIntervalMs > 0 ? tryLoadChiiWssClass() : null;
948
+ const capturedChiiWss = await startChiiWithCapture(loadChiiServer(), {
949
+ server: httpServer,
950
+ domain: `${host}:${requestedPort}`,
951
+ port: requestedPort
952
+ }, chiiWssClass);
815
953
  if (verifyAuth) {
816
- httpServer.on("upgrade", (req, socket) => {
954
+ const chiiUpgradeListeners = httpServer.listeners("upgrade");
955
+ httpServer.removeAllListeners("upgrade");
956
+ const rejectWss = new WebSocketServer({ noServer: true });
957
+ httpServer.on("upgrade", (req, socket, head) => {
817
958
  const rewritten = rewriteAtPathPrefix(req.url ?? "");
818
959
  if (rewritten !== null) req.url = rewritten;
819
960
  if (!verifyAuth(req)) {
820
- socket.write("HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n");
821
- socket.destroy();
961
+ rejectWss.handleUpgrade(req, socket, head, (ws) => {
962
+ ws.close(RELAY_AUTH_REJECT_CLOSE_CODE, RELAY_AUTH_REJECT_REASON);
963
+ });
822
964
  notifyAuthReject("ws-upgrade");
823
965
  return;
824
966
  }
825
- });
826
- httpServer.on("request", (req, res) => {
827
- const rewritten = rewriteAtPathPrefix(req.url ?? "");
828
- if (rewritten === null) return;
829
- req.url = rewritten;
830
- if (!verifyAuth(req)) {
831
- res.statusCode = 401;
832
- res.end();
833
- notifyAuthReject("http-request");
834
- }
967
+ for (const listener of chiiUpgradeListeners) listener(req, socket, head);
835
968
  });
836
969
  }
837
- await loadChiiServer().start({
838
- server: httpServer,
839
- domain: `${host}:${requestedPort}`,
840
- port: requestedPort
841
- });
842
970
  const actualPort = await new Promise((resolve, reject) => {
843
971
  httpServer.once("error", reject);
844
972
  httpServer.listen(requestedPort, host, () => {
@@ -846,10 +974,21 @@ async function startChiiRelay(options = {}) {
846
974
  resolve(httpServer.address().port);
847
975
  });
848
976
  });
977
+ let keepaliveHandle = null;
978
+ if (keepaliveIntervalMs > 0 && capturedChiiWss !== null) {
979
+ const chiiWss = capturedChiiWss;
980
+ keepaliveHandle = setInterval(() => {
981
+ for (const client of chiiWss._wss.clients) if (client.readyState === 1) client.ping();
982
+ }, keepaliveIntervalMs);
983
+ }
849
984
  return {
850
985
  port: actualPort,
851
986
  baseUrl: `http://${host}:${actualPort}`,
852
987
  close: () => new Promise((resolve) => {
988
+ if (keepaliveHandle !== null) {
989
+ clearInterval(keepaliveHandle);
990
+ keepaliveHandle = null;
991
+ }
853
992
  httpServer.close(() => resolve());
854
993
  })
855
994
  };
@@ -1008,35 +1147,65 @@ function buildDeepLinkAttachUrl(schemeUrl, wssUrl, totpCode) {
1008
1147
  //#endregion
1009
1148
  //#region src/mcp/devtools-opener.ts
1010
1149
  /**
1011
- * Base URL for the Chrome DevTools inspector hosted on appspot.
1012
- *
1013
- * The `@` path segment is the "latest / bleeding edge" alias which tracks the
1014
- * current Chrome stable CDP protocol version — compatible with the chobitsu-
1015
- * based CDP that Chii injects. A specific commit hash may be pinned here if
1016
- * a regression is observed.
1017
- */
1018
- const DEVTOOLS_FRONTEND_BASE = "https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html";
1019
- /**
1020
- * Assembles the Chrome DevTools inspector URL that connects to a Chii relay
1021
- * WebSocket.
1022
- *
1023
- * The `wss=` parameter expects a host-and-path string without the `wss://`
1024
- * scheme prefix the DevTools frontend prepends it automatically.
1025
- *
1026
- * @param wssRelayUrl - Full `wss://` URL of the Chii relay (public tunnel).
1027
- * Example: `wss://abc.trycloudflare.com`
1150
+ * Assembles the Chii self-hosted DevTools inspector URL for a given relay
1151
+ * and target.
1152
+ *
1153
+ * Chii serves its own DevTools frontend at
1154
+ * `<relayHttpBaseUrl>/front_end/chii_app.html`. The `ws=` (plain HTTP relay)
1155
+ * or `wss=` (HTTPS relay) query parameter is a URL-encoded string of the form
1156
+ * `<relay-host>/client/<uuid>?target=<id>` (and optionally `&at=<totp>`) —
1157
+ * the same format used by Chii's own target list page (derived from
1158
+ * `chii/public/index.js`).
1159
+ *
1160
+ * The `at=` TOTP code is minted at call time via `mintTotp()`. It is valid
1161
+ * for the current 30-second RFC 6238 step (±1 step skew = 90 s acceptance
1162
+ * window). The developer must open the returned URL within that window. If
1163
+ * the window expires before the browser connects, the relay will reject the
1164
+ * WebSocket upgrade with close code 4401.
1165
+ *
1166
+ * SECRET-HANDLING: `mintTotp` returns a code, not a secret. The code is
1167
+ * embedded in the `wss=` parameter (inside the `at=` param) of the returned
1168
+ * URL. Callers MUST NOT log the returned URL to stdout (stderr is OK — it is
1169
+ * the intended fallback surface for the developer to copy the URL).
1170
+ *
1171
+ * @param relayHttpBaseUrl - Local HTTP base URL of the Chii relay, e.g.
1172
+ * `http://127.0.0.1:9100`. No trailing slash.
1173
+ * @param targetId - Chii target id (from `GET <relay>/targets`).
1174
+ * @param mintTotp - Optional function that returns a fresh 6-digit TOTP code
1175
+ * string. Called at most once. When omitted (TOTP disabled) no `at=` param
1176
+ * is added.
1028
1177
  * @param panel - Initial panel. Defaults to `"console"`.
1029
1178
  *
1030
1179
  * @example
1031
- * buildChromeDevtoolsUrl('wss://abc.trycloudflare.com')
1032
- * // → 'https://chrome-devtools-frontend.appspot.com/serve_file/@/inspector.html?wss=abc.trycloudflare.com&panel=console'
1033
- */
1034
- function buildChromeDevtoolsUrl(wssRelayUrl, panel = "console") {
1035
- const wssParam = wssRelayUrl.replace(/^wss:\/\//i, "");
1036
- return `${DEVTOOLS_FRONTEND_BASE}?${new URLSearchParams({
1037
- wss: wssParam,
1180
+ * buildChiiInspectorUrl(
1181
+ * 'http://127.0.0.1:9100',
1182
+ * 'abc123',
1183
+ * () => generateTotp(secret),
1184
+ * )
1185
+ * // → '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>'
1186
+ */
1187
+ function buildChiiInspectorUrl(relayHttpBaseUrl, targetId, mintTotp, panel = "console") {
1188
+ let relayHost;
1189
+ let wsParamName;
1190
+ try {
1191
+ const parsed = new URL(relayHttpBaseUrl);
1192
+ relayHost = parsed.host;
1193
+ wsParamName = parsed.protocol === "https:" ? "wss" : "ws";
1194
+ } catch {
1195
+ relayHost = relayHttpBaseUrl.replace(/^https?:\/\//i, "");
1196
+ wsParamName = /^https:/i.test(relayHttpBaseUrl) ? "wss" : "ws";
1197
+ }
1198
+ const clientId = `devtools-opener-${Date.now().toString(36)}`;
1199
+ let wsPath = `${relayHost}/client/${clientId}?target=${encodeURIComponent(targetId)}`;
1200
+ if (mintTotp) {
1201
+ const code = mintTotp();
1202
+ wsPath += `&at=${encodeURIComponent(code)}`;
1203
+ }
1204
+ const params = new URLSearchParams({
1205
+ [wsParamName]: wsPath,
1038
1206
  panel
1039
- }).toString()}`;
1207
+ });
1208
+ return `${relayHttpBaseUrl.replace(/\/$/, "")}/front_end/chii_app.html?${params.toString()}`;
1040
1209
  }
1041
1210
  /**
1042
1211
  * Returns `true` when auto-open is **disabled** via the `AIT_AUTO_DEVTOOLS`
@@ -1106,31 +1275,45 @@ function openUrlInBrowser(url) {
1106
1275
  var AutoDevtoolsOpener = class {
1107
1276
  _opened = false;
1108
1277
  /**
1109
- * Attempts to auto-open Chrome DevTools.
1278
+ * Attempts to auto-open Chii DevTools in the developer's browser.
1279
+ *
1280
+ * Builds a `<relay-base>/front_end/chii_app.html?wss=…` URL pointing at the
1281
+ * attached target. A fresh TOTP `at=` code is minted at call time so the
1282
+ * relay's WebSocket upgrade gate accepts the connection.
1110
1283
  *
1111
1284
  * No-op when any of the following conditions hold:
1112
1285
  * 1. Already opened this session (`_opened` is true).
1113
1286
  * 2. `AIT_AUTO_DEVTOOLS=0` opt-out is set.
1114
- * 3. Environment is `mock` (env 1 — F12 is already available).
1115
- * 4. `wssRelayUrl` is null/undefined/empty (tunnel not yet up).
1287
+ * 3. `options.env` is `mock` (env 1 — F12 is already available).
1288
+ * 4. `options.relayHttpBaseUrl` is null/undefined/empty (relay not up yet).
1289
+ * 5. `options.targetId` is null/undefined/empty (no page attached yet).
1116
1290
  *
1117
1291
  * Always writes the DevTools URL to stderr so the developer can copy it
1118
1292
  * if the browser open fails or the popup is blocked.
1119
1293
  *
1120
- * @param wssRelayUrl - The public `wss://` relay URL (from tunnel status).
1121
- * @param env - Current MCP environment (`mock` | `relay`).
1294
+ * TOTP expiry caveat: the `at=` code embedded in the URL is valid for the
1295
+ * current 30-second RFC 6238 step (±1 skew = 90 s). The developer must open
1296
+ * the URL within that window; if they miss it, reload the page or re-run
1297
+ * `open()` (though the once-per-session guard prevents that — restart the
1298
+ * MCP server if needed).
1299
+ *
1300
+ * SECRET-HANDLING: the inspector URL (written to stderr) contains the relay
1301
+ * host and a short-lived TOTP code. Do NOT write it to stdout or any
1302
+ * persistent log.
1122
1303
  */
1123
- open(wssRelayUrl, env) {
1304
+ open(options) {
1124
1305
  if (this._opened) return;
1125
1306
  if (isAutoDevtoolsDisabled()) return;
1126
- if (env === "mock") return;
1127
- if (!wssRelayUrl) return;
1307
+ if (options.env === "mock") return;
1308
+ if (!options.relayHttpBaseUrl) return;
1309
+ if (!options.targetId) return;
1128
1310
  this._opened = true;
1129
- const devtoolsUrl = buildChromeDevtoolsUrl(wssRelayUrl);
1130
- process.stderr.write(`[ait-debug] 기기가 연결됐습니다 — Chrome DevTools를 자동으로 엽니다.
1131
- [ait-debug] Chrome DevTools URL: ${devtoolsUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)
1311
+ const inspectorUrl = buildChiiInspectorUrl(options.relayHttpBaseUrl, options.targetId, options.mintTotp);
1312
+ process.stderr.write(`[ait-debug] 기기가 연결됐습니다 — Chii DevTools를 자동으로 엽니다.
1313
+ [ait-debug] DevTools URL: ${inspectorUrl}\n[ait-debug] (AIT_AUTO_DEVTOOLS=0 으로 자동 열기를 끌 수 있습니다)
1314
+ [ait-debug] 주의: URL의 at= 코드는 30초 창 안에서만 유효합니다.
1132
1315
  `);
1133
- if (!openUrlInBrowser(devtoolsUrl)) process.stderr.write("[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\n");
1316
+ if (!openUrlInBrowser(inspectorUrl)) process.stderr.write("[ait-debug] 브라우저 자동 열기 실패 — 위 URL을 브라우저에서 직접 여세요.\n");
1134
1317
  }
1135
1318
  /** Returns `true` if `open()` has passed all guards and fired once. */
1136
1319
  get opened() {
@@ -1934,6 +2117,7 @@ const en = {
1934
2117
  "launcher.invalidUrl": "Enter a valid http(s):// URL.",
1935
2118
  "launcher.debugAuthFailed": "Debug connection authentication failed",
1936
2119
  "launcher.debugAuthFailedHint": "The QR code may have expired. Scan a fresh QR code.",
2120
+ "launcher.debugAuthExpiredHint": "The debug session has expired. Scan a fresh QR from the attach page on your Mac.",
1937
2121
  "launcher.debugAuthRescanCta": "Scan a new QR",
1938
2122
  "launcher.diagFab": "Diag",
1939
2123
  "launcher.diagTitle": "Viewport diagnostics",
@@ -2177,6 +2361,7 @@ const tables = {
2177
2361
  "launcher.invalidUrl": "올바른 http(s):// URL을 입력하세요.",
2178
2362
  "launcher.debugAuthFailed": "디버그 연결 인증 실패",
2179
2363
  "launcher.debugAuthFailedHint": "QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.",
2364
+ "launcher.debugAuthExpiredHint": "디버그 세션이 만료됐어요. Mac의 attach 페이지에서 새 QR을 스캔하세요.",
2180
2365
  "launcher.debugAuthRescanCta": "새 QR 스캔하기",
2181
2366
  "launcher.diagFab": "진단",
2182
2367
  "launcher.diagTitle": "뷰포트 진단",
@@ -4381,7 +4566,7 @@ async function readMcpSdkVersion() {
4381
4566
  * some test environments that skip the build step).
4382
4567
  */
4383
4568
  function readDevtoolsVersion() {
4384
- return "0.1.69";
4569
+ return "0.1.71";
4385
4570
  }
4386
4571
  /**
4387
4572
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -4885,7 +5070,7 @@ function createDebugServer(deps) {
4885
5070
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
4886
5071
  const server = new Server({
4887
5072
  name: "ait-debug",
4888
- version: "0.1.69"
5073
+ version: "0.1.71"
4889
5074
  }, { capabilities: { tools: { listChanged: true } } });
4890
5075
  server.setRequestHandler(ListToolsRequestSchema, () => {
4891
5076
  const conn = router.active;
@@ -5641,6 +5826,7 @@ async function bootRelayFamily(options = {}) {
5641
5826
  return {
5642
5827
  connection,
5643
5828
  relayOrigin: "intoss-webview",
5829
+ relayHttpUrl: relay.baseUrl,
5644
5830
  getTunnelStatus: () => tunnelStatus,
5645
5831
  stop() {
5646
5832
  tunnelProbe?.stop();
@@ -5677,6 +5863,7 @@ async function bootExternalRelayFamily(relayBaseUrl) {
5677
5863
  return {
5678
5864
  connection,
5679
5865
  relayOrigin: "external-pwa",
5866
+ relayHttpUrl: relayBaseUrl,
5680
5867
  getTunnelStatus: () => tunnelStatus,
5681
5868
  stop() {
5682
5869
  connection.close();
@@ -5843,7 +6030,16 @@ var DualConnectionRouter = class {
5843
6030
  this.attachWatcher = startAttachWatcher(activeFamily.connection, server, this.deps.attachWatcherIntervalMs ?? 1e3, () => {
5844
6031
  this.deps.diagnosticsCollector.recordAttach();
5845
6032
  this.deps.onPageAttach?.();
5846
- if (activeFamily.connection.kind === "relay") this.deps.devtoolsOpener.open(this.relayTunnelStatus().wssUrl, deriveEnvironment(activeFamily.connection.kind, getLiveIntent(), activeFamily.relayOrigin));
6033
+ if (activeFamily.connection.kind === "relay") {
6034
+ const firstTarget = activeFamily.connection.listTargets()[0];
6035
+ const env = deriveEnvironment(activeFamily.connection.kind, getLiveIntent(), activeFamily.relayOrigin);
6036
+ this.deps.devtoolsOpener.open({
6037
+ relayHttpBaseUrl: activeFamily.relayHttpUrl,
6038
+ targetId: firstTarget?.id,
6039
+ mintTotp: process.env.AIT_DEBUG_TOTP_SECRET ? () => generateTotp(process.env.AIT_DEBUG_TOTP_SECRET) : void 0,
6040
+ env
6041
+ });
6042
+ }
5847
6043
  });
5848
6044
  }
5849
6045
  /**
@@ -6761,7 +6957,7 @@ function createDevServer(deps = {}) {
6761
6957
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
6762
6958
  const server = new Server({
6763
6959
  name: "ait-devtools",
6764
- version: "0.1.69"
6960
+ version: "0.1.71"
6765
6961
  }, { capabilities: { tools: {} } });
6766
6962
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
6767
6963
  server.setRequestHandler(CallToolRequestSchema, async (request) => {