@ait-co/devtools 0.1.143 → 0.2.0

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 (223) hide show
  1. package/README.en.md +65 -163
  2. package/README.md +65 -192
  3. package/dist/in-app/auto.d.ts +1 -138
  4. package/dist/in-app/auto.js +28 -1102
  5. package/dist/in-app/auto.js.map +1 -1
  6. package/dist/in-app/index.d.ts +38 -547
  7. package/dist/in-app/index.d.ts.map +1 -1
  8. package/dist/in-app/index.js +62 -939
  9. package/dist/in-app/index.js.map +1 -1
  10. package/dist/mcp/cli.d.ts +1 -54
  11. package/dist/mcp/cli.js +33 -9720
  12. package/dist/mcp/cli.js.map +1 -1
  13. package/dist/mcp/server.d.ts +1 -88
  14. package/dist/mcp/server.js +36 -1076
  15. package/dist/mcp/server.js.map +1 -1
  16. package/dist/mock/index.d.ts +19 -20
  17. package/dist/mock/index.d.ts.map +1 -1
  18. package/dist/mock/index.js.map +1 -1
  19. package/dist/optional-peers-CDEFhlhJ.cjs +131 -0
  20. package/dist/optional-peers-CDEFhlhJ.cjs.map +1 -0
  21. package/dist/optional-peers-FdVbUJst.js +96 -0
  22. package/dist/optional-peers-FdVbUJst.js.map +1 -0
  23. package/dist/panel/index.js +1 -103
  24. package/dist/panel/index.js.map +1 -1
  25. package/dist/relay-url-store-CPZAn-T5.js +107 -0
  26. package/dist/relay-url-store-CPZAn-T5.js.map +1 -0
  27. package/dist/relay-url-store-DLjlvMSA.cjs +108 -0
  28. package/dist/relay-url-store-DLjlvMSA.cjs.map +1 -0
  29. package/dist/stubs/bin-devtools-mcp.js +58 -0
  30. package/dist/stubs/bin-devtools-mcp.js.map +1 -0
  31. package/dist/stubs/bin-devtools-test.d.ts +2 -0
  32. package/dist/stubs/bin-devtools-test.js +55 -0
  33. package/dist/stubs/bin-devtools-test.js.map +1 -0
  34. package/dist/test-runner/config.d.ts +1 -231
  35. package/dist/test-runner/config.js +41 -45
  36. package/dist/test-runner/config.js.map +1 -1
  37. package/dist/{tunnel-BVSMXctM.cjs → tunnel-BKZkOyQp.cjs} +27 -75
  38. package/dist/tunnel-BKZkOyQp.cjs.map +1 -0
  39. package/dist/{tunnel-D7xkimBu.js → tunnel-CqSCIrdU.js} +27 -75
  40. package/dist/tunnel-CqSCIrdU.js.map +1 -0
  41. package/dist/unplugin/index.cjs +17 -30
  42. package/dist/unplugin/index.cjs.map +1 -1
  43. package/dist/unplugin/index.d.cts +34 -5
  44. package/dist/unplugin/index.d.cts.map +1 -1
  45. package/dist/unplugin/index.d.ts +35 -6
  46. package/dist/unplugin/index.d.ts.map +1 -1
  47. package/dist/unplugin/index.js +17 -30
  48. package/dist/unplugin/index.js.map +1 -1
  49. package/dist/unplugin/tunnel.cjs +61 -74
  50. package/dist/unplugin/tunnel.cjs.map +1 -1
  51. package/dist/unplugin/tunnel.d.cts +22 -16
  52. package/dist/unplugin/tunnel.d.cts.map +1 -1
  53. package/dist/unplugin/tunnel.d.ts +22 -16
  54. package/dist/unplugin/tunnel.d.ts.map +1 -1
  55. package/dist/unplugin/tunnel.js +61 -74
  56. package/dist/unplugin/tunnel.js.map +1 -1
  57. package/package.json +17 -22
  58. package/dist/attach-orchestrator-0F0m_UqQ.js +0 -1845
  59. package/dist/attach-orchestrator-0F0m_UqQ.js.map +0 -1
  60. package/dist/attach-orchestrator-D65KxFy_.js +0 -1831
  61. package/dist/attach-orchestrator-D65KxFy_.js.map +0 -1
  62. package/dist/attach-orchestrator-DL3NQ9ca.js +0 -1846
  63. package/dist/attach-orchestrator-DL3NQ9ca.js.map +0 -1
  64. package/dist/bundle-C796JIwG.d.ts +0 -159
  65. package/dist/bundle-C796JIwG.d.ts.map +0 -1
  66. package/dist/capture-DsP525OZ.d.ts +0 -58
  67. package/dist/capture-DsP525OZ.d.ts.map +0 -1
  68. package/dist/cdp-connection-rP1WdnH5.d.ts +0 -287
  69. package/dist/cdp-connection-rP1WdnH5.d.ts.map +0 -1
  70. package/dist/cell-BaLvusOl.js +0 -68
  71. package/dist/cell-BaLvusOl.js.map +0 -1
  72. package/dist/cell-CBUS3-nT.js +0 -274
  73. package/dist/cell-CBUS3-nT.js.map +0 -1
  74. package/dist/cell-EBKKpAAT.js +0 -307
  75. package/dist/cell-EBKKpAAT.js.map +0 -1
  76. package/dist/chii-relay-BZ3HqWL5.js +0 -304
  77. package/dist/chii-relay-BZ3HqWL5.js.map +0 -1
  78. package/dist/chii-relay-D7eK2acz.cjs +0 -304
  79. package/dist/chii-relay-D7eK2acz.cjs.map +0 -1
  80. package/dist/debug-server-B3ABDrRI.js +0 -456
  81. package/dist/debug-server-B3ABDrRI.js.map +0 -1
  82. package/dist/debug-server-BWhwrVXa.js +0 -1158
  83. package/dist/debug-server-BWhwrVXa.js.map +0 -1
  84. package/dist/debug-server-CfQNxxGW.js +0 -600
  85. package/dist/debug-server-CfQNxxGW.js.map +0 -1
  86. package/dist/deeplink-B5-Hxu0Q.js +0 -62
  87. package/dist/deeplink-B5-Hxu0Q.js.map +0 -1
  88. package/dist/deeplink-BpO9qc-D.js +0 -62
  89. package/dist/deeplink-BpO9qc-D.js.map +0 -1
  90. package/dist/deeplink-BzdbA1gV.cjs +0 -62
  91. package/dist/deeplink-BzdbA1gV.cjs.map +0 -1
  92. package/dist/deeplink-DCScMYcp.cjs +0 -62
  93. package/dist/deeplink-DCScMYcp.cjs.map +0 -1
  94. package/dist/devtools-opener-3Drge_RJ.js +0 -75
  95. package/dist/devtools-opener-3Drge_RJ.js.map +0 -1
  96. package/dist/devtools-opener-B8nxrxqu.js +0 -71
  97. package/dist/devtools-opener-B8nxrxqu.js.map +0 -1
  98. package/dist/devtools-opener-BDY0w3_0.cjs +0 -68
  99. package/dist/devtools-opener-BDY0w3_0.cjs.map +0 -1
  100. package/dist/devtools-opener-BTl5A6Cd.js +0 -71
  101. package/dist/devtools-opener-BTl5A6Cd.js.map +0 -1
  102. package/dist/devtools-opener-CJpEsXXQ.js +0 -76
  103. package/dist/devtools-opener-CJpEsXXQ.js.map +0 -1
  104. package/dist/devtools-opener-CxtryS8c.js +0 -75
  105. package/dist/devtools-opener-CxtryS8c.js.map +0 -1
  106. package/dist/devtools-opener-iv1OwfJN.cjs +0 -68
  107. package/dist/devtools-opener-iv1OwfJN.cjs.map +0 -1
  108. package/dist/in-app/auto.d.ts.map +0 -1
  109. package/dist/mcp/cli.d.ts.map +0 -1
  110. package/dist/mcp/server.d.ts.map +0 -1
  111. package/dist/pool-DcaaOwUq.d.ts +0 -14761
  112. package/dist/pool-DcaaOwUq.d.ts.map +0 -1
  113. package/dist/qr-http-server--gl2-WKc.cjs +0 -1644
  114. package/dist/qr-http-server--gl2-WKc.cjs.map +0 -1
  115. package/dist/qr-http-server-Bb7lMeqi.js +0 -1644
  116. package/dist/qr-http-server-Bb7lMeqi.js.map +0 -1
  117. package/dist/qr-http-server-C_lqOrgc.js +0 -1644
  118. package/dist/qr-http-server-C_lqOrgc.js.map +0 -1
  119. package/dist/qr-http-server-CopuMbub.js +0 -1644
  120. package/dist/qr-http-server-CopuMbub.js.map +0 -1
  121. package/dist/qr-http-server-D-Off6K1.cjs +0 -1644
  122. package/dist/qr-http-server-D-Off6K1.cjs.map +0 -1
  123. package/dist/qr-http-server-DrbIVDjO.js +0 -1645
  124. package/dist/qr-http-server-DrbIVDjO.js.map +0 -1
  125. package/dist/qr-http-server-n1twN18z.js +0 -1644
  126. package/dist/qr-http-server-n1twN18z.js.map +0 -1
  127. package/dist/relay-factory-N9QobQxG.js +0 -206
  128. package/dist/relay-factory-N9QobQxG.js.map +0 -1
  129. package/dist/relay-secret-store-BPhN1upr.js +0 -240
  130. package/dist/relay-secret-store-BPhN1upr.js.map +0 -1
  131. package/dist/relay-secret-store-Bmyleu0A.js +0 -154
  132. package/dist/relay-secret-store-Bmyleu0A.js.map +0 -1
  133. package/dist/relay-secret-store-CQenfcSL.js +0 -154
  134. package/dist/relay-secret-store-CQenfcSL.js.map +0 -1
  135. package/dist/relay-secret-store-DKxs7zwq.js +0 -153
  136. package/dist/relay-secret-store-DKxs7zwq.js.map +0 -1
  137. package/dist/relay-secret-store-DWKdV-eY.cjs +0 -241
  138. package/dist/relay-secret-store-DWKdV-eY.cjs.map +0 -1
  139. package/dist/relay-secret-store-WJ8EGkIl.js +0 -153
  140. package/dist/relay-secret-store-WJ8EGkIl.js.map +0 -1
  141. package/dist/relay-url-store-1FGuSYAn.cjs +0 -115
  142. package/dist/relay-url-store-1FGuSYAn.cjs.map +0 -1
  143. package/dist/relay-url-store-BR2XodiO.js +0 -123
  144. package/dist/relay-url-store-BR2XodiO.js.map +0 -1
  145. package/dist/relay-url-store-Bskcyeg8.js +0 -114
  146. package/dist/relay-url-store-Bskcyeg8.js.map +0 -1
  147. package/dist/relay-url-store-CH63fVCm.js +0 -122
  148. package/dist/relay-url-store-CH63fVCm.js.map +0 -1
  149. package/dist/relay-url-store-DaY1QPes.js +0 -123
  150. package/dist/relay-url-store-DaY1QPes.js.map +0 -1
  151. package/dist/relay-url-store-xmUuTjXA.js +0 -122
  152. package/dist/relay-url-store-xmUuTjXA.js.map +0 -1
  153. package/dist/relay-worker-B5HKkGUY.js +0 -832
  154. package/dist/relay-worker-B5HKkGUY.js.map +0 -1
  155. package/dist/relay-worker-YdlpZQl9.d.ts +0 -214
  156. package/dist/relay-worker-YdlpZQl9.d.ts.map +0 -1
  157. package/dist/rolldown-runtime-DGkTqVfb.js +0 -15
  158. package/dist/rolldown-runtime-DUslC3ob.js +0 -14
  159. package/dist/runtime-kn9DxOeg.d.ts +0 -249
  160. package/dist/runtime-kn9DxOeg.d.ts.map +0 -1
  161. package/dist/test-runner/bin.js +0 -2584
  162. package/dist/test-runner/bin.js.map +0 -1
  163. package/dist/test-runner/bridge-stub.d.ts +0 -125
  164. package/dist/test-runner/bridge-stub.d.ts.map +0 -1
  165. package/dist/test-runner/bridge-stub.js +0 -92
  166. package/dist/test-runner/bridge-stub.js.map +0 -1
  167. package/dist/test-runner/bundle.d.ts +0 -2
  168. package/dist/test-runner/bundle.js +0 -439
  169. package/dist/test-runner/bundle.js.map +0 -1
  170. package/dist/test-runner/capture.d.ts +0 -2
  171. package/dist/test-runner/capture.js +0 -44
  172. package/dist/test-runner/capture.js.map +0 -1
  173. package/dist/test-runner/config.d.ts.map +0 -1
  174. package/dist/test-runner/method-pace.d.ts +0 -82
  175. package/dist/test-runner/method-pace.d.ts.map +0 -1
  176. package/dist/test-runner/method-pace.js +0 -120
  177. package/dist/test-runner/method-pace.js.map +0 -1
  178. package/dist/test-runner/pool.d.ts +0 -2
  179. package/dist/test-runner/pool.js +0 -136
  180. package/dist/test-runner/pool.js.map +0 -1
  181. package/dist/test-runner/relay-factory.d.ts +0 -11245
  182. package/dist/test-runner/relay-factory.d.ts.map +0 -1
  183. package/dist/test-runner/relay-factory.js +0 -206
  184. package/dist/test-runner/relay-factory.js.map +0 -1
  185. package/dist/test-runner/relay-worker.d.ts +0 -2
  186. package/dist/test-runner/relay-worker.js +0 -2
  187. package/dist/test-runner/report.d.ts +0 -163
  188. package/dist/test-runner/report.d.ts.map +0 -1
  189. package/dist/test-runner/report.js +0 -198
  190. package/dist/test-runner/report.js.map +0 -1
  191. package/dist/test-runner/rpc.d.ts +0 -56
  192. package/dist/test-runner/rpc.d.ts.map +0 -1
  193. package/dist/test-runner/rpc.js +0 -98
  194. package/dist/test-runner/rpc.js.map +0 -1
  195. package/dist/test-runner/runtime.d.ts +0 -2
  196. package/dist/test-runner/runtime.js +0 -659
  197. package/dist/test-runner/runtime.js.map +0 -1
  198. package/dist/test-runner/task-graph.d.ts +0 -38
  199. package/dist/test-runner/task-graph.d.ts.map +0 -1
  200. package/dist/test-runner/task-graph.js +0 -182
  201. package/dist/test-runner/task-graph.js.map +0 -1
  202. package/dist/throttle-DKKzX1qC.js +0 -59
  203. package/dist/throttle-DKKzX1qC.js.map +0 -1
  204. package/dist/totp-95OAa20j.js +0 -64
  205. package/dist/totp-95OAa20j.js.map +0 -1
  206. package/dist/totp-BjtoQNfu.cjs +0 -64
  207. package/dist/totp-BjtoQNfu.cjs.map +0 -1
  208. package/dist/totp-CZLLKfOC.js +0 -200
  209. package/dist/totp-CZLLKfOC.js.map +0 -1
  210. package/dist/totp-DAxys-r0.js +0 -199
  211. package/dist/totp-DAxys-r0.js.map +0 -1
  212. package/dist/totp-DIbrZtI7.js +0 -189
  213. package/dist/totp-DIbrZtI7.js.map +0 -1
  214. package/dist/totp-Df252ZdA.cjs +0 -192
  215. package/dist/totp-Df252ZdA.cjs.map +0 -1
  216. package/dist/totp-DfekTBk3.js +0 -211
  217. package/dist/totp-DfekTBk3.js.map +0 -1
  218. package/dist/totp-Dwft0Kz7.js +0 -3
  219. package/dist/totp-WY6l0ysP.js +0 -190
  220. package/dist/totp-WY6l0ysP.js.map +0 -1
  221. package/dist/tunnel-BVSMXctM.cjs.map +0 -1
  222. package/dist/tunnel-D7xkimBu.js.map +0 -1
  223. /package/dist/{test-runner/bin.d.ts → stubs/bin-devtools-mcp.d.ts} +0 -0
