@ait-co/devtools 0.1.144 → 0.2.1

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 (182) hide show
  1. package/README.en.md +47 -218
  2. package/README.md +36 -246
  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 +35 -21
  17. package/dist/mock/index.d.ts.map +1 -1
  18. package/dist/mock/index.js +81 -2
  19. package/dist/mock/index.js.map +1 -1
  20. package/dist/panel/index.js +80 -104
  21. package/dist/panel/index.js.map +1 -1
  22. package/dist/relay-url-store-CkVSQZMq.cjs +110 -0
  23. package/dist/relay-url-store-CkVSQZMq.cjs.map +1 -0
  24. package/dist/relay-url-store-dkII-DHD.js +109 -0
  25. package/dist/relay-url-store-dkII-DHD.js.map +1 -0
  26. package/dist/stubs/bin-devtools-mcp.js +58 -0
  27. package/dist/stubs/bin-devtools-mcp.js.map +1 -0
  28. package/dist/stubs/bin-devtools-test.d.ts +2 -0
  29. package/dist/stubs/bin-devtools-test.js +55 -0
  30. package/dist/stubs/bin-devtools-test.js.map +1 -0
  31. package/dist/test-runner/config.d.ts +1 -231
  32. package/dist/test-runner/config.js +41 -45
  33. package/dist/test-runner/config.js.map +1 -1
  34. package/dist/{tunnel-BGT9Curk.cjs → tunnel-BKZkOyQp.cjs} +1 -1
  35. package/dist/{tunnel-BGT9Curk.cjs.map → tunnel-BKZkOyQp.cjs.map} +1 -1
  36. package/dist/{tunnel-BOKmLzBO.js → tunnel-CqSCIrdU.js} +1 -1
  37. package/dist/{tunnel-BOKmLzBO.js.map → tunnel-CqSCIrdU.js.map} +1 -1
  38. package/dist/unplugin/index.cjs +9 -18
  39. package/dist/unplugin/index.cjs.map +1 -1
  40. package/dist/unplugin/index.d.cts +26 -5
  41. package/dist/unplugin/index.d.cts.map +1 -1
  42. package/dist/unplugin/index.d.ts +27 -6
  43. package/dist/unplugin/index.d.ts.map +1 -1
  44. package/dist/unplugin/index.js +10 -19
  45. package/dist/unplugin/index.js.map +1 -1
  46. package/package.json +10 -25
  47. package/dist/attach-orchestrator-0F0m_UqQ.js +0 -1845
  48. package/dist/attach-orchestrator-0F0m_UqQ.js.map +0 -1
  49. package/dist/attach-orchestrator-D65KxFy_.js +0 -1831
  50. package/dist/attach-orchestrator-D65KxFy_.js.map +0 -1
  51. package/dist/attach-orchestrator-DL3NQ9ca.js +0 -1846
  52. package/dist/attach-orchestrator-DL3NQ9ca.js.map +0 -1
  53. package/dist/bundle-C796JIwG.d.ts +0 -159
  54. package/dist/bundle-C796JIwG.d.ts.map +0 -1
  55. package/dist/capture-DsP525OZ.d.ts +0 -58
  56. package/dist/capture-DsP525OZ.d.ts.map +0 -1
  57. package/dist/cdp-connection-rP1WdnH5.d.ts +0 -287
  58. package/dist/cdp-connection-rP1WdnH5.d.ts.map +0 -1
  59. package/dist/cell-BaLvusOl.js +0 -68
  60. package/dist/cell-BaLvusOl.js.map +0 -1
  61. package/dist/cell-CBUS3-nT.js +0 -274
  62. package/dist/cell-CBUS3-nT.js.map +0 -1
  63. package/dist/cell-EBKKpAAT.js +0 -307
  64. package/dist/cell-EBKKpAAT.js.map +0 -1
  65. package/dist/chii-relay-B3ZhjGMi.js +0 -304
  66. package/dist/chii-relay-B3ZhjGMi.js.map +0 -1
  67. package/dist/chii-relay-CGMlePMd.cjs +0 -304
  68. package/dist/chii-relay-CGMlePMd.cjs.map +0 -1
  69. package/dist/debug-server-B3ABDrRI.js +0 -456
  70. package/dist/debug-server-B3ABDrRI.js.map +0 -1
  71. package/dist/debug-server-BWhwrVXa.js +0 -1158
  72. package/dist/debug-server-BWhwrVXa.js.map +0 -1
  73. package/dist/debug-server-CfQNxxGW.js +0 -600
  74. package/dist/debug-server-CfQNxxGW.js.map +0 -1
  75. package/dist/devtools-opener-3Drge_RJ.js +0 -75
  76. package/dist/devtools-opener-3Drge_RJ.js.map +0 -1
  77. package/dist/devtools-opener-CJpEsXXQ.js +0 -76
  78. package/dist/devtools-opener-CJpEsXXQ.js.map +0 -1
  79. package/dist/devtools-opener-CxtryS8c.js +0 -75
  80. package/dist/devtools-opener-CxtryS8c.js.map +0 -1
  81. package/dist/in-app/auto.d.ts.map +0 -1
  82. package/dist/mcp/cli.d.ts.map +0 -1
  83. package/dist/mcp/server.d.ts.map +0 -1
  84. package/dist/pool-DcaaOwUq.d.ts +0 -14761
  85. package/dist/pool-DcaaOwUq.d.ts.map +0 -1
  86. package/dist/qr-http-server-C_lqOrgc.js +0 -1644
  87. package/dist/qr-http-server-C_lqOrgc.js.map +0 -1
  88. package/dist/qr-http-server-CopuMbub.js +0 -1644
  89. package/dist/qr-http-server-CopuMbub.js.map +0 -1
  90. package/dist/qr-http-server-DrbIVDjO.js +0 -1645
  91. package/dist/qr-http-server-DrbIVDjO.js.map +0 -1
  92. package/dist/relay-factory-N9QobQxG.js +0 -206
  93. package/dist/relay-factory-N9QobQxG.js.map +0 -1
  94. package/dist/relay-secret-store-BR0YIkNv.cjs +0 -241
  95. package/dist/relay-secret-store-BR0YIkNv.cjs.map +0 -1
  96. package/dist/relay-secret-store-Bmyleu0A.js +0 -154
  97. package/dist/relay-secret-store-Bmyleu0A.js.map +0 -1
  98. package/dist/relay-secret-store-CQenfcSL.js +0 -154
  99. package/dist/relay-secret-store-CQenfcSL.js.map +0 -1
  100. package/dist/relay-secret-store-CYM8CBIF.js +0 -240
  101. package/dist/relay-secret-store-CYM8CBIF.js.map +0 -1
  102. package/dist/relay-secret-store-DKxs7zwq.js +0 -153
  103. package/dist/relay-secret-store-DKxs7zwq.js.map +0 -1
  104. package/dist/relay-secret-store-WJ8EGkIl.js +0 -153
  105. package/dist/relay-secret-store-WJ8EGkIl.js.map +0 -1
  106. package/dist/relay-url-store-BR2XodiO.js +0 -123
  107. package/dist/relay-url-store-BR2XodiO.js.map +0 -1
  108. package/dist/relay-url-store-C1as_m5G.cjs +0 -115
  109. package/dist/relay-url-store-C1as_m5G.cjs.map +0 -1
  110. package/dist/relay-url-store-CH63fVCm.js +0 -122
  111. package/dist/relay-url-store-CH63fVCm.js.map +0 -1
  112. package/dist/relay-url-store-CzFo_84F.js +0 -114
  113. package/dist/relay-url-store-CzFo_84F.js.map +0 -1
  114. package/dist/relay-url-store-DaY1QPes.js +0 -123
  115. package/dist/relay-url-store-DaY1QPes.js.map +0 -1
  116. package/dist/relay-url-store-xmUuTjXA.js +0 -122
  117. package/dist/relay-url-store-xmUuTjXA.js.map +0 -1
  118. package/dist/relay-worker-B5HKkGUY.js +0 -832
  119. package/dist/relay-worker-B5HKkGUY.js.map +0 -1
  120. package/dist/relay-worker-YdlpZQl9.d.ts +0 -214
  121. package/dist/relay-worker-YdlpZQl9.d.ts.map +0 -1
  122. package/dist/rolldown-runtime-DGkTqVfb.js +0 -15
  123. package/dist/rolldown-runtime-DUslC3ob.js +0 -14
  124. package/dist/runtime-kn9DxOeg.d.ts +0 -249
  125. package/dist/runtime-kn9DxOeg.d.ts.map +0 -1
  126. package/dist/test-runner/bin.js +0 -2584
  127. package/dist/test-runner/bin.js.map +0 -1
  128. package/dist/test-runner/bridge-stub.d.ts +0 -125
  129. package/dist/test-runner/bridge-stub.d.ts.map +0 -1
  130. package/dist/test-runner/bridge-stub.js +0 -92
  131. package/dist/test-runner/bridge-stub.js.map +0 -1
  132. package/dist/test-runner/bundle.d.ts +0 -2
  133. package/dist/test-runner/bundle.js +0 -439
  134. package/dist/test-runner/bundle.js.map +0 -1
  135. package/dist/test-runner/capture.d.ts +0 -2
  136. package/dist/test-runner/capture.js +0 -44
  137. package/dist/test-runner/capture.js.map +0 -1
  138. package/dist/test-runner/config.d.ts.map +0 -1
  139. package/dist/test-runner/method-pace.d.ts +0 -82
  140. package/dist/test-runner/method-pace.d.ts.map +0 -1
  141. package/dist/test-runner/method-pace.js +0 -120
  142. package/dist/test-runner/method-pace.js.map +0 -1
  143. package/dist/test-runner/pool.d.ts +0 -2
  144. package/dist/test-runner/pool.js +0 -136
  145. package/dist/test-runner/pool.js.map +0 -1
  146. package/dist/test-runner/relay-factory.d.ts +0 -11245
  147. package/dist/test-runner/relay-factory.d.ts.map +0 -1
  148. package/dist/test-runner/relay-factory.js +0 -206
  149. package/dist/test-runner/relay-factory.js.map +0 -1
  150. package/dist/test-runner/relay-worker.d.ts +0 -2
  151. package/dist/test-runner/relay-worker.js +0 -2
  152. package/dist/test-runner/report.d.ts +0 -163
  153. package/dist/test-runner/report.d.ts.map +0 -1
  154. package/dist/test-runner/report.js +0 -198
  155. package/dist/test-runner/report.js.map +0 -1
  156. package/dist/test-runner/rpc.d.ts +0 -56
  157. package/dist/test-runner/rpc.d.ts.map +0 -1
  158. package/dist/test-runner/rpc.js +0 -98
  159. package/dist/test-runner/rpc.js.map +0 -1
  160. package/dist/test-runner/runtime.d.ts +0 -2
  161. package/dist/test-runner/runtime.js +0 -659
  162. package/dist/test-runner/runtime.js.map +0 -1
  163. package/dist/test-runner/task-graph.d.ts +0 -38
  164. package/dist/test-runner/task-graph.d.ts.map +0 -1
  165. package/dist/test-runner/task-graph.js +0 -182
  166. package/dist/test-runner/task-graph.js.map +0 -1
  167. package/dist/throttle-DKKzX1qC.js +0 -59
  168. package/dist/throttle-DKKzX1qC.js.map +0 -1
  169. package/dist/totp-BqmCLSNA.js +0 -189
  170. package/dist/totp-BqmCLSNA.js.map +0 -1
  171. package/dist/totp-CMHR5lsW.cjs +0 -191
  172. package/dist/totp-CMHR5lsW.cjs.map +0 -1
  173. package/dist/totp-CZLLKfOC.js +0 -200
  174. package/dist/totp-CZLLKfOC.js.map +0 -1
  175. package/dist/totp-DAxys-r0.js +0 -199
  176. package/dist/totp-DAxys-r0.js.map +0 -1
  177. package/dist/totp-DfekTBk3.js +0 -211
  178. package/dist/totp-DfekTBk3.js.map +0 -1
  179. package/dist/totp-Dwft0Kz7.js +0 -3
  180. package/dist/totp-WY6l0ysP.js +0 -190
  181. package/dist/totp-WY6l0ysP.js.map +0 -1
  182. /package/dist/{test-runner/bin.d.ts → stubs/bin-devtools-mcp.d.ts} +0 -0