@@ -1,456 +0,0 @@
1
- #!/usr/bin/env node
2
- import { c as generateAttachToken, d as startQuickTunnel, f as startTunnelHealthProbe, l as makeTunnelStatus, m as logInfo, p as logError, u as printAttachBanner } from "./attach-orchestrator-DL3NQ9ca.js";
3
- import { n as buildRelayVerifyAuth, t as assertRelayAuthConfigured } from "./totp-CZLLKfOC.js";
4
- import "./cell-EBKKpAAT.js";
5
- import { n as RELAY_AUTH_REJECT_CLOSE_CODE, r as RELAY_AUTH_REJECT_REASON, t as ChiiCdpConnection } from "./test-runner/bin.js";
6
- import "./qr-http-server-DrbIVDjO.js";
7
- import "./relay-secret-store-Bmyleu0A.js";
8
- import { createRequire } from "node:module";
9
- import "node:events";
10
- import { WebSocketServer } from "ws";
11
- import "@modelcontextprotocol/sdk/server/index.js";
12
- import "@modelcontextprotocol/sdk/server/stdio.js";
13
- import "@modelcontextprotocol/sdk/types.js";
14
- import { createServer } from "node:http";
15
- //#region src/mcp/chii-relay.ts
16
- /**
17
- * Boots the local Chii relay server.
18
- *
19
- * Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome
20
- * WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.
21
- * The relay accepts a `target` websocket from the phone's injected `target.js`
22
- * and `client` websockets from CDP frontends (our MCP connection).
23
- *
24
- * Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app
25
- * entries.
26
- *
27
- * TOTP auth (relay-side, authoritative gate):
28
- * When `verifyAuth` is provided, this module gates both inbound surfaces:
29
- *
30
- * - HTTP 'request': a listener registered BEFORE `chii.start({server})`.
31
- * Node's `http.Server` calls listeners in registration order; the first
32
- * to call `res.end()` wins. Invalid auth → 401 + CORS header + a tiny
33
- * JSON body (`{"error":"totp-rejected"}`) so a cross-origin script
34
- * `fetch()` probe can READ the status (issue #478). Valid auth → return
35
- * without side-effect (chii's Koa handler serves it).
36
- *
37
- * - WS 'upgrade': after `chii.start()` has registered chii's own upgrade
38
- * listener, we take over the upgrade chain (remove chii's listeners,
39
- * re-dispatch manually). Invalid auth → accept-then-close: complete the
40
- * handshake via a `noServer` WebSocketServer, then immediately close
41
- * with code 4401 reason 'totp-rejected' (issue #478). A raw 401 +
42
- * `socket.destroy()` only ever surfaced as close code 1006 in the
43
- * browser — indistinguishable from a tunnel failure, which left the
44
- * env-2 phone UI silent. The explicit dispatch (not listener ordering)
45
- * is what keeps chii away from rejected sockets: accept-then-close
46
- * leaves the socket alive, so an order-based early-return would let
47
- * chii's later listener complete a SECOND handshake on the same socket
48
- * — an auth bypass. Valid auth → forward to chii's captured listeners.
49
- *
50
- * TOTP code transports (issue #466) — two equivalent ways to carry the code:
51
- * 1. Query param `at=<code>` — used by the daemon-side `/client` connection
52
- * (`chii-connection.ts` appends it; it holds the secret).
53
- * 2. Path prefix `/at/<code>/…` — used by the phone-side target. Chii's
54
- * stock `target.js` derives its WS endpoint from the script `src`
55
- * (`scriptEl.src.replace('target.js','')`), so the only way for the
56
- * phone to carry a code is to embed it in the script URL path. The
57
- * in-app attach injects `https://<host>/at/<code>/target.js`; both the
58
- * script fetch and the derived `wss://<host>/at/<code>/target/<id>` WS
59
- * dial then carry the prefix. The listeners below rewrite the prefix
60
- * into the query form (`rewriteAtPathPrefix`) and MUTATE `req.url`
61
- * before chii's own handlers (registered later) parse it — chii only
62
- * ever sees the stripped URL.
63
- *
64
- * Threat model: "URL leak" — someone obtains the tunnel URL (Slack paste, QR
65
- * screenshot, shoulder-surfing) but does not have the shared TOTP secret.
66
- * Rotating 6-digit code makes the URL stale after 30 s.
67
- * A determined attacker who extracts the secret from the dogfood bundle can
68
- * still compute valid codes; that is out of scope (see umbrella CLAUDE.md §4).
69
- *
70
- * SECRET-HANDLING: The secret value and computed TOTP codes MUST NOT appear
71
- * in any log, error message, or process output. `verifyAuth` is a black-box
72
- * predicate from the caller's perspective; this module only forwards pass/fail.
73
- */
74
- const require = createRequire(import.meta.url);
75
- /**
76
- * WS keepalive ping interval (ms).
77
- *
78
- * Cloudflare proxied connections are dropped after ~100 s of no traffic.
79
- * 45 s comfortably fits inside that window and lets both the phone-target leg
80
- * and the daemon-client leg survive idle CDP sessions.
81
- */
82
- const DEFAULT_KEEPALIVE_INTERVAL_MS = 45e3;
83
- /**
84
- * Loads chii's internal WebSocketServer class and returns it together with a
85
- * flag indicating whether the real class was found.
86
- *
87
- * Returns `null` if the internal path is not resolvable (future chii release
88
- * changes the layout) — callers skip keepalive gracefully.
89
- */
90
- function tryLoadChiiWssClass() {
91
- try {
92
- const mod = require("chii/server/lib/WebSocketServer");
93
- if (typeof mod === "function") return mod;
94
- } catch {}
95
- return null;
96
- }
97
- /**
98
- * Calls `chii.start()` and returns the chii `WebSocketServer` instance that
99
- * was constructed during the call.
100
- *
101
- * How: `chii/server/index.js`'s `start()` creates `new WebSocketServer()`
102
- * where `WebSocketServer` is captured from `require('./lib/WebSocketServer')`
103
- * at module load time. The class reference is stable, so we can temporarily
104
- * patch `ChiiWssClass.prototype.start` — which runs *on the instance* —
105
- * to record `this` before the original `start` runs.
106
- *
107
- * The patch is installed before `chii.start()` and removed (via `finally`)
108
- * immediately after, so concurrent `startChiiRelay` calls nest correctly: each
109
- * call's patch overrides the previous in the prototype chain for the duration
110
- * of its own `chii.start()` call, restoring the prior descriptor on exit.
111
- *
112
- * If `ChiiWssClass` is null (internal path changed in a future chii release),
113
- * `chii.start()` runs unpatched and the function returns null — callers skip
114
- * keepalive gracefully without affecting relay correctness.
115
- */
116
- async function startChiiWithCapture(chii, startOptions, ChiiWssClass) {
117
- if (ChiiWssClass === null) {
118
- await chii.start(startOptions);
119
- return null;
120
- }
121
- let captured = null;
122
- const proto = ChiiWssClass.prototype;
123
- const originalStart = proto.start;
124
- proto.start = function(server) {
125
- captured = this;
126
- return originalStart.call(this, server);
127
- };
128
- try {
129
- await chii.start(startOptions);
130
- } finally {
131
- proto.start = originalStart;
132
- }
133
- return captured;
134
- }
135
- function loadChiiServer() {
136
- const mod = require("chii");
137
- if (typeof mod === "object" && mod !== null && "start" in mod && typeof mod.start === "function") return mod;
138
- throw new Error("chii server module did not expose start()");
139
- }
140
- /**
141
- * Rewrites a `/at/<code>/…` path-prefixed request URL into the equivalent
142
- * query-based form, e.g.:
143
- *
144
- * `/at/123456/target.js` → `/target.js?at=123456`
145
- * `/at/123456/target/x?url=u` → `/target/x?url=u&at=123456`
146
- * `/at/123456/` → `/?at=123456`
147
- *
148
- * Returns `null` when the URL does not carry the prefix (including an empty
149
- * code segment) — callers fall back to the unmodified URL and the existing
150
- * query-based auth path.
151
- *
152
- * Pure string surgery — this function knows nothing about secrets or code
153
- * validity; verification stays inside the caller-provided `verifyAuth`
154
- * predicate (which parses the query). The raw path segment is appended
155
- * verbatim to the query: both path segments and query values are
156
- * percent-decoded exactly once by their consumers, so no re-encoding is
157
- * needed (TOTP codes are 6 digits and never percent-encoded in practice).
158
- */
159
- function rewriteAtPathPrefix(rawUrl) {
160
- const match = /^\/at\/([^/?]+)(\/[^?]*)?(\?.*)?$/.exec(rawUrl);
161
- if (match === null) return null;
162
- const code = match[1];
163
- const path = match[2] === void 0 || match[2] === "" ? "/" : match[2];
164
- const query = match[3] ?? "";
165
- return `${path}${query}${query === "" ? "?" : "&"}at=${code}`;
166
- }
167
- /**
168
- * Starts the Chii relay and resolves once listening.
169
- *
170
- * Default port is 0 (OS-assigned). With port 0 the OS picks a free ephemeral
171
- * port on every start, so a stale cloudflared orphan holding any particular
172
- * port cannot cause EADDRINUSE. The resolved `ChiiRelay.port` and `baseUrl`
173
- * always reflect the actual bound port.
174
- *
175
- * chii.start() is called with `server` (our pre-created httpServer) BEFORE
176
- * httpServer.listen(). This is intentional: chii attaches its Koa handler and
177
- * WS upgrade listener to the server object, but the actual TCP bind is
178
- * performed by our httpServer.listen() call below. The `port`/`domain` values
179
- * passed to chii.start() are used for display/banner purposes inside chii and
180
- * do not affect which port the server binds. The connection path (clients
181
- * connecting to `relay.baseUrl`) always uses the post-listen confirmed port.
182
- */
183
- async function startChiiRelay(options = {}) {
184
- const requestedPort = options.port ?? 0;
185
- const host = options.host ?? "127.0.0.1";
186
- const { verifyAuth, onAuthReject } = options;
187
- const keepaliveIntervalMs = options.keepaliveIntervalMs !== void 0 ? options.keepaliveIntervalMs : DEFAULT_KEEPALIVE_INTERVAL_MS;
188
- const httpServer = createServer();
189
- const notifyAuthReject = (kind) => {
190
- if (onAuthReject === void 0) return;
191
- try {
192
- onAuthReject({ kind });
193
- } catch {}
194
- };
195
- if (verifyAuth) httpServer.on("request", (req, res) => {
196
- const rewritten = rewriteAtPathPrefix(req.url ?? "");
197
- if (rewritten !== null) {
198
- req.url = rewritten;
199
- if (!verifyAuth(req)) {
200
- res.statusCode = 401;
201
- res.setHeader("Access-Control-Allow-Origin", "*");
202
- res.setHeader("Content-Type", "application/json");
203
- res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
204
- notifyAuthReject("http-request");
205
- }
206
- return;
207
- }
208
- const pathname = (req.url ?? "").split("?")[0];
209
- if (pathname === "/targets" || pathname === "/targets/") {
210
- if (!verifyAuth(req)) {
211
- res.statusCode = 401;
212
- res.setHeader("Access-Control-Allow-Origin", "*");
213
- res.setHeader("Content-Type", "application/json");
214
- res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
215
- notifyAuthReject("http-request");
216
- return;
217
- }
218
- return;
219
- }
220
- });
221
- const chiiWssClass = keepaliveIntervalMs > 0 ? tryLoadChiiWssClass() : null;
222
- const capturedChiiWss = await startChiiWithCapture(loadChiiServer(), {
223
- server: httpServer,
224
- domain: `${host}:${requestedPort}`,
225
- port: requestedPort
226
- }, chiiWssClass);
227
- if (verifyAuth) {
228
- const chiiUpgradeListeners = httpServer.listeners("upgrade");
229
- httpServer.removeAllListeners("upgrade");
230
- const rejectWss = new WebSocketServer({ noServer: true });
231
- httpServer.on("upgrade", (req, socket, head) => {
232
- const rewritten = rewriteAtPathPrefix(req.url ?? "");
233
- if (rewritten !== null) req.url = rewritten;
234
- if (!verifyAuth(req)) {
235
- rejectWss.handleUpgrade(req, socket, head, (ws) => {
236
- ws.close(RELAY_AUTH_REJECT_CLOSE_CODE, RELAY_AUTH_REJECT_REASON);
237
- });
238
- notifyAuthReject("ws-upgrade");
239
- return;
240
- }
241
- for (const listener of chiiUpgradeListeners) listener(req, socket, head);
242
- });
243
- }
244
- const actualPort = await new Promise((resolve, reject) => {
245
- httpServer.once("error", reject);
246
- httpServer.listen(requestedPort, host, () => {
247
- httpServer.off("error", reject);
248
- resolve(httpServer.address().port);
249
- });
250
- });
251
- let keepaliveHandle = null;
252
- if (keepaliveIntervalMs > 0 && capturedChiiWss !== null) {
253
- const chiiWss = capturedChiiWss;
254
- keepaliveHandle = setInterval(() => {
255
- for (const client of chiiWss._wss.clients) if (client.readyState === 1) client.ping();
256
- }, keepaliveIntervalMs);
257
- }
258
- return {
259
- port: actualPort,
260
- baseUrl: `http://${host}:${actualPort}`,
261
- close: () => new Promise((resolve) => {
262
- if (keepaliveHandle !== null) {
263
- clearInterval(keepaliveHandle);
264
- keepaliveHandle = null;
265
- }
266
- if (capturedChiiWss !== null) for (const client of capturedChiiWss._wss.clients) client.terminate();
267
- httpServer.close(() => resolve());
268
- })
269
- };
270
- }
271
- //#endregion
272
- //#region src/mcp/debug-server.ts
273
- /**
274
- * Starts a polling watcher that detects target-set changes on
275
- * `connection.listTargets()` and sends a `notifications/tools/list_changed`
276
- * notification on the given server.
277
- *
278
- * The watcher polls every `intervalMs` (default 1 000 ms). On each tick it
279
- * calls `connection.refreshTargets?.()` first (fix #705-B) so that silent
280
- * disconnects (no CDP event, phone backgrounded / tunnel quiet) are picked up
281
- * before the signature is read. If `refreshTargets` throws — e.g. a transient
282
- * relay error — the tick is skipped entirely to avoid a spurious detach signal.
283
- *
284
- * After the refresh, it fires `server.sendToolListChanged()` + `onAttach()`
285
- * whenever the sorted target-id signature changes AND the new target set is
286
- * non-empty. This covers:
287
- * - 0→N first attach
288
- * - 1→1 target replacement (same count, different id — e.g. rescan)
289
- * - N→M any change where the result is still non-empty
290
- *
291
- * Full detach (→ empty) fires `onDetach()` (fix #705-A) on the exact
292
- * non-empty→empty edge — i.e. only when the previous signature was non-empty.
293
- * This lets callers push an immediate "disconnected" SSE update to the
294
- * dashboard without waiting for the next periodic interval.
295
- *
296
- * The interval is **never cleared automatically** — it keeps running until
297
- * `stop()` is called during shutdown. This ensures that a target replacement
298
- * after the first attach is always detected.
299
- *
300
- * `onAttach` is called on every non-empty signature change (or immediately when
301
- * already attached). Use this to trigger side-effects such as pushing a fresh
302
- * SSE state to open dashboard tabs (issue #509). Both callbacks are optional;
303
- * omitting them preserves the previous behaviour exactly.
304
- *
305
- * SECRET-HANDLING: target `id`/`title`/`url` are not written to any log here.
306
- * Only an attach-detected stderr line is emitted (no target details).
307
- *
308
- * `server` is optional (devtools#772): the standalone test-runner path
309
- * (`test-runner/relay-factory.ts`) has no MCP `Server` instance to notify —
310
- * it only needs the `onAttach`/`onDetach` dashboard-push side effects. When
311
- * omitted, `sendToolListChanged()` is simply skipped; the signature-diff and
312
- * callback logic is byte-for-byte the same. Existing MCP daemon call sites
313
- * always pass a real `Server`, so their behavior is unchanged.
314
- *
315
- * @returns `stop` — call this during shutdown to clear the interval.
316
- */
317
- function startAttachWatcher(connection, server, intervalMs = 1e3, onAttach, onDetach) {
318
- /** Sorted, comma-joined target-id string — '' means no targets attached. */
319
- function signature() {
320
- return connection.listTargets().map((t) => t.id).sort().join(",");
321
- }
322
- let lastSignature = signature();
323
- if (lastSignature !== "") {
324
- server?.sendToolListChanged();
325
- onAttach?.();
326
- }
327
- /** Compare current vs last signature and fire the appropriate callback. */
328
- function tick() {
329
- const current = signature();
330
- if (current !== lastSignature) {
331
- const wasNonEmpty = lastSignature !== "";
332
- lastSignature = current;
333
- if (current !== "") {
334
- server?.sendToolListChanged();
335
- onAttach?.();
336
- } else if (wasNonEmpty) onDetach?.();
337
- }
338
- }
339
- const handle = setInterval(() => {
340
- if (connection.refreshTargets) connection.refreshTargets().then(() => {
341
- tick();
342
- }, (_err) => {});
343
- else tick();
344
- }, intervalMs);
345
- return { stop() {
346
- clearInterval(handle);
347
- } };
348
- }
349
- /**
350
- * Factory that constructs a `ChiiCdpConnection` for the given relay base URL.
351
- *
352
- * Introduced as a named seam so PR-2 (dual-connection, #348) can defer
353
- * construction to first-activation time by moving or replacing this call. Since
354
- * #396 every family (relay included) is constructed lazily on its first
355
- * `start_debug`, so this is always called from the lazy boot path.
356
- *
357
- * The relay base URL is only available after `startChiiRelay()` resolves, so
358
- * the factory is called right after that point (same as before this refactor).
359
- */
360
- function createRelayConnection(relayBaseUrl) {
361
- return new ChiiCdpConnection({
362
- relayBaseUrl,
363
- totpSecret: process.env.AIT_DEBUG_TOTP_SECRET
364
- });
365
- }
366
- /**
367
- * Boots the relay family (issues #348, #356): starts the Chii relay on an
368
- * OS-assigned port (with optional TOTP gate), opens a cloudflared quick tunnel
369
- * to the relay's confirmed port in the background, prints the attach banner,
370
- * and arms the tunnel health probe. Returns a {@link BootedFamily} whose
371
- * `getTunnelStatus()` reflects the live tunnel (it flips up once the background
372
- * tunnel resolves and follows reissues).
373
- *
374
- * Booted lazily via the dual router's `bootLazyFor('relay-intoss')` callback
375
- * (symmetry with {@link bootLocalFamily}), at most once on the first
376
- * `start_debug({ mode: 'relay-staging' })` (all-lazy, #396 — every relay boot now
377
- * flows through `switchMode` after the project-local secret load). `relay-live`
378
- * removed (#665).
379
- *
380
- * The relay base URL is only known after `startChiiRelay()` resolves, so the
381
- * `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside
382
- * this function, after the relay port is confirmed.
383
- *
384
- * SECRET-HANDLING: the TOTP secret rides only inside `verifyAuth`; the wssUrl
385
- * (relay host) is never logged here directly.
386
- */
387
- async function bootRelayFamily(options = {}) {
388
- assertRelayAuthConfigured();
389
- const relayPort = options.relayPort ?? 0;
390
- const totpEnabled = options.verifyAuth !== void 0;
391
- const relay = await startChiiRelay({
392
- port: relayPort,
393
- verifyAuth: options.verifyAuth,
394
- onAuthReject: options.onAuthReject
395
- });
396
- logInfo("server.start", {
397
- port: relay.port,
398
- totpEnabled
399
- });
400
- let tunnel = null;
401
- let tunnelStatus = makeTunnelStatus(false, null);
402
- let tunnelProbe = null;
403
- generateAttachToken();
404
- startQuickTunnel(relay.port).then((t) => {
405
- tunnel = t;
406
- tunnelStatus = makeTunnelStatus(true, t.wssUrl);
407
- options.onWssUrl?.(t.wssUrl);
408
- if (t.childPid !== void 0) options.onTunnelChildPid?.(t.childPid);
409
- logInfo("tunnel.up", { totpEnabled });
410
- tunnelProbe = startTunnelHealthProbe(t, relay.port, {
411
- onReissue: (newTunnel) => {
412
- tunnel = newTunnel;
413
- tunnelStatus = makeTunnelStatus(true, newTunnel.wssUrl, null, 0);
414
- options.onWssUrl?.(newTunnel.wssUrl);
415
- if (newTunnel.childPid !== void 0) options.onTunnelChildPid?.(newTunnel.childPid);
416
- printAttachBanner({
417
- wssUrl: newTunnel.wssUrl,
418
- totpEnabled
419
- }).then(() => {
420
- logInfo("tunnel.up", {
421
- totpEnabled,
422
- reissued: true
423
- });
424
- });
425
- },
426
- onPermanentDrop: (droppedAt) => {
427
- tunnelStatus = makeTunnelStatus(false, null, droppedAt, 3);
428
- logError("tunnel.down", { msg: `tunnel permanently dropped (${droppedAt}). Restart: npx @ait-co/devtools devtools-mcp` });
429
- options.onTunnelDown?.();
430
- }
431
- });
432
- return printAttachBanner({
433
- wssUrl: t.wssUrl,
434
- totpEnabled
435
- });
436
- }, (err) => {
437
- logError("tunnel.down", { msg: `Failed to open cloudflared quick tunnel: ${err instanceof Error ? err.message : String(err)}. The relay is up locally; attach over the public URL is unavailable until the tunnel starts.` });
438
- });
439
- const connection = createRelayConnection(relay.baseUrl);
440
- return {
441
- connection,
442
- relayOrigin: "intoss-webview",
443
- relayHttpUrl: relay.baseUrl,
444
- getTunnelStatus: () => tunnelStatus,
445
- stop() {
446
- tunnelProbe?.stop();
447
- tunnel?.stop();
448
- connection.close();
449
- relay.close();
450
- }
451
- };
452
- }
453
- //#endregion
454
- export { bootRelayFamily, buildRelayVerifyAuth, startAttachWatcher };
455
-
456
- //# sourceMappingURL=debug-server-B3ABDrRI.js.map