@@ -1,600 +0,0 @@
1
- import { a as ChiiCdpConnection, o as RELAY_AUTH_REJECT_CLOSE_CODE, s as RELAY_AUTH_REJECT_REASON } from "./relay-worker-B5HKkGUY.js";
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-0F0m_UqQ.js";
3
- import { n as buildRelayVerifyAuth, t as assertRelayAuthConfigured } from "./totp-DAxys-r0.js";
4
- import "./cell-CBUS3-nT.js";
5
- import "./relay-factory-N9QobQxG.js";
6
- import "./qr-http-server-CopuMbub.js";
7
- import "./relay-secret-store-WJ8EGkIl.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
- `
16
- devtools-test — run mini-app tests on a real device WebView over the CDP relay
17
-
18
- USAGE
19
- devtools-test <glob> [<glob> ...] [options]
20
-
21
- OPTIONS
22
- --scheme-url <url> intoss-private:// URL from \`ait deploy --scheme-only\`
23
- (required for standalone relay attach / env3). Unused
24
- and ignored when --attach-launcher is set.
25
- --attach-launcher Attach over the env-2 AITC Sandbox PWA launcher
26
- (real-device WebKit) instead of the env-3 intoss
27
- scheme deep-link. The relay/QR/dashboard/attach-wait
28
- are identical; only the QR is a launcher deep-link.
29
- Requires --app-url. The SDK still hits the mock in
30
- env 2, so this measures the mock + standard Web API
31
- layer's engine-attributable behavior (env1↔env2
32
- equivalence), NOT native-bridge fidelity (that is env3).
33
- --app-url <url> The consumer dev server's HTTP tunnel URL (e.g. the
34
- *.trycloudflare.com URL from \`pnpm dev:phone:cdp\`)
35
- that the launcher PWA frames. REQUIRED with
36
- --attach-launcher; ignored otherwise. SECRET: a tunnel
37
- host — never printed; it rides only inside the QR.
38
- --timeout <ms> Per-file evaluate timeout in ms (default: 60000).
39
- Controls how long a single test file is allowed to run
40
- before it is considered hung. Does NOT affect how long
41
- the CLI waits for a human to scan the QR code — use
42
- --attach-timeout for that.
43
- --attach-timeout <ms> How long to wait for a human to scan the QR code with
44
- their phone. Omit (default) to wait indefinitely — the
45
- runner stays up until you stop it (Ctrl-C/SIGTERM).
46
- Pass a value to bound the wait for CI/headless runs.
47
- --cell-sdk-line <line> SDK line to inject as __AIT_CELL__.sdkLine (2.x|3.x)
48
- --cell-platform <plat> Platform to inject as __AIT_CELL__.platform
49
- (mock|ios|android|ios-pwa, default: AIT_CELL_PLATFORM
50
- env). Use ios-pwa for env-2 (--attach-launcher) runs
51
- so env1(mock@desktop) and env2(mock@WebKit) captures
52
- stay distinguishable. An unknown value is rejected.
53
- --report-dir <dir> Persist a runner-agnostic report + captures to <dir>
54
- (report: <sdkLine>.<platform>.json; captures:
55
- <dir>/.ait-capture/<category>.<sdkLine>.<platform>.json).
56
- Omitted = nothing saved. Enables console capture.
57
- --dashboard-port <port> Base port for the QR dashboard HTTP server. On
58
- EADDRINUSE it increments (+1, up to 20 tries) before
59
- falling back to an ephemeral port. Omit to use
60
- AIT_DEBUG_HTTP_PORT env or the built-in default
61
- (8317) — pass 0 to force a random ephemeral port.
62
- --no-qr-stdout Suppress the QR/attach block on stdout (auto-on for
63
- non-interactive stdout / CI / AIT_NO_QR_STDOUT)
64
- --headless Disable browser auto-open (text QR only)
65
- --project-root <dir> Project root for .ait_relay secret lookup
66
- (default: current working directory)
67
- --pace <ms> Minimum delay in ms between test-to-test AND
68
- file-to-file bridge calls (default: 0, i.e. no
69
- added delay — today's behavior byte-for-byte).
70
- Falls back to the AIT_PACE env var when omitted
71
- (--pace takes precedence over the env var when both
72
- are given). Use on a 2.x cell scan when the native
73
- per-method bridge rate limit (APP_BRIDGE_THROTTLED,
74
- devtools#767) is rejecting rapid same-method calls —
75
- 3.x cells are unaffected by that limiter and do not
76
- need this flag.
77
- --pace-method <ms> Minimum delay in ms BETWEEN calls to the SAME named
78
- SDK function — paces a same-method burst WITHIN a
79
- single test body (e.g. a clipboard happy-path loop
80
- calling setClipboardText/getClipboardText 8 times
81
- back to back), which --pace's test/file spacing
82
- cannot reach (devtools#769). Falls back to the
83
- AIT_PACE_METHOD env var when omitted (--pace-method
84
- takes precedence over the env var when both are
85
- given). Default: 250ms when --cell-sdk-line is 2.x
86
- (unset --cell-sdk-line also defaults to 2.x — see
87
- --cell-sdk-line), 0 (no added delay) otherwise. Pass
88
- --pace-method 0 to opt out even on a 2.x cell.
89
- --manual-blocking Run manual-tagged test files (*.manual.ait.test.ts)
90
- LAST, after all regular files, with a human present.
91
- Before each manual file, the QR dashboard is pushed
92
- a step-by-step Korean prompt naming the file + its
93
- progress (k/n), and the same line is printed to
94
- stdout. Manual files get a 5-minute per-file evaluate
95
- timeout (vs. --timeout for everything else) since a
96
- human is expected to tap through a native sheet
97
- (photo picker, permission dialog, fullscreen ad).
98
- Without this flag (default off), *.manual.ait.test.ts
99
- files are EXCLUDED from the glob expansion entirely —
100
- existing unattended runs are byte-for-byte unaffected.
101
- With --report-dir, a run that included manual files
102
- ALSO writes <sdkLine>.<platform>.manual.json
103
- alongside (never replacing) the standard report, and
104
- each manual file's report entry is stamped
105
- mode: 'manual' — never diff a manual run against an
106
- unattended baseline as if they were equivalent.
107
- --stub-blocking Run manual-tagged test files (*.manual.ait.test.ts)
108
- UNATTENDED (devtools#740, DT-2) by intercepting a
109
- fixed allowlist of blocking-UI SDK calls (ads
110
- show*, openPermissionDialog/requestPermission,
111
- saveBase64Data) in the page and answering them from
112
- fixtures captured by a real --manual-blocking run,
113
- instead of forwarding them to native UI. Implies
114
- --manual-blocking (manual files are included in the
115
- run); no human presence or QR-dashboard prompt is
116
- needed for them. HYBRID cell, not pure env3 — every
117
- other SDK call in the same run still hits the real
118
- native bridge. With --report-dir, files that ran
119
- under the stub are written to a SEPARATE
120
- <sdkLine>.<platform>.stubbed.json artifact (never
121
- merged into the standard or .manual.json report) and
122
- the report body is stamped cell.bridgeStub: true —
123
- never diff a stubbed run against a real device
124
- baseline (manual or unattended) as if equivalent.
125
- --help, -h Show this help message
126
-
127
- DESCRIPTION
128
- Boots a Chii relay + cloudflared tunnel, renders a QR code, waits for a real
129
- device to scan and attach, injects the cell globals (__AIT_CELL__), bundles
130
- each matched test file with esbuild (SDK imports redirected to window.__sdk),
131
- injects the bundle into the attached WebView via Runtime.evaluate, and prints
132
- a summary.
133
-
134
- With --report-dir, also harvests __AIT_CAPTURE__ console lines and writes a
135
- runner-agnostic report + per-category capture files so 2.x↔3.0 runs can be
136
- compared offline.
137
-
138
- The test files run against the live relay connection started by this process;
139
- no separate MCP daemon is required.
140
-
141
- EXAMPLE (env 3 — intoss-private scheme)
142
- devtools-test 'src/**/*.ait.test.ts' \\
143
- --scheme-url "intoss-private://..." \\
144
- --cell-sdk-line 3.x \\
145
- --cell-platform ios \\
146
- --report-dir .ait-report \\
147
- --timeout 60000
148
-
149
- EXAMPLE (env 2 — AITC Sandbox PWA launcher)
150
- devtools-test 'src/**/*.ait.test.ts' \\
151
- --attach-launcher \\
152
- --app-url "https://<subdomain>.trycloudflare.com" \\
153
- --cell-sdk-line 3.x \\
154
- --cell-platform ios-pwa \\
155
- --report-dir .ait-report
156
-
157
- `.trimStart();
158
- //#endregion
159
- //#region src/mcp/chii-relay.ts
160
- /**
161
- * Boots the local Chii relay server.
162
- *
163
- * Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome
164
- * WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.
165
- * The relay accepts a `target` websocket from the phone's injected `target.js`
166
- * and `client` websockets from CDP frontends (our MCP connection).
167
- *
168
- * Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app
169
- * entries.
170
- *
171
- * TOTP auth (relay-side, authoritative gate):
172
- * When `verifyAuth` is provided, this module gates both inbound surfaces:
173
- *
174
- * - HTTP 'request': a listener registered BEFORE `chii.start({server})`.
175
- * Node's `http.Server` calls listeners in registration order; the first
176
- * to call `res.end()` wins. Invalid auth → 401 + CORS header + a tiny
177
- * JSON body (`{"error":"totp-rejected"}`) so a cross-origin script
178
- * `fetch()` probe can READ the status (issue #478). Valid auth → return
179
- * without side-effect (chii's Koa handler serves it).
180
- *
181
- * - WS 'upgrade': after `chii.start()` has registered chii's own upgrade
182
- * listener, we take over the upgrade chain (remove chii's listeners,
183
- * re-dispatch manually). Invalid auth → accept-then-close: complete the
184
- * handshake via a `noServer` WebSocketServer, then immediately close
185
- * with code 4401 reason 'totp-rejected' (issue #478). A raw 401 +
186
- * `socket.destroy()` only ever surfaced as close code 1006 in the
187
- * browser — indistinguishable from a tunnel failure, which left the
188
- * env-2 phone UI silent. The explicit dispatch (not listener ordering)
189
- * is what keeps chii away from rejected sockets: accept-then-close
190
- * leaves the socket alive, so an order-based early-return would let
191
- * chii's later listener complete a SECOND handshake on the same socket
192
- * — an auth bypass. Valid auth → forward to chii's captured listeners.
193
- *
194
- * TOTP code transports (issue #466) — two equivalent ways to carry the code:
195
- * 1. Query param `at=<code>` — used by the daemon-side `/client` connection
196
- * (`chii-connection.ts` appends it; it holds the secret).
197
- * 2. Path prefix `/at/<code>/…` — used by the phone-side target. Chii's
198
- * stock `target.js` derives its WS endpoint from the script `src`
199
- * (`scriptEl.src.replace('target.js','')`), so the only way for the
200
- * phone to carry a code is to embed it in the script URL path. The
201
- * in-app attach injects `https://<host>/at/<code>/target.js`; both the
202
- * script fetch and the derived `wss://<host>/at/<code>/target/<id>` WS
203
- * dial then carry the prefix. The listeners below rewrite the prefix
204
- * into the query form (`rewriteAtPathPrefix`) and MUTATE `req.url`
205
- * before chii's own handlers (registered later) parse it — chii only
206
- * ever sees the stripped URL.
207
- *
208
- * Threat model: "URL leak" — someone obtains the tunnel URL (Slack paste, QR
209
- * screenshot, shoulder-surfing) but does not have the shared TOTP secret.
210
- * Rotating 6-digit code makes the URL stale after 30 s.
211
- * A determined attacker who extracts the secret from the dogfood bundle can
212
- * still compute valid codes; that is out of scope (see umbrella CLAUDE.md §4).
213
- *
214
- * SECRET-HANDLING: The secret value and computed TOTP codes MUST NOT appear
215
- * in any log, error message, or process output. `verifyAuth` is a black-box
216
- * predicate from the caller's perspective; this module only forwards pass/fail.
217
- */
218
- const require = createRequire(import.meta.url);
219
- /**
220
- * WS keepalive ping interval (ms).
221
- *
222
- * Cloudflare proxied connections are dropped after ~100 s of no traffic.
223
- * 45 s comfortably fits inside that window and lets both the phone-target leg
224
- * and the daemon-client leg survive idle CDP sessions.
225
- */
226
- const DEFAULT_KEEPALIVE_INTERVAL_MS = 45e3;
227
- /**
228
- * Loads chii's internal WebSocketServer class and returns it together with a
229
- * flag indicating whether the real class was found.
230
- *
231
- * Returns `null` if the internal path is not resolvable (future chii release
232
- * changes the layout) — callers skip keepalive gracefully.
233
- */
234
- function tryLoadChiiWssClass() {
235
- try {
236
- const mod = require("chii/server/lib/WebSocketServer");
237
- if (typeof mod === "function") return mod;
238
- } catch {}
239
- return null;
240
- }
241
- /**
242
- * Calls `chii.start()` and returns the chii `WebSocketServer` instance that
243
- * was constructed during the call.
244
- *
245
- * How: `chii/server/index.js`'s `start()` creates `new WebSocketServer()`
246
- * where `WebSocketServer` is captured from `require('./lib/WebSocketServer')`
247
- * at module load time. The class reference is stable, so we can temporarily
248
- * patch `ChiiWssClass.prototype.start` — which runs *on the instance* —
249
- * to record `this` before the original `start` runs.
250
- *
251
- * The patch is installed before `chii.start()` and removed (via `finally`)
252
- * immediately after, so concurrent `startChiiRelay` calls nest correctly: each
253
- * call's patch overrides the previous in the prototype chain for the duration
254
- * of its own `chii.start()` call, restoring the prior descriptor on exit.
255
- *
256
- * If `ChiiWssClass` is null (internal path changed in a future chii release),
257
- * `chii.start()` runs unpatched and the function returns null — callers skip
258
- * keepalive gracefully without affecting relay correctness.
259
- */
260
- async function startChiiWithCapture(chii, startOptions, ChiiWssClass) {
261
- if (ChiiWssClass === null) {
262
- await chii.start(startOptions);
263
- return null;
264
- }
265
- let captured = null;
266
- const proto = ChiiWssClass.prototype;
267
- const originalStart = proto.start;
268
- proto.start = function(server) {
269
- captured = this;
270
- return originalStart.call(this, server);
271
- };
272
- try {
273
- await chii.start(startOptions);
274
- } finally {
275
- proto.start = originalStart;
276
- }
277
- return captured;
278
- }
279
- function loadChiiServer() {
280
- const mod = require("chii");
281
- if (typeof mod === "object" && mod !== null && "start" in mod && typeof mod.start === "function") return mod;
282
- throw new Error("chii server module did not expose start()");
283
- }
284
- /**
285
- * Rewrites a `/at/<code>/…` path-prefixed request URL into the equivalent
286
- * query-based form, e.g.:
287
- *
288
- * `/at/123456/target.js` → `/target.js?at=123456`
289
- * `/at/123456/target/x?url=u` → `/target/x?url=u&at=123456`
290
- * `/at/123456/` → `/?at=123456`
291
- *
292
- * Returns `null` when the URL does not carry the prefix (including an empty
293
- * code segment) — callers fall back to the unmodified URL and the existing
294
- * query-based auth path.
295
- *
296
- * Pure string surgery — this function knows nothing about secrets or code
297
- * validity; verification stays inside the caller-provided `verifyAuth`
298
- * predicate (which parses the query). The raw path segment is appended
299
- * verbatim to the query: both path segments and query values are
300
- * percent-decoded exactly once by their consumers, so no re-encoding is
301
- * needed (TOTP codes are 6 digits and never percent-encoded in practice).
302
- */
303
- function rewriteAtPathPrefix(rawUrl) {
304
- const match = /^\/at\/([^/?]+)(\/[^?]*)?(\?.*)?$/.exec(rawUrl);
305
- if (match === null) return null;
306
- const code = match[1];
307
- const path = match[2] === void 0 || match[2] === "" ? "/" : match[2];
308
- const query = match[3] ?? "";
309
- return `${path}${query}${query === "" ? "?" : "&"}at=${code}`;
310
- }
311
- /**
312
- * Starts the Chii relay and resolves once listening.
313
- *
314
- * Default port is 0 (OS-assigned). With port 0 the OS picks a free ephemeral
315
- * port on every start, so a stale cloudflared orphan holding any particular
316
- * port cannot cause EADDRINUSE. The resolved `ChiiRelay.port` and `baseUrl`
317
- * always reflect the actual bound port.
318
- *
319
- * chii.start() is called with `server` (our pre-created httpServer) BEFORE
320
- * httpServer.listen(). This is intentional: chii attaches its Koa handler and
321
- * WS upgrade listener to the server object, but the actual TCP bind is
322
- * performed by our httpServer.listen() call below. The `port`/`domain` values
323
- * passed to chii.start() are used for display/banner purposes inside chii and
324
- * do not affect which port the server binds. The connection path (clients
325
- * connecting to `relay.baseUrl`) always uses the post-listen confirmed port.
326
- */
327
- async function startChiiRelay(options = {}) {
328
- const requestedPort = options.port ?? 0;
329
- const host = options.host ?? "127.0.0.1";
330
- const { verifyAuth, onAuthReject } = options;
331
- const keepaliveIntervalMs = options.keepaliveIntervalMs !== void 0 ? options.keepaliveIntervalMs : DEFAULT_KEEPALIVE_INTERVAL_MS;
332
- const httpServer = createServer();
333
- const notifyAuthReject = (kind) => {
334
- if (onAuthReject === void 0) return;
335
- try {
336
- onAuthReject({ kind });
337
- } catch {}
338
- };
339
- if (verifyAuth) httpServer.on("request", (req, res) => {
340
- const rewritten = rewriteAtPathPrefix(req.url ?? "");
341
- if (rewritten !== null) {
342
- req.url = rewritten;
343
- if (!verifyAuth(req)) {
344
- res.statusCode = 401;
345
- res.setHeader("Access-Control-Allow-Origin", "*");
346
- res.setHeader("Content-Type", "application/json");
347
- res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
348
- notifyAuthReject("http-request");
349
- }
350
- return;
351
- }
352
- const pathname = (req.url ?? "").split("?")[0];
353
- if (pathname === "/targets" || pathname === "/targets/") {
354
- if (!verifyAuth(req)) {
355
- res.statusCode = 401;
356
- res.setHeader("Access-Control-Allow-Origin", "*");
357
- res.setHeader("Content-Type", "application/json");
358
- res.end(JSON.stringify({ error: RELAY_AUTH_REJECT_REASON }));
359
- notifyAuthReject("http-request");
360
- return;
361
- }
362
- return;
363
- }
364
- });
365
- const chiiWssClass = keepaliveIntervalMs > 0 ? tryLoadChiiWssClass() : null;
366
- const capturedChiiWss = await startChiiWithCapture(loadChiiServer(), {
367
- server: httpServer,
368
- domain: `${host}:${requestedPort}`,
369
- port: requestedPort
370
- }, chiiWssClass);
371
- if (verifyAuth) {
372
- const chiiUpgradeListeners = httpServer.listeners("upgrade");
373
- httpServer.removeAllListeners("upgrade");
374
- const rejectWss = new WebSocketServer({ noServer: true });
375
- httpServer.on("upgrade", (req, socket, head) => {
376
- const rewritten = rewriteAtPathPrefix(req.url ?? "");
377
- if (rewritten !== null) req.url = rewritten;
378
- if (!verifyAuth(req)) {
379
- rejectWss.handleUpgrade(req, socket, head, (ws) => {
380
- ws.close(RELAY_AUTH_REJECT_CLOSE_CODE, RELAY_AUTH_REJECT_REASON);
381
- });
382
- notifyAuthReject("ws-upgrade");
383
- return;
384
- }
385
- for (const listener of chiiUpgradeListeners) listener(req, socket, head);
386
- });
387
- }
388
- const actualPort = await new Promise((resolve, reject) => {
389
- httpServer.once("error", reject);
390
- httpServer.listen(requestedPort, host, () => {
391
- httpServer.off("error", reject);
392
- resolve(httpServer.address().port);
393
- });
394
- });
395
- let keepaliveHandle = null;
396
- if (keepaliveIntervalMs > 0 && capturedChiiWss !== null) {
397
- const chiiWss = capturedChiiWss;
398
- keepaliveHandle = setInterval(() => {
399
- for (const client of chiiWss._wss.clients) if (client.readyState === 1) client.ping();
400
- }, keepaliveIntervalMs);
401
- }
402
- return {
403
- port: actualPort,
404
- baseUrl: `http://${host}:${actualPort}`,
405
- close: () => new Promise((resolve) => {
406
- if (keepaliveHandle !== null) {
407
- clearInterval(keepaliveHandle);
408
- keepaliveHandle = null;
409
- }
410
- if (capturedChiiWss !== null) for (const client of capturedChiiWss._wss.clients) client.terminate();
411
- httpServer.close(() => resolve());
412
- })
413
- };
414
- }
415
- //#endregion
416
- //#region src/mcp/debug-server.ts
417
- /**
418
- * Starts a polling watcher that detects target-set changes on
419
- * `connection.listTargets()` and sends a `notifications/tools/list_changed`
420
- * notification on the given server.
421
- *
422
- * The watcher polls every `intervalMs` (default 1 000 ms). On each tick it
423
- * calls `connection.refreshTargets?.()` first (fix #705-B) so that silent
424
- * disconnects (no CDP event, phone backgrounded / tunnel quiet) are picked up
425
- * before the signature is read. If `refreshTargets` throws — e.g. a transient
426
- * relay error — the tick is skipped entirely to avoid a spurious detach signal.
427
- *
428
- * After the refresh, it fires `server.sendToolListChanged()` + `onAttach()`
429
- * whenever the sorted target-id signature changes AND the new target set is
430
- * non-empty. This covers:
431
- * - 0→N first attach
432
- * - 1→1 target replacement (same count, different id — e.g. rescan)
433
- * - N→M any change where the result is still non-empty
434
- *
435
- * Full detach (→ empty) fires `onDetach()` (fix #705-A) on the exact
436
- * non-empty→empty edge — i.e. only when the previous signature was non-empty.
437
- * This lets callers push an immediate "disconnected" SSE update to the
438
- * dashboard without waiting for the next periodic interval.
439
- *
440
- * The interval is **never cleared automatically** — it keeps running until
441
- * `stop()` is called during shutdown. This ensures that a target replacement
442
- * after the first attach is always detected.
443
- *
444
- * `onAttach` is called on every non-empty signature change (or immediately when
445
- * already attached). Use this to trigger side-effects such as pushing a fresh
446
- * SSE state to open dashboard tabs (issue #509). Both callbacks are optional;
447
- * omitting them preserves the previous behaviour exactly.
448
- *
449
- * SECRET-HANDLING: target `id`/`title`/`url` are not written to any log here.
450
- * Only an attach-detected stderr line is emitted (no target details).
451
- *
452
- * `server` is optional (devtools#772): the standalone test-runner path
453
- * (`test-runner/relay-factory.ts`) has no MCP `Server` instance to notify —
454
- * it only needs the `onAttach`/`onDetach` dashboard-push side effects. When
455
- * omitted, `sendToolListChanged()` is simply skipped; the signature-diff and
456
- * callback logic is byte-for-byte the same. Existing MCP daemon call sites
457
- * always pass a real `Server`, so their behavior is unchanged.
458
- *
459
- * @returns `stop` — call this during shutdown to clear the interval.
460
- */
461
- function startAttachWatcher(connection, server, intervalMs = 1e3, onAttach, onDetach) {
462
- /** Sorted, comma-joined target-id string — '' means no targets attached. */
463
- function signature() {
464
- return connection.listTargets().map((t) => t.id).sort().join(",");
465
- }
466
- let lastSignature = signature();
467
- if (lastSignature !== "") {
468
- server?.sendToolListChanged();
469
- onAttach?.();
470
- }
471
- /** Compare current vs last signature and fire the appropriate callback. */
472
- function tick() {
473
- const current = signature();
474
- if (current !== lastSignature) {
475
- const wasNonEmpty = lastSignature !== "";
476
- lastSignature = current;
477
- if (current !== "") {
478
- server?.sendToolListChanged();
479
- onAttach?.();
480
- } else if (wasNonEmpty) onDetach?.();
481
- }
482
- }
483
- const handle = setInterval(() => {
484
- if (connection.refreshTargets) connection.refreshTargets().then(() => {
485
- tick();
486
- }, (_err) => {});
487
- else tick();
488
- }, intervalMs);
489
- return { stop() {
490
- clearInterval(handle);
491
- } };
492
- }
493
- /**
494
- * Factory that constructs a `ChiiCdpConnection` for the given relay base URL.
495
- *
496
- * Introduced as a named seam so PR-2 (dual-connection, #348) can defer
497
- * construction to first-activation time by moving or replacing this call. Since
498
- * #396 every family (relay included) is constructed lazily on its first
499
- * `start_debug`, so this is always called from the lazy boot path.
500
- *
501
- * The relay base URL is only available after `startChiiRelay()` resolves, so
502
- * the factory is called right after that point (same as before this refactor).
503
- */
504
- function createRelayConnection(relayBaseUrl) {
505
- return new ChiiCdpConnection({
506
- relayBaseUrl,
507
- totpSecret: process.env.AIT_DEBUG_TOTP_SECRET
508
- });
509
- }
510
- /**
511
- * Boots the relay family (issues #348, #356): starts the Chii relay on an
512
- * OS-assigned port (with optional TOTP gate), opens a cloudflared quick tunnel
513
- * to the relay's confirmed port in the background, prints the attach banner,
514
- * and arms the tunnel health probe. Returns a {@link BootedFamily} whose
515
- * `getTunnelStatus()` reflects the live tunnel (it flips up once the background
516
- * tunnel resolves and follows reissues).
517
- *
518
- * Booted lazily via the dual router's `bootLazyFor('relay-intoss')` callback
519
- * (symmetry with {@link bootLocalFamily}), at most once on the first
520
- * `start_debug({ mode: 'relay-staging' })` (all-lazy, #396 — every relay boot now
521
- * flows through `switchMode` after the project-local secret load). `relay-live`
522
- * removed (#665).
523
- *
524
- * The relay base URL is only known after `startChiiRelay()` resolves, so the
525
- * `ChiiCdpConnection` (via {@link createRelayConnection}) is constructed inside
526
- * this function, after the relay port is confirmed.
527
- *
528
- * SECRET-HANDLING: the TOTP secret rides only inside `verifyAuth`; the wssUrl
529
- * (relay host) is never logged here directly.
530
- */
531
- async function bootRelayFamily(options = {}) {
532
- assertRelayAuthConfigured();
533
- const relayPort = options.relayPort ?? 0;
534
- const totpEnabled = options.verifyAuth !== void 0;
535
- const relay = await startChiiRelay({
536
- port: relayPort,
537
- verifyAuth: options.verifyAuth,
538
- onAuthReject: options.onAuthReject
539
- });
540
- logInfo("server.start", {
541
- port: relay.port,
542
- totpEnabled
543
- });
544
- let tunnel = null;
545
- let tunnelStatus = makeTunnelStatus(false, null);
546
- let tunnelProbe = null;
547
- generateAttachToken();
548
- startQuickTunnel(relay.port).then((t) => {
549
- tunnel = t;
550
- tunnelStatus = makeTunnelStatus(true, t.wssUrl);
551
- options.onWssUrl?.(t.wssUrl);
552
- if (t.childPid !== void 0) options.onTunnelChildPid?.(t.childPid);
553
- logInfo("tunnel.up", { totpEnabled });
554
- tunnelProbe = startTunnelHealthProbe(t, relay.port, {
555
- onReissue: (newTunnel) => {
556
- tunnel = newTunnel;
557
- tunnelStatus = makeTunnelStatus(true, newTunnel.wssUrl, null, 0);
558
- options.onWssUrl?.(newTunnel.wssUrl);
559
- if (newTunnel.childPid !== void 0) options.onTunnelChildPid?.(newTunnel.childPid);
560
- printAttachBanner({
561
- wssUrl: newTunnel.wssUrl,
562
- totpEnabled
563
- }).then(() => {
564
- logInfo("tunnel.up", {
565
- totpEnabled,
566
- reissued: true
567
- });
568
- });
569
- },
570
- onPermanentDrop: (droppedAt) => {
571
- tunnelStatus = makeTunnelStatus(false, null, droppedAt, 3);
572
- logError("tunnel.down", { msg: `tunnel permanently dropped (${droppedAt}). Restart: npx @ait-co/devtools devtools-mcp` });
573
- options.onTunnelDown?.();
574
- }
575
- });
576
- return printAttachBanner({
577
- wssUrl: t.wssUrl,
578
- totpEnabled
579
- });
580
- }, (err) => {
581
- 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.` });
582
- });
583
- const connection = createRelayConnection(relay.baseUrl);
584
- return {
585
- connection,
586
- relayOrigin: "intoss-webview",
587
- relayHttpUrl: relay.baseUrl,
588
- getTunnelStatus: () => tunnelStatus,
589
- stop() {
590
- tunnelProbe?.stop();
591
- tunnel?.stop();
592
- connection.close();
593
- relay.close();
594
- }
595
- };
596
- }
597
- //#endregion
598
- export { bootRelayFamily, buildRelayVerifyAuth, startAttachWatcher };
599
-
600
- //# sourceMappingURL=debug-server-CfQNxxGW.js.map