@ait-co/devtools 0.1.122 → 0.1.124

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 (78) hide show
  1. package/dist/{attach-orchestrator-EnZEr4Uu.js → attach-orchestrator-CpE0dMew.js} +2 -2
  2. package/dist/{attach-orchestrator-EnZEr4Uu.js.map → attach-orchestrator-CpE0dMew.js.map} +1 -1
  3. package/dist/{cell-3bIG9538.js → cell-Ba9lCI3B.js} +2 -2
  4. package/dist/{cell-3bIG9538.js.map → cell-Ba9lCI3B.js.map} +1 -1
  5. package/dist/{cell-Cad-vi7X.js → cell-CX92lDI0.js} +3 -3
  6. package/dist/{cell-CgKUkDxY.js.map → cell-CX92lDI0.js.map} +1 -1
  7. package/dist/{cell-CgKUkDxY.js → cell-D0dJSGHi.js} +2 -2
  8. package/dist/{cell-Cad-vi7X.js.map → cell-D0dJSGHi.js.map} +1 -1
  9. package/dist/{debug-server-D79E3hTa.js → debug-server-B3W_0Ejf.js} +6 -226
  10. package/dist/debug-server-B3W_0Ejf.js.map +1 -0
  11. package/dist/{debug-server-CF-1s3ks.js → debug-server-BDfeWytB.js} +6 -944
  12. package/dist/debug-server-BDfeWytB.js.map +1 -0
  13. package/dist/{debug-server-D_28CxC1.js → debug-server-BzzNkETg.js} +6 -6
  14. package/dist/{debug-server-D_28CxC1.js.map → debug-server-BzzNkETg.js.map} +1 -1
  15. package/dist/mcp/cli.js +8214 -4
  16. package/dist/mcp/cli.js.map +1 -1
  17. package/dist/mcp/server.js +1 -1
  18. package/dist/panel/index.js +1 -1
  19. package/dist/{qr-http-server-BsWlOA85.js → qr-http-server-BH0UFeUn.js} +11 -1
  20. package/dist/{qr-http-server-gHBd-z7T.js.map → qr-http-server-BH0UFeUn.js.map} +1 -1
  21. package/dist/{qr-http-server-UBV2qUEd.js → qr-http-server-Bo8LMOJQ.js} +11 -1
  22. package/dist/{qr-http-server-BJjw0hDr.js.map → qr-http-server-Bo8LMOJQ.js.map} +1 -1
  23. package/dist/{qr-http-server-CrVKno9V.cjs → qr-http-server-Bt_RAxFn.cjs} +11 -1
  24. package/dist/{qr-http-server-BpTvfeku.cjs.map → qr-http-server-Bt_RAxFn.cjs.map} +1 -1
  25. package/dist/{qr-http-server-CKbtqvKy.js → qr-http-server-CrGohOwC.js} +11 -1
  26. package/dist/{qr-http-server-CKbtqvKy.js.map → qr-http-server-CrGohOwC.js.map} +1 -1
  27. package/dist/{qr-http-server-BpTvfeku.cjs → qr-http-server-DM2R25jP.cjs} +11 -1
  28. package/dist/{qr-http-server-CrVKno9V.cjs.map → qr-http-server-DM2R25jP.cjs.map} +1 -1
  29. package/dist/{qr-http-server-BJjw0hDr.js → qr-http-server-s1iW_5VQ.js} +11 -1
  30. package/dist/{qr-http-server-BsWlOA85.js.map → qr-http-server-s1iW_5VQ.js.map} +1 -1
  31. package/dist/{relay-factory-BBnZM-an.js → relay-factory-CqY264j0.js} +22 -5
  32. package/dist/relay-factory-CqY264j0.js.map +1 -0
  33. package/dist/{relay-secret-store-BJjoxes1.js → relay-secret-store-CFc9n0OA.js} +1 -1
  34. package/dist/{relay-secret-store-BJjoxes1.js.map → relay-secret-store-CFc9n0OA.js.map} +1 -1
  35. package/dist/{relay-secret-store-CtK1eqoV.js → relay-secret-store-DhzAnnj-.js} +3 -3
  36. package/dist/{relay-secret-store-CtK1eqoV.js.map → relay-secret-store-DhzAnnj-.js.map} +1 -1
  37. package/dist/{relay-url-store-D3H067u0.js → relay-url-store-CwKT7i04.js} +2 -2
  38. package/dist/{relay-url-store-D3H067u0.js.map → relay-url-store-CwKT7i04.js.map} +1 -1
  39. package/dist/{relay-url-store-uyVNe-Az.js → relay-url-store-DGQ-HPQC.js} +2 -2
  40. package/dist/{relay-url-store-uyVNe-Az.js.map → relay-url-store-DGQ-HPQC.js.map} +1 -1
  41. package/dist/test-runner/bin.d.ts +2 -0
  42. package/dist/{cli-CaJ-vnBj.js → test-runner/bin.js} +49 -9
  43. package/dist/test-runner/bin.js.map +1 -0
  44. package/dist/test-runner/config.js +1 -1
  45. package/dist/test-runner/relay-factory.js +21 -4
  46. package/dist/test-runner/relay-factory.js.map +1 -1
  47. package/dist/totp-D1pulXLa.js +3 -0
  48. package/dist/{totp-0bvlo9Yb.js → totp-WY6l0ysP.js} +1 -1
  49. package/dist/{totp-0bvlo9Yb.js.map → totp-WY6l0ysP.js.map} +1 -1
  50. package/dist/{tunnel-B7U5k1xa.js → tunnel-BFwOSwT4.js} +2 -2
  51. package/dist/{tunnel-B7U5k1xa.js.map → tunnel-BFwOSwT4.js.map} +1 -1
  52. package/dist/{tunnel-C-cgMnyY.cjs → tunnel-DyEV6WwL.cjs} +2 -2
  53. package/dist/{tunnel-C-cgMnyY.cjs.map → tunnel-DyEV6WwL.cjs.map} +1 -1
  54. package/dist/unplugin/index.cjs +1 -1
  55. package/dist/unplugin/index.js +1 -1
  56. package/dist/unplugin/tunnel.cjs +1 -1
  57. package/dist/unplugin/tunnel.js +1 -1
  58. package/package.json +2 -2
  59. package/dist/attach-orchestrator-BYMCSPjf.js +0 -2797
  60. package/dist/attach-orchestrator-BYMCSPjf.js.map +0 -1
  61. package/dist/attach-orchestrator-LOjvsE9R.js +0 -3
  62. package/dist/cell-Bfbke0cT.js +0 -3
  63. package/dist/cell-SKNUQTr8.js +0 -85
  64. package/dist/cell-SKNUQTr8.js.map +0 -1
  65. package/dist/cli-CaJ-vnBj.js.map +0 -1
  66. package/dist/debug-server-CF-1s3ks.js.map +0 -1
  67. package/dist/debug-server-D79E3hTa.js.map +0 -1
  68. package/dist/debug-server-IVaJ9vHH.js +0 -4278
  69. package/dist/debug-server-IVaJ9vHH.js.map +0 -1
  70. package/dist/qr-http-server-BpcDt-cp.js +0 -3
  71. package/dist/qr-http-server-UBV2qUEd.js.map +0 -1
  72. package/dist/qr-http-server-gHBd-z7T.js +0 -1506
  73. package/dist/relay-factory-BBnZM-an.js.map +0 -1
  74. package/dist/relay-secret-store-BIM_0y18.js +0 -3
  75. package/dist/test-runner/cli.d.ts +0 -547
  76. package/dist/test-runner/cli.d.ts.map +0 -1
  77. package/dist/test-runner/cli.js +0 -3
  78. package/dist/totp-zGIlDsZS.js +0 -3
@@ -60,13 +60,24 @@ function createRelayConnectionFactory(opts) {
60
60
  let qrServer;
61
61
  return {
62
62
  async open() {
63
- const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("./attach-orchestrator-EnZEr4Uu.js");
64
- const { injectDebugIndicator, injectGlobals } = await import("./cell-Cad-vi7X.js");
65
- const { loadRelaySecretReadOnly } = await import("./relay-secret-store-BJjoxes1.js");
66
- const { bootRelayFamily, buildRelayVerifyAuth } = await import("./debug-server-D_28CxC1.js");
63
+ const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-CpE0dMew.js");
64
+ const { injectDebugIndicator, injectGlobals } = await import("../cell-CX92lDI0.js");
65
+ const { loadRelaySecretReadOnly } = await import("../relay-secret-store-CFc9n0OA.js");
66
+ const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-BzzNkETg.js");
67
67
  await loadRelaySecretReadOnly({ projectRoot });
68
- const booted = await bootRelayFamily({ verifyAuth: buildRelayVerifyAuth() });
68
+ let resolveTunnelUp;
69
+ const tunnelReady = new Promise((resolve) => {
70
+ resolveTunnelUp = resolve;
71
+ });
72
+ const booted = await bootRelayFamily({
73
+ verifyAuth: buildRelayVerifyAuth(),
74
+ onWssUrl: () => {
75
+ resolveTunnelUp();
76
+ qrServer?.notifyStateChange();
77
+ }
78
+ });
69
79
  family = booted;
80
+ if (booted.getTunnelStatus?.().up) resolveTunnelUp();
70
81
  let lastAttachParts;
71
82
  const attachDeps = {
72
83
  getTunnelStatus: booted.getTunnelStatus ?? (() => ({
@@ -79,7 +90,7 @@ function createRelayConnectionFactory(opts) {
79
90
  canOpenBrowser: () => !headless
80
91
  };
81
92
  try {
82
- const { startQrHttpServer } = await import("./qr-http-server-BJjw0hDr.js");
93
+ const { startQrHttpServer } = await import("../qr-http-server-s1iW_5VQ.js");
83
94
  const getDashboardState = () => ({
84
95
  tunnel: attachDeps.getTunnelStatus(),
85
96
  pages: null,
@@ -94,16 +105,22 @@ function createRelayConnectionFactory(opts) {
94
105
  };
95
106
  process.stderr.write(`devtools-test: QR dashboard: http://127.0.0.1:${qrServer.port}/\n`);
96
107
  } catch {}
108
+ const TUNNEL_BOOT_TIMEOUT_MS = 15e3;
109
+ await Promise.race([tunnelReady, new Promise((resolve) => setTimeout(resolve, TUNNEL_BOOT_TIMEOUT_MS))]);
97
110
  const prep = await prepareAttach(attachDeps, "relay-dev", { scheme_url: opts.schemeUrl }, booted.connection);
98
111
  if (!prep.ok) {
99
112
  booted.stop();
100
113
  family = void 0;
114
+ await qrServer?.close();
115
+ qrServer = void 0;
101
116
  throw new Error("createRelayConnectionFactory: attach preparation failed — check the scheme_url and that the relay tunnel is up");
102
117
  }
103
118
  const waitResult = await renderAndMaybeWait(attachDeps, prep, true, timeoutMs, booted.connection);
104
119
  if (waitResult.isError) {
105
120
  booted.stop();
106
121
  family = void 0;
122
+ await qrServer?.close();
123
+ qrServer = void 0;
107
124
  const timeoutSec = Math.round(timeoutMs / 1e3);
108
125
  throw new Error(`createRelayConnectionFactory: attach timed out after ${timeoutSec}s — phone did not scan the QR within the timeout`);
109
126
  }
@@ -1067,11 +1084,34 @@ async function main(argv = process.argv.slice(2)) {
1067
1084
  process.exitCode = exitCode;
1068
1085
  }
1069
1086
  }
1070
- if (import.meta.url === new URL(process.argv[1], "file://").href) main().catch((e) => {
1087
+ //#endregion
1088
+ //#region src/test-runner/bin.ts
1089
+ /**
1090
+ * `devtools-test` bin entry point — export-free by design.
1091
+ *
1092
+ * This file is intentionally export-free so that Rolldown (tsdown) emits the
1093
+ * main() call directly into the output bundle rather than hoisting the module
1094
+ * body into a shared chunk and replacing this file with a re-export wrapper.
1095
+ *
1096
+ * Background (#711): cli.ts exports `main`, `runWithConnection`, and
1097
+ * `shouldSuppressQr` for use by tests and relay-factory. When cli.ts was also
1098
+ * the bin entry, Rolldown split the module body into a shared chunk
1099
+ * (`cli-<hash>.js`) and reduced the bin output to a 3-line re-export wrapper.
1100
+ * The self-invoke guard (`import.meta.url === process.argv[1]`) evaluated
1101
+ * inside the shared chunk where `import.meta.url` is the chunk path, not the
1102
+ * bin path — a structural permanent mismatch — so main() was never called and
1103
+ * every `devtools-test` / `pnpm test:env3` invocation exited 0 as a silent
1104
+ * no-op.
1105
+ *
1106
+ * NOTE: no shebang in this source file — the tsdown entry's `banner` option
1107
+ * injects `#!/usr/bin/env node` into the compiled output (same pattern as
1108
+ * other Node bin entries in tsdown.config.ts).
1109
+ */
1110
+ main().catch((e) => {
1071
1111
  process.stderr.write(`devtools-test: unexpected error: ${e instanceof Error ? e.message : String(e)}\n`);
1072
1112
  process.exitCode = 1;
1073
1113
  });
1074
1114
  //#endregion
1075
- export { runWithConnection as n, shouldSuppressQr as r, main as t };
1115
+ export {};
1076
1116
 
1077
- //# sourceMappingURL=cli-CaJ-vnBj.js.map
1117
+ //# sourceMappingURL=bin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"bin.js","names":["path"],"sources":["../../src/test-runner/discover.ts","../../src/test-runner/relay-factory.ts","../../src/test-runner/bundle.ts","../../src/test-runner/capture.ts","../../src/test-runner/rpc.ts","../../src/test-runner/relay-worker.ts","../../src/test-runner/report.ts","../../src/test-runner/cli.ts","../../src/test-runner/bin.ts"],"sourcesContent":["/**\n * Test-file discovery shared by the `devtools-test` CLI and the `run_tests`\n * MCP tool, so both expand glob patterns with identical semantics.\n *\n * Uses Node's built-in `fs/promises` `glob` (Node 22+) — no extra dependency,\n * which keeps the MCP daemon install graph lean (a plain glob lib would land in\n * the `npx … devtools-mcp` path for no benefit).\n *\n * Pure Node IO only (`node:fs/promises` + `node:path`) — react-free, so it is\n * safe to import from the MCP daemon graph.\n */\n\nimport { glob } from 'node:fs/promises';\nimport { isAbsolute, resolve } from 'node:path';\n\n/**\n * Expands `patterns` (globs or plain paths) into a sorted, de-duplicated list of\n * ABSOLUTE test file paths, resolved relative to `cwd`.\n *\n * A plain (non-glob) path passes through when it matches a real file; a glob\n * expands against `cwd`. Absolute matches are kept as-is; relative matches are\n * resolved against `cwd`. `bundleTestFile` requires an absolute path, so the\n * absolute output feeds it directly.\n *\n * @param patterns Glob patterns or file paths (e.g. `['src/**\\/*.ait.test.ts']`).\n * @param cwd Base directory for relative patterns/results.\n * @returns Sorted, de-duplicated absolute file paths. Empty when nothing matches.\n */\nexport async function discoverTestFiles(patterns: string[], cwd: string): Promise<string[]> {\n const out = new Set<string>();\n for await (const match of glob(patterns, { cwd })) {\n out.add(isAbsolute(match) ? match : resolve(cwd, match));\n }\n return [...out].sort();\n}\n","/**\n * Relay connection factory for the Vitest custom pool (devtools#696).\n *\n * The Vitest pool (`pool.ts`) takes a {@link RelayConnectionFactory} that knows\n * how to `open()` a live CDP relay connection (boot relay → QR → phone scan →\n * cell inject → enableDomains) and `close()` it. Before this module that exact\n * assembly lived only inside the `devtools-test` CLI's `main()`; the standalone\n * CLI and the Vitest pool would otherwise each hand-roll it and drift.\n *\n * This is the single source of that assembly. `cli.ts` is refactored to call\n * `createRelayConnectionFactory(...).open()`, and `definePhoneVitestConfig`\n * (config.ts) exposes it so a downstream `vitest.config.ts` can wire the pool:\n *\n * import { createRelayConnectionFactory } from '@ait-co/devtools/test-runner';\n * const connection = createRelayConnectionFactory({\n * schemeUrl,\n * cell,\n * // REQUIRED — own the stdout decision (the chunks carry the relay wss +\n * // TOTP code). Suppress on non-interactive stdout; print otherwise.\n * onQrContent: (chunks) => {\n * if (!process.stdout.isTTY) return;\n * for (const c of chunks) process.stdout.write(`${c}\\n`);\n * },\n * });\n * export default defineConfig({ test: definePhoneVitestConfig({ connection }) });\n *\n * The heavy boot graph (chii relay, cloudflared, ws, debug-server) is pulled via\n * DYNAMIC import inside `open()` — so merely importing this module (or the\n * `@ait-co/devtools/test-runner` barrel) does NOT statically drag that graph in.\n *\n * SECRET-HANDLING: relay wss URLs, scheme URLs, and the TOTP secret/code are\n * never logged. `open()` reads the project-local `.ait_relay` secret read-only\n * (never mints) and the minted TOTP code rides only inside the QR `at=` param.\n *\n * Node-only. react-free (CdpConnection + lazily-imported MCP boot helpers only).\n */\n\nimport type { AttachDeps, AttachUrlParts } from '../mcp/attach-orchestrator.js';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport type { QrHttpServer } from '../mcp/qr-http-server.js';\nimport type { RelayConnectionFactory } from './pool.js';\n\n// NOTE: every value import below is a DYNAMIC import inside `open()`. This module\n// keeps ONLY type-level static imports so that re-exporting it from the\n// `@ait-co/devtools/test-runner` barrel (config.ts) does NOT statically drag the\n// heavy MCP graph (cell.ts → attach-orchestrator.ts → tools.ts → server-lock,\n// plus chii/cloudflared via debug-server) onto that Node-config entry.\n\n/** Options for {@link createRelayConnectionFactory}. */\nexport interface RelayConnectionFactoryOptions {\n /**\n * intoss-private:// scheme URL from `ait deploy --scheme-only` (env3). The\n * phone cold-loads the candidate bundle this URL points at. SECRET-HANDLING:\n * never logged.\n */\n schemeUrl: string;\n /**\n * Project root for the `.ait_relay` secret lookup (read-only). Defaults to\n * `process.cwd()`.\n */\n projectRoot?: string;\n /**\n * Attach wait timeout in ms — how long `open()` waits for the phone to scan\n * the QR and attach. Defaults to 600 000 (10 min) to give time for a manual\n * scan. (The per-file evaluate timeout is separate, passed via the pool's\n * `run` options.)\n */\n timeoutMs?: number;\n /** Disable browser auto-open of the QR dashboard (text QR only). */\n headless?: boolean;\n /**\n * Cell axes injected as `__AIT_CELL__` before the first test bundle runs, so\n * sdk-example's capture picks up the correct sdkLine/platform. Optional — when\n * omitted no cell is injected. The values are not secrets.\n */\n cell?: { sdkLine: string; platform: string };\n /**\n * Receives the QR/attach render content (text chunks) from\n * `renderAndMaybeWait`, so the caller decides whether to print them — e.g.\n * suppress on non-interactive stdout.\n *\n * REQUIRED (not optional) on purpose: these chunks contain the QR payload\n * (which encodes the relay wss + TOTP `at=` code) and the attach JSON block.\n * Making the hook mandatory means the factory never falls back to printing\n * them itself — a downstream `vitest.config.ts` consumer that wires this via\n * the `@ait-co/devtools/test-runner` barrel is forced to make an explicit\n * stdout decision rather than silently leaking the secret-bearing block.\n *\n * SECRET-HANDLING: when a non-interactive caller is detected the hook MUST\n * suppress the WHOLE chunk (not just `attachUrl`), since `relayUrl` rides in\n * the same block.\n */\n onQrContent: (textChunks: string[]) => void;\n}\n\n/**\n * Builds a {@link RelayConnectionFactory} that opens a standalone env3 relay\n * connection.\n *\n * `open()` performs the full attach lifecycle and BLOCKS for tens of seconds (up\n * to `timeoutMs`) while a human scans the rendered QR with their phone — there\n * is no way around the manual scan for env3. It resolves with the live\n * `CdpConnection` once a matching page attaches; `close()` tears the relay\n * family down.\n *\n * The factory holds the booted relay family in a closure so `close()` can stop\n * it. A second `open()` on the same factory boots a fresh family (the previous\n * one should have been `close()`d first).\n */\nexport function createRelayConnectionFactory(\n opts: RelayConnectionFactoryOptions,\n): RelayConnectionFactory {\n const projectRoot = opts.projectRoot ?? process.cwd();\n const timeoutMs = opts.timeoutMs ?? 600_000;\n const headless = opts.headless === true;\n\n // Captured so close() can stop the family that open() booted.\n let family: { connection: CdpConnection; stop(): void } | undefined;\n // QR HTTP server — started during open() when not headless, closed in close().\n let qrServer: QrHttpServer | undefined;\n\n return {\n async open(): Promise<CdpConnection> {\n // Dynamic imports: keep the chii/cloudflared/debug-server graph OFF the\n // static import graph of this module (and the test-runner config barrel).\n const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import(\n '../mcp/attach-orchestrator.js'\n );\n const { injectDebugIndicator, injectGlobals } = await import('./cell.js');\n const { loadRelaySecretReadOnly } = await import('../mcp/relay-secret-store.js');\n const { bootRelayFamily, buildRelayVerifyAuth } = await import('../mcp/debug-server.js');\n\n // Load the project-local .ait_relay secret into AIT_DEBUG_TOTP_SECRET\n // BEFORE booting the relay so assertRelayAuthConfigured()/buildRelayVerifyAuth()\n // at the boot site see it. Read-only — never mints. SECRET-HANDLING: the\n // value is never logged here.\n await loadRelaySecretReadOnly({ projectRoot });\n\n // PRIMARY FIX (devtools#714): bootRelayFamily starts the cloudflared tunnel\n // as a background promise and returns immediately with tunnel.up === false.\n // We must NOT call prepareAttach until the tunnel is up — it fails fast when\n // tunnel.up is false (attach-orchestrator.ts tunnel-down guard).\n //\n // Wire onWssUrl (mirroring the MCP daemon path in debug-server.ts) to:\n // 1. resolve a caller-held tunnel-ready promise so open() can await it, and\n // 2. re-push dashboard SSE on late tunnel-up events (notifyStateChange).\n //\n // SECRET-HANDLING: onWssUrl receives the relay wss URL — never log it.\n let resolveTunnelUp!: () => void;\n const tunnelReady = new Promise<void>((resolve) => {\n resolveTunnelUp = resolve;\n });\n\n const booted = await bootRelayFamily({\n verifyAuth: buildRelayVerifyAuth(),\n onWssUrl: () => {\n // Resolve the tunnel-ready gate so the prepareAttach call below is\n // unblocked. qrServer may not be set yet at call time (it's started\n // after bootRelayFamily), so we use optional chaining — if the tunnel\n // happens to come up after qrServer is started, notifyStateChange pushes\n // the freshly minted attachUrl to any waiting dashboard SSE clients.\n // SECRET-HANDLING: wssUrl is NOT forwarded here — the value travels only\n // inside the closure via getTunnelStatus().wssUrl (used by mintAttachUrl).\n resolveTunnelUp();\n qrServer?.notifyStateChange();\n },\n });\n family = booted;\n\n // If the tunnel is already up (extremely fast boot or test double), resolve\n // immediately so we don't stall on a promise that will never fire.\n if (booted.getTunnelStatus?.().up) {\n resolveTunnelUp();\n }\n\n // Track the last-captured attach parts so getDashboardState can mint a\n // fresh attach URL on every dashboard request/SSE push (fresh TOTP at=).\n // SECRET-HANDLING: parts contain the relay wss + scheme URL — never logged.\n let lastAttachParts: AttachUrlParts | undefined;\n\n // Assemble AttachDeps. We set qrHttpServer and onAttachUrlBuilt below\n // (before prepareAttach) after optionally starting the web-QR server.\n const attachDeps: AttachDeps = {\n getTunnelStatus: booted.getTunnelStatus ?? (() => ({ up: false, wssUrl: null })),\n getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,\n qrHttpServer: undefined, // filled in below if web-QR server started\n onAttachUrlBuilt: undefined, // filled in below\n canOpenBrowser: () => !headless,\n };\n\n // Web-QR server: reuse the same loopback HTTP dashboard that the MCP\n // start_attach path uses (src/mcp/qr-http-server.ts). This makes the QR\n // scannable even when stdout is non-interactive (Claude Code `!` / CI),\n // because the browser is opened by URL — not via captured stdout.\n //\n // Headless decision: we start the server even in --headless mode so the\n // printed stderr URL can be opened manually. The existing\n // canOpenBrowser: () => !headless gate inside renderAndMaybeWait prevents\n // the auto-open; headless users see only the stderr URL.\n //\n // On failure we fall back gracefully to the text-QR path — do NOT crash.\n // SECRET-HANDLING: only http://127.0.0.1:<port>/ (no secrets) goes to stderr.\n try {\n const { startQrHttpServer } = await import('../mcp/qr-http-server.js');\n\n const getDashboardState = () => ({\n tunnel: attachDeps.getTunnelStatus(),\n pages: null, // CLI/pool: no page-list introspection needed\n attachUrl: lastAttachParts ? mintAttachUrl(attachDeps, lastAttachParts) : null,\n mode: 'relay-dev' as const,\n });\n\n qrServer = await startQrHttpServer(getDashboardState);\n\n // Wire the QR server into attachDeps BEFORE prepareAttach is called so\n // renderAndMaybeWait sees it and takes Path 2/3 (web-QR) instead of Path 4.\n attachDeps.qrHttpServer = qrServer;\n\n // Capture attach parts via onAttachUrlBuilt so getDashboardState can\n // mint a fresh URL (fresh TOTP at= code) on every dashboard render.\n attachDeps.onAttachUrlBuilt = (parts: AttachUrlParts) => {\n lastAttachParts = parts;\n qrServer?.notifyStateChange();\n };\n\n // Print the loopback dashboard URL to stderr — it carries no secrets\n // (TOTP codes and relay wss live only in the in-memory HTTP response).\n process.stderr.write(`devtools-test: QR dashboard: http://127.0.0.1:${qrServer.port}/\\n`);\n } catch {\n // startQrHttpServer failed (e.g. port conflict, import error). Fall back\n // to the existing text-QR path by leaving qrHttpServer: undefined.\n // qrServer remains undefined; close() handles that with optional chaining.\n }\n\n // PRIMARY FIX (devtools#714): await tunnel readiness before calling\n // prepareAttach. Race: a 15 s timeout is generous — cloudflared typically\n // comes up in < 5 s. On timeout we still call prepareAttach; it will hit\n // the tunnel-down guard and throw a secret-free error (same as before,\n // but now with a clear diagnostic instead of a silent WAITING freeze).\n //\n // Implementation: Promise.race against a 15 000 ms timeout signal. We do\n // NOT use a timer-based early-exit because the existing code already\n // surface-fails on tunnel-down inside prepareAttach with a secret-free\n // message. The timeout here is purely a \"give the tunnel a fair chance\"\n // gate — not a correctness boundary.\n const TUNNEL_BOOT_TIMEOUT_MS = 15_000;\n await Promise.race([\n tunnelReady,\n new Promise<void>((resolve) => setTimeout(resolve, TUNNEL_BOOT_TIMEOUT_MS)),\n ]);\n\n const prep = await prepareAttach(\n attachDeps,\n 'relay-dev',\n { scheme_url: opts.schemeUrl },\n booted.connection,\n );\n if (!prep.ok) {\n booted.stop();\n family = undefined;\n // SECONDARY FIX (devtools#714): close the QR server on the failure path\n // so the loopback port listener does not leak. The normal-exit path is\n // handled by close(); this mirrors that cleanup for the error path.\n await qrServer?.close();\n qrServer = undefined;\n // SECRET-HANDLING: do NOT surface `prep.error.content` in the thrown\n // message. Some prep error paths build their text from attach\n // components, and the CLI catch writes `e.message` to stderr — embedding\n // that text risks leaking the scheme/relay wss URL. The detailed\n // diagnostic is the daemon/dashboard's job; the factory throws a\n // secret-free message only.\n throw new Error(\n 'createRelayConnectionFactory: attach preparation failed — check the scheme_url and that the relay tunnel is up',\n );\n }\n\n // Render the QR + wait for the phone to attach. SECRET-HANDLING: this\n // function never logs scheme/wss/TOTP values.\n const waitResult = await renderAndMaybeWait(\n attachDeps,\n prep,\n true,\n timeoutMs,\n booted.connection,\n );\n if (waitResult.isError) {\n booted.stop();\n family = undefined;\n // SECONDARY FIX (devtools#714): close the QR server on the timeout path\n // (mirrors the prep.ok failure path above).\n await qrServer?.close();\n qrServer = undefined;\n // SECRET-HANDLING (BLOCKER fix): `waitResult.content` is the timeout\n // result built from `buildTimeoutError(baseText, ...)`, and `baseText`\n // is `JSON.stringify({ attachUrl, relayUrl, ... })` — `attachUrl` carries\n // the TOTP `at=` code and `relayUrl` is the relay `wss://` URL. The CLI\n // catch writes `e.message` to stderr, so extracting that text here would\n // leak the relay wss + TOTP code on every timeout. Throw a secret-free\n // message with only the timeout duration.\n const timeoutSec = Math.round(timeoutMs / 1000);\n throw new Error(\n `createRelayConnectionFactory: attach timed out after ${timeoutSec}s — phone did not scan the QR within the timeout`,\n );\n }\n\n // Surface the QR/attach render content to the caller, which owns the\n // stdout decision (suppress on non-interactive stdout). There is no\n // fallback that prints here itself: `onQrContent` is a required option so\n // the secret-bearing block (attachUrl TOTP + relayUrl wss) can never be\n // emitted without an explicit caller decision. SECRET-HANDLING.\n const qrChunks = waitResult.content\n .filter((c): c is { type: 'text'; text: string } => c.type === 'text')\n .map((c) => c.text);\n opts.onQrContent(qrChunks);\n\n // Debugger attached — show the on-phone \"Debugger Connected\" badge.\n await injectDebugIndicator(booted.connection);\n\n // Inject the cell globals before any test bundle runs (session-global).\n if (opts.cell !== undefined) {\n await injectGlobals(booted.connection, { __AIT_CELL__: opts.cell });\n }\n\n // Open the CDP client websocket + enable domains so the first run's\n // Runtime.evaluate and console stream are live. enableDomains is\n // idempotent; runTestFilesOverRelay also calls it defensively.\n await booted.connection.enableDomains();\n\n return booted.connection;\n },\n\n async close(_connection: CdpConnection): Promise<void> {\n // family.stop() is synchronous best-effort: closes the CDP connection and\n // shuts down the relay + cloudflared child.\n family?.stop();\n family = undefined;\n // Close the web-QR HTTP server if one was started during open().\n // close() is idempotent via optional chaining + reassignment to undefined.\n await qrServer?.close();\n qrServer = undefined;\n },\n };\n}\n","/**\n * esbuild-based bundler for user test files.\n *\n * Bundles a single test file into a self-contained IIFE string that can be\n * injected into a WebView via `Runtime.evaluate`. The bundle includes the\n * test runtime (`runtime.ts`), which provides `describe/it/test/expect` and\n * the `runTestModule(factory)` entry point.\n *\n * ## How the wiring works\n *\n * The bundle exposes two exports on `globalThis.__testBundle`:\n * - `runTestModule` — the runtime's entry function.\n * - `__userFactory` — an async function whose body is the user's top-level\n * test registration code (describe/it/test calls).\n *\n * The Node-side RPC (`rpc.ts`) calls:\n * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`\n *\n * `runTestModule` then installs `describe/it/test/expect` as globals, invokes\n * the factory (which registers all tests), runs them, and returns a `RunReport`.\n *\n * ## Why a factory wrapper is needed\n *\n * Naively adding the runtime to `entryPoints` and bundling the user file would\n * fail for two reasons:\n * 1. `describe/it/test/expect` from the runtime are module-local in the IIFE\n * scope. The user's top-level `describe(...)` calls expect them as globals —\n * they are not globals until `runTestModule` installs them.\n * 2. Even with globals pre-installed, the user file runs at IIFE-evaluation\n * time, before the RPC layer calls `runTestModule` to reset state and start\n * the test clock.\n *\n * The factory approach solves both: the user's registration code is deferred\n * into a function that `runTestModule` calls AFTER installing the globals.\n *\n * ## Factory extraction algorithm\n *\n * The `userFactoryPlugin` reads the user file and splits lines into:\n * - **top-level**: `import …` and re-export lines — kept at module scope\n * (the only valid position for static `import` in ESM).\n * - **body**: all other statements — moved into the body of the exported\n * `__userFactory` async function.\n *\n * esbuild processes the re-generated module, following each static import\n * through the normal dependency graph (including the SDK-redirect plugin).\n *\n * ## SDK redirect\n *\n * Imports of `@apps-in-toss/web-framework` (and sub-paths) are intercepted via\n * the `sdkRedirectPlugin` and replaced with a virtual `window.__sdk` proxy that\n * `src/in-app/auto.ts` installs at runtime. This works for both 2.x and 3.x SDK.\n *\n * SECRET-HANDLING: the returned bundle code is caller-managed; never log it.\n */\n\nimport { accessSync } from 'node:fs';\nimport * as fs from 'node:fs/promises';\nimport * as path from 'node:path';\nimport { fileURLToPath } from 'node:url';\n// esbuild is imported for TYPES only at module scope; the runtime module is\n// loaded lazily inside `bundleTestFile` via dynamic import. esbuild runs a\n// startup invariant check (`TextEncoder().encode('') instanceof Uint8Array`)\n// that fails in a jsdom realm — a static import would break every MCP/test\n// module that merely *imports* this file's transitive graph (e.g. debug-server →\n// run_tests). Lazy load keeps esbuild off the import graph until a bundle is\n// actually built, and mirrors the cloudflared/chii dynamic-import precedent.\nimport type * as esbuild from 'esbuild';\n\n/** Options accepted by `bundleTestFile`. */\nexport interface BundleOptions {\n /**\n * Additional esbuild `external` patterns. The SDK package\n * (`@apps-in-toss/web-framework` and `@apps-in-toss/web-framework/*`) is\n * always handled by the SDK redirect plugin — callers may add more patterns\n * to be left as globals.\n */\n extraExternals?: string[];\n /**\n * Global name for the IIFE output object. Defaults to `__testBundle`.\n * The runtime entry uses this to call `__testBundle.runTestModule(__userFactory)`.\n */\n globalName?: string;\n}\n\n/**\n * The result of bundling a test file.\n * `code` is a self-contained IIFE string ready for `Runtime.evaluate`.\n */\nexport interface BundleResult {\n code: string;\n warnings: string[];\n}\n\n/** The SDK package name that mini-app test code imports from. */\nconst SDK_PACKAGE = '@apps-in-toss/web-framework';\n\n/**\n * Names the runtime installs as globals before invoking the user factory.\n * The `vitest` virtual module re-exports each as a lazy getter that reads from\n * `globalThis` at access time. Keep in sync with the globals installed in\n * `runtime.ts#runTestModule`.\n */\nconst VITEST_GLOBAL_NAMES = [\n 'describe',\n 'it',\n 'test',\n 'expect',\n 'beforeAll',\n 'afterAll',\n 'beforeEach',\n 'afterEach',\n 'vi',\n] as const;\n\n/**\n * Matches the bare SDK package and any sub-path import\n * (`@apps-in-toss/web-framework`, `@apps-in-toss/web-framework/foo`).\n * Built from {@link SDK_PACKAGE} so the package name has a single source.\n */\nconst SDK_IMPORT_FILTER = new RegExp(`^${SDK_PACKAGE.replace(/[.*+?^${}()|[\\]\\\\]/g, '\\\\$&')}`);\n\n/**\n * esbuild plugin that intercepts SDK imports and redirects them to the\n * `window.__sdk` proxy that `src/in-app/auto.ts` installs at runtime.\n *\n * Strategy: for every import of `@apps-in-toss/web-framework` (or sub-paths),\n * esbuild resolves it to a virtual module that re-exports all named exports\n * via `window.__sdk[name]`. This avoids bundling the real SDK (which may not\n * be available in the test environment) while still making named imports work.\n *\n * If `window.__sdk` is absent (non-dog-food build), every access throws a\n * descriptive error rather than returning `undefined` silently.\n */\nfunction sdkRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'sdk-redirect',\n setup(build) {\n // Match the bare package and any sub-path imports\n build.onResolve({ filter: SDK_IMPORT_FILTER }, (args) => ({\n path: args.path,\n namespace: 'sdk-redirect',\n }));\n\n build.onLoad({ filter: /.*/, namespace: 'sdk-redirect' }, () => ({\n // Generate a virtual CommonJS-style module so that esbuild does NOT perform\n // strict named-export matching. When `format:'iife'` bundles a CJS module,\n // it wraps it with its own __toCommonJS helper and satisfies named imports\n // via property access on the module.exports object — which is our Proxy.\n // This means `import { getPlatformOS } from '...'` becomes\n // `__proxy.getPlatformOS` at runtime, which correctly reads from window.__sdk.\n contents: `\nvar __proxy = (typeof window !== 'undefined' && window.__sdk)\n ? window.__sdk\n : new Proxy({}, {\n get: function(_t, p) {\n throw new Error('window.__sdk is not installed — run in a dog-food build. Missing: ' + String(p));\n }\n });\nmodule.exports = __proxy;\n`,\n loader: 'js',\n }));\n },\n };\n}\n\n/**\n * esbuild plugin that intercepts `import … from 'vitest'` and replaces it with\n * a virtual module that delegates every named import to `globalThis` at ACCESS\n * time (not at bundle-evaluation time).\n *\n * The runtime installs `describe/it/test/expect/beforeAll/afterAll/beforeEach/\n * afterEach/vi` as globals inside `runTestModule`, which runs AFTER the bundle\n * IIFE is evaluated. A value-copy redirect (`export var describe =\n * globalThis.describe`) would therefore capture `undefined` at evaluation time\n * and the user's `describe(...)` calls would be no-ops — registering zero tests.\n *\n * The fix defers the lookup to call time using per-name **getter** exports.\n * We emit a CommonJS module that:\n * 1. sets `__esModule = true` so esbuild's `__toESM` interop maps each named\n * import directly to a property access on the module (NOT wrapped under a\n * `default` shim — which is what happens for a bare Proxy whose own-keys\n * are empty, leaving every named import `undefined`);\n * 2. defines each global name as a getter that reads `globalThis[name]` on\n * every access. So `import { describe } from 'vitest'` compiles to\n * `import_vitest.describe`, whose getter returns the real `describe` only\n * when the factory calls it — after `runTestModule` installs the globals.\n *\n * A plain `module.exports = new Proxy(...)` does NOT work here: esbuild routes\n * the virtual module through `__toESM`, which enumerates own-keys (none on an\n * empty Proxy target) and therefore exposes zero named exports. Explicit getter\n * properties give `__toESM` real keys to map while keeping access lazy.\n */\nfunction vitestRedirectPlugin(): esbuild.Plugin {\n return {\n name: 'vitest-redirect',\n setup(build) {\n build.onResolve({ filter: /^vitest$/ }, () => ({\n path: 'vitest',\n namespace: 'vitest-redirect',\n }));\n\n build.onLoad({ filter: /^vitest$/, namespace: 'vitest-redirect' }, () => {\n const getters = VITEST_GLOBAL_NAMES.map(\n (name) =>\n `Object.defineProperty(exports, ${JSON.stringify(name)}, { enumerable: true, get: function() { return globalThis[${JSON.stringify(name)}]; } });`,\n ).join('\\n');\n // __esModule lets esbuild __toESM map named imports to these getters\n // directly. Each getter reads globalThis lazily so the value resolves at\n // call time — after runTestModule installs the globals.\n return {\n contents: `Object.defineProperty(exports, '__esModule', { value: true });\\n${getters}\\n`,\n loader: 'js',\n };\n });\n },\n };\n}\n\n/**\n * esbuild plugin that transforms the user test file into a module that exports\n * an async `__userFactory` function. The factory defers the user's top-level\n * test registration code (describe/it/test calls) so it only runs when\n * `runTestModule(__userFactory)` explicitly invokes it — AFTER the runtime has\n * installed describe/it/test/expect as globals.\n *\n * Algorithm:\n * - Import declarations and re-export statements are kept at module top-level\n * (the only valid ESM position for static `import`). A statement that spans\n * multiple lines — e.g. a named import with one member per line:\n * import {\n * appLogin,\n * getAnonymousKey,\n * } from '@apps-in-toss/web-framework';\n * is tracked as a single block: every line from the opening `import {` /\n * `export {` through the closing `from '…'` (or side-effect `'…'`) line is\n * kept together at top-level. This prevents the member lines and the\n * closing `} from '…'` line from leaking into the factory body, which would\n * leave an unterminated `import {` at module scope (the #678 env3 failure:\n * esbuild threw `Expected \"as\" but found \"{\"` on multi-line SDK imports).\n * - All other lines (describe/it/test calls, local declarations, etc.) are\n * moved into the body of the exported async factory function.\n *\n * This preserves SDK import resolution (the sdk-redirect plugin processes\n * top-level imports normally) while deferring test registration to the factory.\n */\nfunction userFactoryPlugin(absPath: string): esbuild.Plugin {\n const NAMESPACE = 'user-test-factory';\n return {\n name: 'user-test-factory',\n setup(build) {\n // Resolve the virtual \"user-test-factory\" specifier to our namespace.\n build.onResolve({ filter: /^user-test-factory$/ }, () => ({\n path: absPath,\n namespace: NAMESPACE,\n }));\n\n // Load the user file, split imports from body, wrap body in the factory.\n build.onLoad({ filter: /.*/, namespace: NAMESPACE }, async (args) => {\n const source = await fs.readFile(args.path, 'utf8');\n const lines = source.split('\\n');\n\n const topLevelLines: string[] = [];\n const bodyLines: string[] = [];\n\n // Matches `export` value declarations that cannot appear inside a\n // function body. We strip the `export` keyword so they become plain\n // declarations inside the factory.\n const EXPORT_DECLARATION_RE =\n /^(export\\s+)(default\\s+|async\\s+function\\s+|function\\s+|class\\s+|const\\s+|let\\s+|var\\s+)/;\n\n // True when `trimmed` begins a static `import` statement.\n const isImportStart = (trimmed: string): boolean =>\n trimmed.startsWith('import ') ||\n trimmed.startsWith('import{') ||\n trimmed.startsWith(\"import'\") ||\n trimmed.startsWith('import\"');\n\n // A module-scope `import`/`export … from` statement is \"complete\" on a\n // single line when it ends with a quoted module specifier (optionally\n // followed by `;`/whitespace), e.g. `… from '@x';` or side-effect\n // `import './x';`. A line ending in `{` or `,` (the common multi-line\n // named-import shape) is therefore NOT complete and must accumulate\n // further lines until the closing `from '…'` line.\n const endsStatement = (trimmed: string): boolean =>\n /['\"]\\s*;?\\s*$/.test(trimmed.replace(/\\/\\/.*$/, '').trimEnd());\n\n // When set, we are inside an unterminated multi-line import/re-export\n // block: every subsequent line stays at top level until the block ends.\n let inImportBlock = false;\n\n for (const line of lines) {\n const trimmed = line.trimStart();\n const indent = line.slice(0, line.length - trimmed.length);\n\n // Continuation of a multi-line import / re-export block — keep at\n // top level. The block ends on the line that terminates the\n // statement (closing `from '…'` / side-effect `'…'`).\n if (inImportBlock) {\n topLevelLines.push(line);\n if (endsStatement(trimmed)) {\n inImportBlock = false;\n }\n continue;\n }\n\n // Static import declarations must stay at module top level\n // (the ESM spec forbids `import` inside a function body). If the\n // statement does not terminate on this line, open an import block so\n // the member lines and closing `} from '…'` line stay top-level too.\n if (isImportStart(trimmed)) {\n topLevelLines.push(line);\n if (!endsStatement(trimmed)) {\n inImportBlock = true;\n }\n } else if (trimmed.startsWith('export ')) {\n // Determine whether this is a re-export (stays top-level) or a value\n // declaration (goes into the factory, export keyword stripped).\n const m = trimmed.match(EXPORT_DECLARATION_RE);\n if (m) {\n // Value declaration — strip `export ` and move into factory body.\n // e.g. `export function hello()` → `function hello()`\n // `export const x = 1` → `const x = 1`\n bodyLines.push(indent + trimmed.slice('export '.length));\n } else {\n // Re-export or `export type { … }` — stays at top level. A\n // multi-line `export { … } from '…'` opens an import block too.\n topLevelLines.push(line);\n if (/\\bfrom\\b/.test(trimmed) ? !endsStatement(trimmed) : trimmed.endsWith('{')) {\n inImportBlock = true;\n }\n }\n } else {\n bodyLines.push(line);\n }\n }\n\n const factoryContent = [\n ...topLevelLines,\n '',\n '// biome-ignore lint: generated factory wrapper',\n 'export default async function __userFactory(): Promise<void> {',\n ...bodyLines.map((l) => ` ${l}`),\n '}',\n ].join('\\n');\n\n return {\n contents: factoryContent,\n loader: 'ts',\n resolveDir: path.dirname(absPath),\n };\n });\n },\n };\n}\n\n/**\n * Returns the absolute filesystem path to the test-runner runtime module\n * (dist/test-runner/runtime.js — a fully self-contained page-side bundle).\n *\n * Rolldown code-splitting duplicates this bundling logic into shared chunks\n * emitted at ARBITRARY dist depths: the `devtools-test` CLI pulls it from\n * dist/test-runner/bundle.js (dir = dist/test-runner/), while the `devtools-mcp`\n * daemon (dist/mcp/cli.js) pulls it through a ROOT chunk\n * (dist/debug-server-<hash>.js, dir = dist/). A fixed `..`-hop candidate list\n * is therefore wrong from at least one chunk — the live #697 regression.\n *\n * This resolves WITHOUT assuming chunk depth: from `import.meta.url`'s dir it\n * probes the co-located `runtime.js` and the nested `test-runner/runtime.js`,\n * then ascends one directory at a time (bounded) repeating both probes. The\n * nested probe catches dist/test-runner/runtime.js from the dist/ root level no\n * matter which depth the chunk was hoisted to (root, dist/mcp/, or a future\n * relocation). The build always emits dist/test-runner/runtime.js (tsdown entry\n * `'test-runner/runtime'`; guarded by scripts/check-test-runner-dist.sh).\n *\n * An ABSOLUTE path is returned deliberately: esbuild loads it as a literal file\n * read, bypassing Node module resolution entirely, so this works identically in\n * the npx-daemon context (its own dist tree) and the consumer-CLI context\n * (the mini-app's installed @ait-co/devtools dist) — neither needs the package\n * to be node-resolvable from the caller.\n */\nfunction getRuntimePath(): string {\n const startDir = path.dirname(fileURLToPath(import.meta.url));\n // .js first so a SHIPPED chunk never feeds esbuild raw TypeScript; the .ts\n // probe is last, for the source-tree (tsx) dev case where runtime.ts sits\n // beside bundle.ts and no dist exists.\n const RELATIVE_PROBES: string[][] = [\n ['runtime.js'],\n ['test-runner', 'runtime.js'],\n ['runtime.ts'],\n ['test-runner', 'runtime.ts'],\n ];\n // Bounded ascent: a package's dist is only a few levels deep. 12 is far more\n // than any real layout (root chunk → dist is 0 hops; dist/mcp → dist is 1)\n // and terminates well before the filesystem root in every case.\n let dir = startDir;\n for (let i = 0; i < 12; i++) {\n for (const segs of RELATIVE_PROBES) {\n const candidate = path.join(dir, ...segs);\n try {\n accessSync(candidate);\n return candidate;\n } catch {\n // try next probe\n }\n }\n const parent = path.dirname(dir);\n if (parent === dir) break; // reached filesystem root\n dir = parent;\n }\n // Nothing matched — return the co-located path so esbuild emits a clear\n // \"Could not resolve\" against a concrete path rather than failing cryptically.\n return path.join(startDir, 'runtime.js');\n}\n\n/**\n * Bundles `absPath` into a single IIFE string suitable for `Runtime.evaluate`.\n *\n * The IIFE installs `window.__testBundle` (or the custom `globalName`) with:\n * - `runTestModule` — the runtime entry (from `runtime.ts`).\n * - `__userFactory` — an async function wrapping the user's test registration\n * code so it runs AFTER `runTestModule` installs the globals.\n *\n * Callers (rpc.ts) invoke:\n * `globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory)`\n *\n * @param absPath - Absolute path to the user test file.\n * @param opts - Optional bundling overrides.\n */\nexport async function bundleTestFile(absPath: string, opts?: BundleOptions): Promise<BundleResult> {\n const globalName = opts?.globalName ?? '__testBundle';\n const extraExternals = opts?.extraExternals ?? [];\n\n // Lazy load esbuild at call time (see the module-scope import note).\n const esbuild = await import('esbuild');\n const runtimePath = getRuntimePath();\n\n // Stdin wrapper: import the runtime and the user factory, re-export both.\n // esbuild follows the static imports to include runtime.ts and the user file\n // (via the userFactoryPlugin) in the single IIFE output.\n const wrapperContent = [\n `import { runTestModule } from ${JSON.stringify(runtimePath)};`,\n `import __userFactory from \"user-test-factory\";`,\n `export { runTestModule, __userFactory };`,\n ].join('\\n');\n\n const result = await esbuild.build({\n stdin: {\n contents: wrapperContent,\n loader: 'ts',\n // resolveDir is used for relative imports from the wrapper. Since the\n // wrapper only imports absolute paths (runtimePath) and the virtual\n // \"user-test-factory\" specifier (resolved by plugin), the directory\n // doesn't matter — but we still provide a sensible default.\n resolveDir: path.dirname(absPath),\n },\n bundle: true,\n format: 'iife',\n globalName,\n platform: 'browser',\n target: 'es2022',\n write: false,\n plugins: [userFactoryPlugin(absPath), vitestRedirectPlugin(), sdkRedirectPlugin()],\n external: extraExternals,\n treeShaking: true,\n // Ensure the IIFE result is always reachable via globalThis regardless of\n // the evaluation context. esbuild's `globalName` emits:\n // var __testBundle = (() => { ... })();\n // When `Runtime.evaluate` runs this bundle code inside an outer wrapper\n // (rpc.ts's async IIFE), `var` creates a local variable — NOT a global\n // property — so `globalThis.__testBundle` stays `undefined`. The footer\n // explicitly assigns the local variable to `globalThis` to close that gap.\n footer: {\n js: `globalThis[${JSON.stringify(globalName)}] = ${globalName};`,\n },\n });\n\n const warnings = result.warnings.map(\n (w) =>\n `${path.relative(process.cwd(), w.location?.file ?? '')}:${w.location?.line ?? '?'}: ${w.text}`,\n );\n\n const outputFile = result.outputFiles?.[0];\n if (!outputFile) {\n throw new Error('bundleTestFile: esbuild produced no output — check entryPoints');\n }\n\n return { code: outputFile.text, warnings };\n}\n","/**\n * `__AIT_CAPTURE__` console-line parser (devtools#696).\n *\n * env3 test runs inject sdk-example's `aitCapture.ts`, which — when running in a\n * WebView (no filesystem) — emits one `console.log` line per category in the\n * stable form:\n *\n * __AIT_CAPTURE__ <category> <json>\n *\n * The relay-worker harvests `Runtime.consoleAPICalled` events as plain text\n * lines (see `relay-worker.ts`) and hands them here. This module's only job is\n * to recognise the allowlisted prefix, split off the category, and keep the\n * remaining JSON string OPAQUE — devtools does NOT own or interpret the\n * `__AIT_CAPTURE__` record shape (that lives in sdk-example). We carry\n * `{ category, json }` through untouched so the 2.x↔3.0 line comparison stays a\n * downstream concern.\n *\n * Why a strict allowlist prefix (SECRET-HANDLING): the raw console stream may\n * also contain wss/scheme/relay noise lines. Only lines that EXACTLY start with\n * `'__AIT_CAPTURE__ '` pass; everything else is discarded, so a stray relay URL\n * can never be serialised into a capture artifact.\n *\n * react-free, dependency-free — safe to import from any test-runner entry\n * without dragging in the chii/cloudflared Node graph.\n */\n\n/**\n * One parsed capture line. `json` is the raw JSON string exactly as the page\n * emitted it (an array of sdk-example capture records). devtools treats it as\n * opaque — it is validated as parseable JSON but never inspected.\n */\nexport interface AitCaptureLine {\n /** The capture category (first whitespace-delimited token after the prefix). */\n category: string;\n /** The remaining JSON payload, verbatim. Opaque to devtools. */\n json: string;\n}\n\n/** The exact console-line prefix sdk-example's `flushCapture` emits. */\nconst CAPTURE_PREFIX = '__AIT_CAPTURE__ ';\n\n/**\n * Parses raw console line texts into {@link AitCaptureLine}s.\n *\n * Filtering rules (each independently drops a line — never throws):\n * - the line text must `startsWith(CAPTURE_PREFIX)` exactly (allowlist);\n * - there must be a non-empty category token (up to the next space);\n * - the remaining payload must be valid JSON (`JSON.parse` succeeds).\n *\n * Lines that fail any rule (wss/scheme noise, truncated, broken JSON) are\n * silently discarded — capture harvesting is best-effort and must never fail a\n * run or leak a malformed/secret-bearing line.\n *\n * @param raw - Console line objects (only `.text` is read).\n * @returns The captured lines, in input order.\n */\nexport function parseCaptureLines(raw: ReadonlyArray<{ text: string }>): AitCaptureLine[] {\n const out: AitCaptureLine[] = [];\n for (const { text } of raw) {\n // Allowlist: only the exact `__AIT_CAPTURE__ ` prefix passes. wss/scheme\n // noise lines are dropped here (SECRET-HANDLING).\n if (!text.startsWith(CAPTURE_PREFIX)) continue;\n\n const body = text.slice(CAPTURE_PREFIX.length);\n const spaceIdx = body.indexOf(' ');\n // No payload separator → malformed; drop.\n if (spaceIdx === -1) continue;\n\n const category = body.slice(0, spaceIdx);\n const json = body.slice(spaceIdx + 1);\n if (category === '' || json === '') continue;\n\n // Validate the payload is parseable JSON, but keep it as an OPAQUE string —\n // devtools does not own the record shape. Broken JSON is discarded (no throw).\n try {\n JSON.parse(json);\n } catch {\n continue;\n }\n\n out.push({ category, json });\n }\n return out;\n}\n","/**\n * Node-side RPC helpers for injecting and collecting test execution over CDP.\n *\n * Uses the same IIFE + JSON.stringify envelope pattern as `buildCallSdkExpression`\n * in `src/mcp/tools.ts` to reliably shuttle structured results through\n * `Runtime.evaluate`'s `returnByValue: true` boundary.\n *\n * SECRET-HANDLING: bundle code, relay URLs, and result values are NOT logged.\n */\n\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport type { RunReport } from './runtime.js';\n\n/** Maximum milliseconds to wait for a single evaluate round-trip. */\nconst DEFAULT_TIMEOUT_MS = 30_000;\n\n/**\n * Wraps bundle code in a self-executing IIFE that:\n * 1. Evaluates the bundle (registering describe/it/test).\n * 2. Calls `__testBundle.runTestModule(...)` — the entry the runtime exports.\n * 3. Returns a JSON-serialised `RunReport` string.\n *\n * The double-serialisation (RunReport → JSON string → returnByValue string)\n * is intentional: CDP `returnByValue` reliably transports strings; deeply\n * nested objects can lose fidelity across the Chii relay.\n *\n * SECRET-HANDLING: `bundleCode` MUST NOT be logged by callers.\n */\nexport function buildRunTestsExpression(bundleCode: string): string {\n // We trust bundleCode is already a self-contained IIFE that installs\n // `window.__testBundle` (or `globalThis.__testBundle`).\n // We then call `__testBundle.runTestModule()` and return a JSON string.\n return (\n `(async () => {` +\n // Step 1: evaluate the bundle to register tests\n ` try { ${bundleCode} } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'bundle-eval: ' + String(e && e.message || e)});` +\n ` }` +\n // Step 2: check that the expected exports are present\n ` if (typeof globalThis.__testBundle !== 'object' || typeof globalThis.__testBundle.runTestModule !== 'function' || typeof globalThis.__testBundle.__userFactory !== 'function') {` +\n ` return JSON.stringify({ok:false,error:'bundle-missing-export: __testBundle.runTestModule or __userFactory is not a function'});` +\n ` }` +\n // Step 3: run tests — pass the factory so runTestModule installs globals\n // first, then invokes the factory to register describe/it/test blocks.\n ` try {` +\n ` const report = await globalThis.__testBundle.runTestModule(globalThis.__testBundle.__userFactory);` +\n ` return JSON.stringify({ok:true,value:report});` +\n ` } catch(e) {` +\n ` return JSON.stringify({ok:false,error:'test-run: ' + String(e && e.message || e)});` +\n ` }` +\n `})()`\n );\n}\n\n/**\n * Result of `injectAndRunBundle`.\n */\nexport type RpcRunResult = { ok: true; report: RunReport } | { ok: false; error: string };\n\n/**\n * Parses the raw CDP `returnByValue` result from a `buildRunTestsExpression`\n * evaluate call into a typed `RpcRunResult`.\n *\n * Throws only on parse failure — an `ok:false` envelope is a normal result.\n *\n * SECRET-HANDLING: `rawValue` is not included in error messages.\n */\nexport function parseRunTestsResult(rawValue: unknown): RpcRunResult {\n if (typeof rawValue !== 'string') {\n throw new Error(\n `rpc.parseRunTestsResult: unexpected return type \"${typeof rawValue}\" — expected JSON string`,\n );\n }\n let parsed: unknown;\n try {\n parsed = JSON.parse(rawValue);\n } catch {\n // Do NOT include rawValue — could contain secrets.\n throw new Error('rpc.parseRunTestsResult: bridge returned non-JSON string');\n }\n if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {\n throw new Error('rpc.parseRunTestsResult: parsed result is not an object');\n }\n const obj = parsed as Record<string, unknown>;\n if (obj.ok === true) {\n return { ok: true, report: obj.value as RunReport };\n }\n if (obj.ok === false) {\n return {\n ok: false,\n error: typeof obj.error === 'string' ? obj.error : String(obj.error),\n };\n }\n throw new Error('rpc.parseRunTestsResult: result missing \"ok\" field');\n}\n\n/**\n * Injects `bundleCode` into the attached page and awaits test execution.\n *\n * Uses `Runtime.evaluate` with `awaitPromise: true` to wait for the\n * async IIFE to settle. The 30-second CDP command timeout covers even\n * long-running test suites; split into smaller files if you hit it.\n *\n * @param connection - Active CDP connection (relay or local).\n * @param bundleCode - IIFE bundle string from `bundleTestFile`.\n * @param timeoutMs - Override the default 30 s timeout.\n *\n * SECRET-HANDLING: `bundleCode` and the raw CDP result value are never logged.\n */\nexport async function injectAndRunBundle(\n connection: CdpConnection,\n bundleCode: string,\n timeoutMs = DEFAULT_TIMEOUT_MS,\n): Promise<RpcRunResult> {\n const expression = buildRunTestsExpression(bundleCode);\n\n // Use AbortSignal-style timeout via Promise.race so we surface a clear\n // message rather than hanging indefinitely.\n const timeoutPromise = new Promise<never>((_, reject) =>\n setTimeout(() => reject(new Error(`rpc: evaluate timed out after ${timeoutMs}ms`)), timeoutMs),\n );\n\n const evalPromise = connection.send('Runtime.evaluate', {\n expression,\n returnByValue: true,\n awaitPromise: true,\n });\n\n const cdpResult = await Promise.race([evalPromise, timeoutPromise]);\n\n if (cdpResult.exceptionDetails) {\n // Surface only the engine error string — not the expression or value.\n const msg =\n cdpResult.exceptionDetails.exception?.description ??\n cdpResult.exceptionDetails.text ??\n 'Runtime.evaluate threw an exception';\n throw new Error(`rpc.injectAndRunBundle: ${msg}`);\n }\n\n return parseRunTestsResult(cdpResult.result.value);\n}\n","/**\n * Orchestrator: runs a list of test files sequentially over a CDP relay.\n *\n * Each file goes through: bundle → inject → run → collect.\n * This is the transport layer: it does NOT integrate with Vitest's pool or the\n * MCP surface. The Vitest custom pool (`pool.ts`) and the `run_tests` MCP tool\n * are separate callers that build on this orchestrator.\n *\n * Single-attach constraint: only one page is active at a time. Files run\n * sequentially; parallel execution across targets is out of scope.\n *\n * The 30-second per-file timeout is inherited from `injectAndRunBundle`.\n * For suites that exceed it, split the file into smaller pieces.\n *\n * SECRET-HANDLING: file paths are surfaced in reports; relay URLs are not.\n */\n\nimport type { CdpConnection, ConsoleApiCalledEvent } from '../mcp/cdp-connection.js';\nimport { type BundleOptions, bundleTestFile } from './bundle.js';\nimport { type AitCaptureLine, parseCaptureLines } from './capture.js';\nimport { injectAndRunBundle } from './rpc.js';\nimport type { RunReport, TestResult } from './runtime.js';\n\n/** Per-file result in the aggregate `RunReport`. */\nexport interface FileResult {\n /** Absolute or relative path to the test file. */\n file: string;\n /** Full run report for this file, or an error if bundling/injection failed. */\n result: RunReport | { error: string };\n}\n\n/** Aggregate report returned by `runTestFilesOverRelay`. */\nexport interface RelayRunReport {\n /** ISO timestamp of when the run started. */\n startedAt: string;\n /** Total elapsed wall-clock milliseconds. */\n duration: number;\n /** Per-file results in execution order. */\n files: FileResult[];\n /** Flattened totals across all files. */\n totals: {\n passed: number;\n failed: number;\n skipped: number;\n total: number;\n };\n /**\n * `__AIT_CAPTURE__` lines harvested from the page console during the run\n * (additive field, devtools#696). Empty unless `collectCaptures` was set.\n * Each entry is opaque (`{ category, json }`) — devtools does not interpret\n * the record shape, only forwards it for downstream 2.x↔3.0 diffing.\n *\n * SECRET-HANDLING: only lines matching the `__AIT_CAPTURE__ ` allowlist\n * prefix reach here; relay/wss/scheme noise lines are dropped in\n * `parseCaptureLines`.\n */\n captures: AitCaptureLine[];\n}\n\n/** Options for `runTestFilesOverRelay`. */\nexport interface RelayRunOptions {\n /**\n * Options forwarded to `bundleTestFile` for each file.\n */\n bundleOptions?: BundleOptions;\n /**\n * Per-file evaluate timeout in milliseconds. Defaults to 30 000.\n * Increase for long-running suites or split the file.\n */\n timeoutMs?: number;\n /**\n * When `true`, registers a live `Runtime.consoleAPICalled` listener for the\n * duration of the run and harvests `__AIT_CAPTURE__` lines into\n * `RelayRunReport.captures`. Defaults to **false** so the build-only\n * eval/e2e path (and the Vitest pool) pay no listener/state overhead —\n * `captures` is then an empty array.\n *\n * The listener is registered just BEFORE the first file runs and removed in a\n * `finally` so it never accumulates across runs (no ring-buffer drain — the\n * default 500-entry buffer would silently drop lines via `shift`).\n */\n collectCaptures?: boolean;\n}\n\n/**\n * Runs all `files` sequentially over the given CDP `connection`.\n *\n * For each file:\n * 1. Bundle with esbuild (includes SDK shim + runtime).\n * 2. Inject into the attached page via `Runtime.evaluate`.\n * 3. Await the `RunReport` JSON response.\n * 4. Accumulate results.\n *\n * Returns a `RelayRunReport` with per-file results and flattened totals.\n *\n * This function does NOT open or manage the relay connection — the caller\n * is responsible for attaching and closing it.\n *\n * TODO (#645): implement the Vitest `PoolRunnerInitializer` interface here\n * so that `runTestFilesOverRelay` can be used as a Vitest pool entry.\n *\n * @param connection - Active CDP connection (relay or local kind).\n * @param files - Absolute paths to test files, run in order.\n * @param opts - Optional per-run overrides.\n */\nexport async function runTestFilesOverRelay(\n connection: CdpConnection,\n files: string[],\n opts?: RelayRunOptions,\n): Promise<RelayRunReport> {\n const wallStart = Date.now();\n const startedAt = new Date(wallStart).toISOString();\n const fileResults: FileResult[] = [];\n\n // Enable CDP domains ONCE up front. Without this the relay connection has not\n // opened its client websocket, so the very first `Runtime.evaluate` (in\n // `injectAndRunBundle`) explodes AND `Runtime.consoleAPICalled` never streams\n // — the console capture harvest would be structurally 0. `enableDomains()` is\n // idempotent (see chii-connection: early-returns when the ws is already OPEN,\n // and shares an in-flight promise), so calling it here is safe even when a\n // caller (the MCP `run_tests` path) already enabled it.\n //\n // Failure here (e.g. no target attached yet) must NOT throw the whole run: the\n // per-file inject below produces a structured error result instead. We warn on\n // stderr (secret-free) and continue. SECRET-HANDLING: the message names the\n // failure only — no relay/wss URL.\n let domainsEnabled = false;\n try {\n await connection.enableDomains();\n domainsEnabled = true;\n } catch (e) {\n process.stderr.write(\n `relay-worker: enableDomains() failed before run — console capture may be empty (${\n e instanceof Error ? e.message : String(e)\n })\\n`,\n );\n }\n\n // Live console capture (#696): when requested, accumulate every\n // `Runtime.consoleAPICalled` event into a local array via a LIVE listener\n // registered just before the run. We deliberately do NOT drain\n // `getBufferedEvents` after the fact — that ring buffer caps at 500 and\n // `shift()`s older entries, so a chatty run would silently lose capture lines.\n // The listener is removed in `finally` so it never bleeds into the next run.\n //\n // Gate the listener on `domainsEnabled`: if enableDomains() soft-failed, the\n // client ws never opened so `Runtime.consoleAPICalled` cannot stream —\n // registering would only add a dead listener that never fires. (The `finally`\n // unsubscribe is an undefined no-op in that case, so this stays safe.)\n const collectCaptures = opts?.collectCaptures === true;\n const liveConsole: ConsoleApiCalledEvent[] = [];\n let unsubscribeConsole: (() => void) | undefined;\n if (collectCaptures && domainsEnabled) {\n unsubscribeConsole = connection.on('Runtime.consoleAPICalled', (event) => {\n liveConsole.push(event);\n });\n }\n\n try {\n for (const file of files) {\n let fileEntry: FileResult;\n try {\n const { code } = await bundleTestFile(file, opts?.bundleOptions);\n const rpcResult = await injectAndRunBundle(connection, code, opts?.timeoutMs);\n if (rpcResult.ok) {\n fileEntry = { file, result: rpcResult.report };\n } else {\n fileEntry = { file, result: { error: rpcResult.error } };\n }\n } catch (e) {\n // Capture bundle/inject errors per-file so subsequent files still run.\n fileEntry = {\n file,\n result: {\n error: e instanceof Error ? e.message : String(e),\n },\n };\n }\n fileResults.push(fileEntry);\n }\n } finally {\n // Always remove the live listener — leaking it would accumulate across runs\n // on a reused connection (the Vitest pool keeps one connection for the whole\n // run; the MCP daemon keeps one for the session).\n unsubscribeConsole?.();\n }\n\n // Convert the accumulated console events to `__AIT_CAPTURE__` lines. Each\n // event's args are rendered to a single line text (inlined console rendering —\n // no tools.ts import, to keep this module off the heavy MCP graph), then the\n // allowlist-prefix parser keeps only genuine capture lines.\n const captures = collectCaptures\n ? parseCaptureLines(liveConsole.map((e) => ({ text: renderConsoleLineText(e) })))\n : [];\n\n const totals = fileResults.reduce(\n (acc, { result }) => {\n if ('error' in result) {\n // Treat whole-file errors as a single failure.\n acc.failed += 1;\n acc.total += 1;\n } else {\n acc.passed += result.passed;\n acc.failed += result.failed;\n acc.skipped += result.skipped;\n acc.total += result.passed + result.failed + result.skipped;\n }\n return acc;\n },\n { passed: 0, failed: 0, skipped: 0, total: 0 },\n );\n\n return {\n startedAt,\n duration: Date.now() - wallStart,\n files: fileResults,\n totals,\n captures,\n };\n}\n\n/**\n * Renders one `Runtime.consoleAPICalled` event to a single line of text, the\n * same way `tools.ts#normalizeConsoleMessage` does (args rendered + space-\n * joined). Inlined here (≈8 lines) so this module avoids importing `tools.ts`,\n * which would drag the heavy MCP/Node graph (server-lock, parent-watcher, …)\n * onto the test-runner entry.\n *\n * SECRET-HANDLING: this only stringifies console args; the caller's\n * allowlist-prefix parser then discards everything that is not a genuine\n * `__AIT_CAPTURE__` line.\n */\nfunction renderConsoleLineText(event: ConsoleApiCalledEvent): string {\n return event.args\n .map((arg) => {\n if (arg.value !== undefined) {\n if (typeof arg.value === 'string') return arg.value;\n try {\n return JSON.stringify(arg.value);\n } catch {\n return String(arg.value);\n }\n }\n if (arg.description !== undefined) return arg.description;\n if (arg.className !== undefined) return arg.className;\n return arg.subtype ?? arg.type;\n })\n .join(' ');\n}\n\n/**\n * Flattens all test results from a `RelayRunReport` into a single array.\n * Files that errored during bundle/inject produce a synthetic failed entry.\n */\nexport function flattenResults(report: RelayRunReport): Array<TestResult & { file: string }> {\n const out: Array<TestResult & { file: string }> = [];\n for (const { file, result } of report.files) {\n if ('error' in result) {\n out.push({\n file,\n name: `<bundle/inject error>`,\n status: 'fail',\n duration: 0,\n error: result.error,\n });\n } else {\n for (const t of result.tests) {\n out.push({ ...t, file });\n }\n }\n }\n return out;\n}\n","/**\n * Runner-agnostic report serialisation for env3 test runs (devtools#696).\n *\n * Both env3 execution paths — the Vitest custom pool (`pool.ts`) and the\n * standalone `devtools-test` CLI (`cli.ts`) — call the same core\n * `runTestFilesOverRelay` and so produce the same {@link RelayRunReport}. This\n * module is the single, runner-neutral place that turns that in-memory report\n * into a stable on-disk artifact so a 2.x run and a 3.0 run can be diffed\n * cell-by-cell after the fact.\n *\n * The serialised schema is deliberately MINIMAL and secret-free:\n *\n * - file paths are stored RELATIVE to `projectRoot` (no absolute `/Users/...`\n * leakage — see {@link RunnerAgnosticReport.files});\n * - the cell metadata (sdkLine/platform) is baked INTO the body, not only the\n * filename, so a moved artifact never loses its provenance;\n * - NO relay wss / scheme / TOTP / relayUrl fields exist in the schema at all\n * (enforced by the type + this comment) — error strings are the matcher\n * message only, inherited from rpc.ts which already strips expression/value.\n *\n * react-free — depends only on the type-level `RelayRunReport` and `node:fs` /\n * `node:path`. Safe to bundle without pulling the chii/cloudflared graph.\n */\n\nimport { mkdir, writeFile } from 'node:fs/promises';\nimport path from 'node:path';\nimport type { AitCaptureLine } from './capture.js';\nimport type { RelayRunReport } from './relay-worker.js';\nimport type { TestResult } from './runtime.js';\n\n/** The cell axes a report is stamped with — the test-matrix coordinates. */\nexport interface ReportCellMeta {\n /** SDK line under test (e.g. `'2.x'` / `'3.x'`). */\n sdkLine: string;\n /** Platform under test (e.g. `'ios'` / `'android'` / `'mock'`). */\n platform: string;\n /**\n * Project root the run was launched from. Used ONLY to relativise file paths\n * out of the serialised report — never stored in the output. SECRET-HANDLING:\n * absolute project paths must not leak into artifacts.\n */\n projectRoot: string;\n}\n\n/** Per-file slice of a {@link RunnerAgnosticReport}. */\nexport interface RunnerAgnosticFileReport {\n /**\n * Test file path, RELATIVE to `projectRoot`. Never absolute — `path.relative`\n * strips the machine-specific prefix so the artifact is portable and leaks no\n * local filesystem layout.\n */\n file: string;\n /** Whole-file bundle/inject error (matcher message only), when the file failed. */\n error?: string;\n /** In-page run duration (ms) for this file, when it ran. */\n duration?: number;\n passed?: number;\n failed?: number;\n skipped?: number;\n /** Per-test results, when the file ran. Error strings are matcher messages only. */\n tests?: TestResult[];\n}\n\n/**\n * The runner-neutral, secret-free report written to disk. There are\n * intentionally NO wss/scheme/TOTP/relayUrl fields on this type — the absence is\n * load-bearing (SECRET-HANDLING) and must not be \"completed\" by a future edit.\n */\nexport interface RunnerAgnosticReport {\n /** Cell axes this run belongs to — baked into the body for portability. */\n cell: { sdkLine: string; platform: string };\n /** ISO timestamp of when the run started (from the core report). */\n startedAt: string;\n /** Total wall-clock ms (bundling + sequential injection). */\n duration: number;\n /** Flattened totals across all files. */\n totals: RelayRunReport['totals'];\n /** Per-file results with projectRoot-relative paths. */\n files: RunnerAgnosticFileReport[];\n}\n\n/**\n * Converts an absolute (or already-relative) file path to a projectRoot-relative\n * one. `path.relative` returns `''` when the paths are equal — guard that to the\n * basename so the field is never empty.\n *\n * SECRET-HANDLING: this is the single choke point that strips absolute project\n * paths from the artifact.\n */\nfunction relativise(projectRoot: string, file: string): string {\n const rel = path.relative(projectRoot, file);\n if (rel === '' || rel.startsWith('..') || path.isAbsolute(rel)) {\n // Outside the project root (or equal) — fall back to the basename rather\n // than emitting an absolute or `../../..` traversal path.\n return path.basename(file);\n }\n return rel;\n}\n\n/**\n * Serialises a {@link RelayRunReport} into the runner-agnostic, secret-free\n * on-disk shape. Pure — no IO; testable with a plain report + meta.\n *\n * @param report - The core relay run report.\n * @param meta - Cell axes + projectRoot (projectRoot is consumed, not stored).\n */\nexport function serializeRelayReport(\n report: RelayRunReport,\n meta: ReportCellMeta,\n): RunnerAgnosticReport {\n return {\n cell: { sdkLine: meta.sdkLine, platform: meta.platform },\n startedAt: report.startedAt,\n duration: report.duration,\n totals: report.totals,\n files: report.files.map((f): RunnerAgnosticFileReport => {\n const file = relativise(meta.projectRoot, f.file);\n if ('error' in f.result) {\n // Matcher/inject error message only — rpc.ts already stripped the\n // expression/value upstream.\n return { file, error: f.result.error };\n }\n return {\n file,\n duration: f.result.duration,\n passed: f.result.passed,\n failed: f.result.failed,\n skipped: f.result.skipped,\n tests: f.result.tests,\n };\n }),\n };\n}\n\n/**\n * Writes the serialised report to `<dir>/<sdkLine>.<platform>.json`, creating\n * `dir` if needed. Returns the absolute path written.\n *\n * The cell-suffixed filename keeps 2.x and 3.0 (and per-platform) runs as\n * distinct artifacts in the same directory; the same cell metadata is also baked\n * into the body so a renamed/moved file still carries its provenance.\n *\n * SECRET-HANDLING: the written body contains no relay/secret fields (the schema\n * has none). `dir`/`projectRoot` are local filesystem paths, never logged here.\n *\n * @param report - The core relay run report.\n * @param dir - Output directory (created recursively if missing).\n * @param meta - Cell axes + projectRoot.\n * @returns The absolute path of the written file.\n */\nexport async function writeReportArtifact(\n report: RelayRunReport,\n dir: string,\n meta: ReportCellMeta,\n): Promise<string> {\n const serialised = serializeRelayReport(report, meta);\n await mkdir(dir, { recursive: true });\n const outFile = path.join(dir, `${meta.sdkLine}.${meta.platform}.json`);\n await writeFile(outFile, `${JSON.stringify(serialised, null, 2)}\\n`, 'utf8');\n return outFile;\n}\n\n/**\n * Writes harvested `__AIT_CAPTURE__` lines to per-category files under `dir`,\n * named `<category>.<sdkLine>.<platform>.json` — the SAME convention\n * sdk-example's env1 `flushCapture` uses on the filesystem, so env1 and env3\n * capture artifacts line up for diffing.\n *\n * Each line's `json` payload is an opaque JSON array of capture records. Lines\n * sharing a category are concatenated into one array, in harvest order.\n *\n * SECRET-HANDLING: only allowlist-prefixed capture lines reach here (the parser\n * dropped wss/scheme noise); the `json` payload is written verbatim but is a\n * capture record array, not a relay/secret.\n *\n * @param captures - Parsed capture lines (from `RelayRunReport.captures`).\n * @param dir - Output directory (created recursively if missing).\n * @param cell - Cell axes for the filename suffix.\n * @returns The absolute paths written (one per category), in category order.\n */\nexport async function writeCaptureArtifacts(\n captures: ReadonlyArray<AitCaptureLine>,\n dir: string,\n cell: { sdkLine: string; platform: string },\n): Promise<string[]> {\n if (captures.length === 0) return [];\n\n // Group payloads by category, preserving harvest order. Each payload is an\n // opaque JSON array string; concatenate parsed arrays under the same category.\n const byCategory = new Map<string, unknown[]>();\n for (const { category, json } of captures) {\n let merged = byCategory.get(category);\n if (!merged) {\n merged = [];\n byCategory.set(category, merged);\n }\n // The parser already validated `json` parses; an array payload is expected.\n const parsed = JSON.parse(json) as unknown;\n if (Array.isArray(parsed)) {\n merged.push(...parsed);\n } else {\n merged.push(parsed);\n }\n }\n\n await mkdir(dir, { recursive: true });\n const written: string[] = [];\n for (const [category, records] of byCategory) {\n const outFile = path.join(dir, `${category}.${cell.sdkLine}.${cell.platform}.json`);\n await writeFile(outFile, `${JSON.stringify(records, null, 2)}\\n`, 'utf8');\n written.push(outFile);\n }\n return written;\n}\n","/**\n * `devtools-test` CLI.\n *\n * Shares test-file discovery with the `run_tests` MCP tool (`discoverTestFiles`)\n * and exposes `runWithConnection` — the pure run core that bundles, injects, and\n * collects each file over a CDP connection. The CLI's `main()` performs a\n * standalone relay attach (boot relay → QR → phone scan → cell inject → run).\n *\n * NOTE: no shebang in this source file — the tsdown entry's `banner` option\n * injects `#!/usr/bin/env node` into the compiled output (same pattern as\n * `src/mcp/cli.ts`).\n */\n\nimport { parseArgs } from 'node:util';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport { discoverTestFiles } from './discover.js';\nimport { createRelayConnectionFactory } from './relay-factory.js';\nimport type { RelayRunOptions, RelayRunReport } from './relay-worker.js';\nimport { runTestFilesOverRelay } from './relay-worker.js';\nimport { writeCaptureArtifacts, writeReportArtifact } from './report.js';\n\n/* -------------------------------------------------------------------------- */\n/* CLI help */\n/* -------------------------------------------------------------------------- */\n\nconst USAGE = `\ndevtools-test — run mini-app tests on a real device WebView over the CDP relay\n\nUSAGE\n devtools-test <glob> [<glob> ...] [options]\n\nOPTIONS\n --scheme-url <url> intoss-private:// URL from \\`ait deploy --scheme-only\\`\n (required for standalone relay attach / env3)\n --timeout <ms> Per-file evaluate timeout in ms (default: 30000)\n --cell-sdk-line <line> SDK line to inject as __AIT_CELL__.sdkLine (2.x|3.x)\n --cell-platform <plat> Platform to inject as __AIT_CELL__.platform\n (mock|ios|android, default: AIT_CELL_PLATFORM env)\n --report-dir <dir> Persist a runner-agnostic report + captures to <dir>\n (report: <sdkLine>.<platform>.json; captures:\n <dir>/.ait-capture/<category>.<sdkLine>.<platform>.json).\n Omitted = nothing saved. Enables console capture.\n --no-qr-stdout Suppress the QR/attach block on stdout (auto-on for\n non-interactive stdout / CI / AIT_NO_QR_STDOUT)\n --headless Disable browser auto-open (text QR only)\n --project-root <dir> Project root for .ait_relay secret lookup\n (default: current working directory)\n --help, -h Show this help message\n\nDESCRIPTION\n Boots a Chii relay + cloudflared tunnel, renders a QR code, waits for a real\n device to scan and attach, injects the cell globals (__AIT_CELL__), bundles\n each matched test file with esbuild (SDK imports redirected to window.__sdk),\n injects the bundle into the attached WebView via Runtime.evaluate, and prints\n a summary.\n\n With --report-dir, also harvests __AIT_CAPTURE__ console lines and writes a\n runner-agnostic report + per-category capture files so 2.x↔3.0 runs can be\n compared offline.\n\n The test files run against the live relay connection started by this process;\n no separate MCP daemon is required.\n\nEXAMPLE\n devtools-test 'src/**/*.ait.test.ts' \\\\\n --scheme-url \"intoss-private://...\" \\\\\n --cell-sdk-line 3.x \\\\\n --cell-platform ios \\\\\n --report-dir .ait-report \\\\\n --timeout 60000\n\n`.trimStart();\n\n/* -------------------------------------------------------------------------- */\n/* Pure run function (testable without a real relay) */\n/* -------------------------------------------------------------------------- */\n\n/** Options for `runWithConnection`. */\nexport interface RunWithConnectionOptions extends RelayRunOptions {\n /** If true, print a summary to stdout. Defaults to false in tests. */\n printSummary?: boolean;\n}\n\n/**\n * Runs `files` over `connection` and returns the aggregate report.\n * This pure function is the testable core of the CLI (and is what the\n * `run_tests` MCP tool calls against the daemon's attached connection); it is\n * separate from `main()` so tests can call it without spawning a subprocess.\n */\nexport async function runWithConnection(\n connection: CdpConnection,\n files: string[],\n opts?: RunWithConnectionOptions,\n): Promise<RelayRunReport> {\n const report = await runTestFilesOverRelay(connection, files, opts);\n\n if (opts?.printSummary) {\n const { totals } = report;\n process.stdout.write(\n `\\ndevtools-test: ${totals.passed} passed, ${totals.failed} failed, ${totals.skipped} skipped (${report.duration}ms)\\n`,\n );\n }\n\n return report;\n}\n\n/* -------------------------------------------------------------------------- */\n/* main() — CLI entry point */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Decides whether to suppress the QR/attach block on stdout.\n *\n * Suppress when EITHER the user passed `--no-qr-stdout`, OR stdout is not a TTY\n * / `CI` is set / `AIT_NO_QR_STDOUT` is set (non-interactive — a captured stdout\n * must not leak the relay wss + TOTP `at=` code that the QR block encodes). The\n * suppression is whole-chunk: `attachUrl` AND `relayUrl` ride in the same block.\n *\n * Exported for unit testing.\n */\nexport function shouldSuppressQr(noQrFlag: boolean): boolean {\n return (\n noQrFlag ||\n !process.stdout.isTTY ||\n process.env.CI !== undefined ||\n process.env.AIT_NO_QR_STDOUT !== undefined\n );\n}\n\n/**\n * CLI entry point.\n *\n * Performs a standalone relay attach → run lifecycle, sharing the attach\n * assembly with the Vitest pool via `createRelayConnectionFactory` (single\n * source — no drift):\n *\n * 1. Parse args: globs, --timeout, --cell-sdk-line, --cell-platform,\n * --scheme-url (required for env3), --report-dir, --no-qr-stdout,\n * --headless, --project-root.\n * 2. Discover test files; exit 1 if none.\n * 3. factory.open() — boot relay → render QR (suppressed on non-interactive\n * stdout) → wait for phone → inject cell → enableDomains. Returns the conn.\n * 4. runWithConnection(conn, files, { timeoutMs, collectCaptures, printSummary }).\n * 5. With --report-dir: write the runner-agnostic report + capture files.\n * 6. factory.close(); process.exitCode = failed > 0 ? 1 : 0.\n *\n * The CLI is not a daemon — no lock, router, SSE, or tools_list is needed.\n * Attach timeout exits with code 1; test failures exit with code 1.\n *\n * SECRET-HANDLING: scheme_url / relay wssUrl / TOTP codes are never written to\n * stdout/stderr directly. The QR block (which encodes the TOTP `at=` code) is\n * printed only when stdout is interactive AND not suppressed.\n */\nexport async function main(argv: string[] = process.argv.slice(2)): Promise<void> {\n // ── Step 1: parse arguments ───────────────────────────────────────────────\n let parsed: ReturnType<typeof parseArgs>;\n try {\n parsed = parseArgs({\n args: argv,\n options: {\n help: { type: 'boolean', short: 'h' },\n timeout: { type: 'string' },\n 'scheme-url': { type: 'string' },\n 'cell-sdk-line': { type: 'string' },\n 'cell-platform': { type: 'string' },\n 'report-dir': { type: 'string' },\n 'no-qr-stdout': { type: 'boolean' },\n headless: { type: 'boolean' },\n 'project-root': { type: 'string' },\n } as const,\n allowPositionals: true,\n });\n } catch (e) {\n process.stderr.write(`devtools-test: ${e instanceof Error ? e.message : String(e)}\\n`);\n process.exitCode = 1;\n return;\n }\n\n if (parsed.values.help || argv.length === 0) {\n process.stdout.write(USAGE);\n return;\n }\n\n // Extract string-typed flags explicitly — parseArgs returns `string | boolean | (string | boolean)[]`\n // for the union `values` type when options have mixed `type` fields, so we\n // narrow each flag here.\n const vals = parsed.values;\n const rawTimeout = typeof vals.timeout === 'string' ? vals.timeout : undefined;\n const timeoutMs = rawTimeout !== undefined ? parseInt(rawTimeout, 10) : 30_000;\n if (Number.isNaN(timeoutMs) || timeoutMs <= 0) {\n process.stderr.write(`devtools-test: --timeout must be a positive integer\\n`);\n process.exitCode = 1;\n return;\n }\n\n const schemeUrl = typeof vals['scheme-url'] === 'string' ? vals['scheme-url'] : '';\n if (schemeUrl === '') {\n process.stderr.write(\n `devtools-test: --scheme-url is required for standalone relay attach.\\n` +\n ` Pass the intoss-private:// URL from \\`ait deploy --scheme-only\\`.\\n`,\n );\n process.exitCode = 1;\n return;\n }\n\n const headless = vals.headless === true;\n const projectRoot =\n typeof vals['project-root'] === 'string' ? vals['project-root'] : process.cwd();\n const reportDir = typeof vals['report-dir'] === 'string' ? vals['report-dir'] : undefined;\n const suppressQr = shouldSuppressQr(vals['no-qr-stdout'] === true);\n\n // Cell: --cell-sdk-line and --cell-platform (fall back to AIT_CELL_PLATFORM env).\n const cellSdkLine = typeof vals['cell-sdk-line'] === 'string' ? vals['cell-sdk-line'] : undefined;\n const cellPlatform =\n typeof vals['cell-platform'] === 'string'\n ? vals['cell-platform']\n : process.env.AIT_CELL_PLATFORM;\n const hasCell = cellSdkLine !== undefined || cellPlatform !== undefined;\n // The cell injected onto the page and the report/capture filename suffix.\n // sdk-example's own fallbacks are '2.x'/'mock'; mirror them when a flag is\n // absent so artifacts still get a stable, meaningful cell suffix.\n const cell = { sdkLine: cellSdkLine ?? '2.x', platform: cellPlatform ?? 'mock' };\n\n // ── Step 2: discover test files ───────────────────────────────────────────\n const globs = parsed.positionals;\n if (globs.length === 0) {\n process.stderr.write(`devtools-test: at least one glob pattern is required\\n`);\n process.stdout.write(USAGE);\n process.exitCode = 1;\n return;\n }\n\n const files = await discoverTestFiles(globs, process.cwd());\n if (files.length === 0) {\n process.stderr.write(`devtools-test: no test files matched ${globs.join(', ')}\\n`);\n process.exitCode = 1;\n return;\n }\n process.stderr.write(`devtools-test: found ${files.length} test file(s)\\n`);\n\n // ── Step 3: open the relay connection via the shared factory ──────────────\n // The factory boots the relay, renders the QR, waits for the phone, injects\n // the cell, and enables CDP domains — the same assembly the Vitest pool uses.\n // We pass `onQrContent` so the CLI owns the stdout decision: suppress the whole\n // QR block on non-interactive stdout (it encodes the relay wss + TOTP code).\n // SECRET-HANDLING: scheme_url / wss / TOTP are never logged by the factory.\n if (hasCell) {\n process.stderr.write(`devtools-test: injecting __AIT_CELL__ = ${JSON.stringify(cell)}\\n`);\n }\n const factory = createRelayConnectionFactory({\n schemeUrl,\n projectRoot,\n timeoutMs,\n headless,\n cell: hasCell ? cell : undefined,\n onQrContent: (chunks) => {\n if (suppressQr) {\n process.stdout.write('QR suppressed (non-interactive)\\n');\n return;\n }\n for (const chunk of chunks) process.stdout.write(`${chunk}\\n`);\n },\n });\n\n let connection: CdpConnection;\n try {\n connection = await factory.open();\n } catch (e) {\n process.stderr.write(`devtools-test: ${e instanceof Error ? e.message : String(e)}\\n`);\n process.exitCode = 1;\n return;\n }\n\n // Ensure cleanup on early exit.\n let exitCode = 0;\n try {\n // ── Step 4: run test files ──────────────────────────────────────────────\n // collectCaptures is enabled only when a report dir is given (the only sink\n // for captures) — keeps the no-report path free of listener overhead.\n const report = await runWithConnection(connection, files, {\n timeoutMs,\n printSummary: true,\n collectCaptures: reportDir !== undefined,\n });\n\n // ── Step 5: persist artifacts (only with --report-dir) ──────────────────\n if (reportDir !== undefined) {\n try {\n const reportPath = await writeReportArtifact(report, reportDir, {\n sdkLine: cell.sdkLine,\n platform: cell.platform,\n projectRoot,\n });\n process.stderr.write(`devtools-test: wrote report ${reportPath}\\n`);\n const capturePaths = await writeCaptureArtifacts(\n report.captures,\n `${reportDir}/.ait-capture`,\n cell,\n );\n if (capturePaths.length > 0) {\n process.stderr.write(`devtools-test: wrote ${capturePaths.length} capture file(s)\\n`);\n }\n } catch (e) {\n // Artifact write failure must not mask the test result.\n process.stderr.write(\n `devtools-test: failed to write report artifacts: ${e instanceof Error ? e.message : String(e)}\\n`,\n );\n }\n }\n\n exitCode = report.totals.failed > 0 ? 1 : 0;\n } finally {\n // ── Step 6: teardown ────────────────────────────────────────────────────\n // factory.close() stops the relay family (closes the CDP connection +\n // shuts down the relay + cloudflared child).\n await factory.close(connection);\n process.exitCode = exitCode;\n }\n}\n","/**\n * `devtools-test` bin entry point — export-free by design.\n *\n * This file is intentionally export-free so that Rolldown (tsdown) emits the\n * main() call directly into the output bundle rather than hoisting the module\n * body into a shared chunk and replacing this file with a re-export wrapper.\n *\n * Background (#711): cli.ts exports `main`, `runWithConnection`, and\n * `shouldSuppressQr` for use by tests and relay-factory. When cli.ts was also\n * the bin entry, Rolldown split the module body into a shared chunk\n * (`cli-<hash>.js`) and reduced the bin output to a 3-line re-export wrapper.\n * The self-invoke guard (`import.meta.url === process.argv[1]`) evaluated\n * inside the shared chunk where `import.meta.url` is the chunk path, not the\n * bin path — a structural permanent mismatch — so main() was never called and\n * every `devtools-test` / `pnpm test:env3` invocation exited 0 as a silent\n * no-op.\n *\n * NOTE: no shebang in this source file — the tsdown entry's `banner` option\n * injects `#!/usr/bin/env node` into the compiled output (same pattern as\n * other Node bin entries in tsdown.config.ts).\n */\n\nimport { main } from './cli.js';\n\nmain().catch((e: unknown) => {\n process.stderr.write(\n `devtools-test: unexpected error: ${e instanceof Error ? e.message : String(e)}\\n`,\n );\n process.exitCode = 1;\n});\n"],"mappings":"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"}
@@ -1,5 +1,5 @@
1
1
  import { RELAY_POOL_NAME, createRelayPool } from "./pool.js";
2
- import { t as createRelayConnectionFactory } from "../relay-factory-BBnZM-an.js";
2
+ import { t as createRelayConnectionFactory } from "../relay-factory-CqY264j0.js";
3
3
  //#region src/test-runner/config.ts
4
4
  const DEFAULT_CONFIG = {
5
5
  include: ["**/*.ait.test.ts"],
@@ -22,12 +22,23 @@ function createRelayConnectionFactory(opts) {
22
22
  return {
23
23
  async open() {
24
24
  const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import("../attach-orchestrator-RhOdO3DS.js");
25
- const { injectDebugIndicator, injectGlobals } = await import("../cell-3bIG9538.js");
25
+ const { injectDebugIndicator, injectGlobals } = await import("../cell-Ba9lCI3B.js");
26
26
  const { loadRelaySecretReadOnly } = await import("../relay-secret-store-BcVrWwTq.js");
27
- const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-CF-1s3ks.js");
27
+ const { bootRelayFamily, buildRelayVerifyAuth } = await import("../debug-server-BDfeWytB.js");
28
28
  await loadRelaySecretReadOnly({ projectRoot });
29
- const booted = await bootRelayFamily({ verifyAuth: buildRelayVerifyAuth() });
29
+ let resolveTunnelUp;
30
+ const tunnelReady = new Promise((resolve) => {
31
+ resolveTunnelUp = resolve;
32
+ });
33
+ const booted = await bootRelayFamily({
34
+ verifyAuth: buildRelayVerifyAuth(),
35
+ onWssUrl: () => {
36
+ resolveTunnelUp();
37
+ qrServer?.notifyStateChange();
38
+ }
39
+ });
30
40
  family = booted;
41
+ if (booted.getTunnelStatus?.().up) resolveTunnelUp();
31
42
  let lastAttachParts;
32
43
  const attachDeps = {
33
44
  getTunnelStatus: booted.getTunnelStatus ?? (() => ({
@@ -40,7 +51,7 @@ function createRelayConnectionFactory(opts) {
40
51
  canOpenBrowser: () => !headless
41
52
  };
42
53
  try {
43
- const { startQrHttpServer } = await import("../qr-http-server-BsWlOA85.js");
54
+ const { startQrHttpServer } = await import("../qr-http-server-CrGohOwC.js");
44
55
  const getDashboardState = () => ({
45
56
  tunnel: attachDeps.getTunnelStatus(),
46
57
  pages: null,
@@ -55,16 +66,22 @@ function createRelayConnectionFactory(opts) {
55
66
  };
56
67
  process.stderr.write(`devtools-test: QR dashboard: http://127.0.0.1:${qrServer.port}/\n`);
57
68
  } catch {}
69
+ const TUNNEL_BOOT_TIMEOUT_MS = 15e3;
70
+ await Promise.race([tunnelReady, new Promise((resolve) => setTimeout(resolve, TUNNEL_BOOT_TIMEOUT_MS))]);
58
71
  const prep = await prepareAttach(attachDeps, "relay-dev", { scheme_url: opts.schemeUrl }, booted.connection);
59
72
  if (!prep.ok) {
60
73
  booted.stop();
61
74
  family = void 0;
75
+ await qrServer?.close();
76
+ qrServer = void 0;
62
77
  throw new Error("createRelayConnectionFactory: attach preparation failed — check the scheme_url and that the relay tunnel is up");
63
78
  }
64
79
  const waitResult = await renderAndMaybeWait(attachDeps, prep, true, timeoutMs, booted.connection);
65
80
  if (waitResult.isError) {
66
81
  booted.stop();
67
82
  family = void 0;
83
+ await qrServer?.close();
84
+ qrServer = void 0;
68
85
  const timeoutSec = Math.round(timeoutMs / 1e3);
69
86
  throw new Error(`createRelayConnectionFactory: attach timed out after ${timeoutSec}s — phone did not scan the QR within the timeout`);
70
87
  }
@@ -1 +1 @@
1
- {"version":3,"file":"relay-factory.js","names":[],"sources":["../../src/test-runner/relay-factory.ts"],"sourcesContent":["/**\n * Relay connection factory for the Vitest custom pool (devtools#696).\n *\n * The Vitest pool (`pool.ts`) takes a {@link RelayConnectionFactory} that knows\n * how to `open()` a live CDP relay connection (boot relay → QR → phone scan →\n * cell inject → enableDomains) and `close()` it. Before this module that exact\n * assembly lived only inside the `devtools-test` CLI's `main()`; the standalone\n * CLI and the Vitest pool would otherwise each hand-roll it and drift.\n *\n * This is the single source of that assembly. `cli.ts` is refactored to call\n * `createRelayConnectionFactory(...).open()`, and `definePhoneVitestConfig`\n * (config.ts) exposes it so a downstream `vitest.config.ts` can wire the pool:\n *\n * import { createRelayConnectionFactory } from '@ait-co/devtools/test-runner';\n * const connection = createRelayConnectionFactory({\n * schemeUrl,\n * cell,\n * // REQUIRED — own the stdout decision (the chunks carry the relay wss +\n * // TOTP code). Suppress on non-interactive stdout; print otherwise.\n * onQrContent: (chunks) => {\n * if (!process.stdout.isTTY) return;\n * for (const c of chunks) process.stdout.write(`${c}\\n`);\n * },\n * });\n * export default defineConfig({ test: definePhoneVitestConfig({ connection }) });\n *\n * The heavy boot graph (chii relay, cloudflared, ws, debug-server) is pulled via\n * DYNAMIC import inside `open()` — so merely importing this module (or the\n * `@ait-co/devtools/test-runner` barrel) does NOT statically drag that graph in.\n *\n * SECRET-HANDLING: relay wss URLs, scheme URLs, and the TOTP secret/code are\n * never logged. `open()` reads the project-local `.ait_relay` secret read-only\n * (never mints) and the minted TOTP code rides only inside the QR `at=` param.\n *\n * Node-only. react-free (CdpConnection + lazily-imported MCP boot helpers only).\n */\n\nimport type { AttachDeps, AttachUrlParts } from '../mcp/attach-orchestrator.js';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport type { QrHttpServer } from '../mcp/qr-http-server.js';\nimport type { RelayConnectionFactory } from './pool.js';\n\n// NOTE: every value import below is a DYNAMIC import inside `open()`. This module\n// keeps ONLY type-level static imports so that re-exporting it from the\n// `@ait-co/devtools/test-runner` barrel (config.ts) does NOT statically drag the\n// heavy MCP graph (cell.ts → attach-orchestrator.ts → tools.ts → server-lock,\n// plus chii/cloudflared via debug-server) onto that Node-config entry.\n\n/** Options for {@link createRelayConnectionFactory}. */\nexport interface RelayConnectionFactoryOptions {\n /**\n * intoss-private:// scheme URL from `ait deploy --scheme-only` (env3). The\n * phone cold-loads the candidate bundle this URL points at. SECRET-HANDLING:\n * never logged.\n */\n schemeUrl: string;\n /**\n * Project root for the `.ait_relay` secret lookup (read-only). Defaults to\n * `process.cwd()`.\n */\n projectRoot?: string;\n /**\n * Attach wait timeout in ms — how long `open()` waits for the phone to scan\n * the QR and attach. Defaults to 600 000 (10 min) to give time for a manual\n * scan. (The per-file evaluate timeout is separate, passed via the pool's\n * `run` options.)\n */\n timeoutMs?: number;\n /** Disable browser auto-open of the QR dashboard (text QR only). */\n headless?: boolean;\n /**\n * Cell axes injected as `__AIT_CELL__` before the first test bundle runs, so\n * sdk-example's capture picks up the correct sdkLine/platform. Optional — when\n * omitted no cell is injected. The values are not secrets.\n */\n cell?: { sdkLine: string; platform: string };\n /**\n * Receives the QR/attach render content (text chunks) from\n * `renderAndMaybeWait`, so the caller decides whether to print them — e.g.\n * suppress on non-interactive stdout.\n *\n * REQUIRED (not optional) on purpose: these chunks contain the QR payload\n * (which encodes the relay wss + TOTP `at=` code) and the attach JSON block.\n * Making the hook mandatory means the factory never falls back to printing\n * them itself — a downstream `vitest.config.ts` consumer that wires this via\n * the `@ait-co/devtools/test-runner` barrel is forced to make an explicit\n * stdout decision rather than silently leaking the secret-bearing block.\n *\n * SECRET-HANDLING: when a non-interactive caller is detected the hook MUST\n * suppress the WHOLE chunk (not just `attachUrl`), since `relayUrl` rides in\n * the same block.\n */\n onQrContent: (textChunks: string[]) => void;\n}\n\n/**\n * Builds a {@link RelayConnectionFactory} that opens a standalone env3 relay\n * connection.\n *\n * `open()` performs the full attach lifecycle and BLOCKS for tens of seconds (up\n * to `timeoutMs`) while a human scans the rendered QR with their phone — there\n * is no way around the manual scan for env3. It resolves with the live\n * `CdpConnection` once a matching page attaches; `close()` tears the relay\n * family down.\n *\n * The factory holds the booted relay family in a closure so `close()` can stop\n * it. A second `open()` on the same factory boots a fresh family (the previous\n * one should have been `close()`d first).\n */\nexport function createRelayConnectionFactory(\n opts: RelayConnectionFactoryOptions,\n): RelayConnectionFactory {\n const projectRoot = opts.projectRoot ?? process.cwd();\n const timeoutMs = opts.timeoutMs ?? 600_000;\n const headless = opts.headless === true;\n\n // Captured so close() can stop the family that open() booted.\n let family: { connection: CdpConnection; stop(): void } | undefined;\n // QR HTTP server — started during open() when not headless, closed in close().\n let qrServer: QrHttpServer | undefined;\n\n return {\n async open(): Promise<CdpConnection> {\n // Dynamic imports: keep the chii/cloudflared/debug-server graph OFF the\n // static import graph of this module (and the test-runner config barrel).\n const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import(\n '../mcp/attach-orchestrator.js'\n );\n const { injectDebugIndicator, injectGlobals } = await import('./cell.js');\n const { loadRelaySecretReadOnly } = await import('../mcp/relay-secret-store.js');\n const { bootRelayFamily, buildRelayVerifyAuth } = await import('../mcp/debug-server.js');\n\n // Load the project-local .ait_relay secret into AIT_DEBUG_TOTP_SECRET\n // BEFORE booting the relay so assertRelayAuthConfigured()/buildRelayVerifyAuth()\n // at the boot site see it. Read-only — never mints. SECRET-HANDLING: the\n // value is never logged here.\n await loadRelaySecretReadOnly({ projectRoot });\n\n const booted = await bootRelayFamily({ verifyAuth: buildRelayVerifyAuth() });\n family = booted;\n\n // Track the last-captured attach parts so getDashboardState can mint a\n // fresh attach URL on every dashboard request/SSE push (fresh TOTP at=).\n // SECRET-HANDLING: parts contain the relay wss + scheme URL — never logged.\n let lastAttachParts: AttachUrlParts | undefined;\n\n // Assemble AttachDeps. We set qrHttpServer and onAttachUrlBuilt below\n // (before prepareAttach) after optionally starting the web-QR server.\n const attachDeps: AttachDeps = {\n getTunnelStatus: booted.getTunnelStatus ?? (() => ({ up: false, wssUrl: null })),\n getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,\n qrHttpServer: undefined, // filled in below if web-QR server started\n onAttachUrlBuilt: undefined, // filled in below\n canOpenBrowser: () => !headless,\n };\n\n // Web-QR server: reuse the same loopback HTTP dashboard that the MCP\n // start_attach path uses (src/mcp/qr-http-server.ts). This makes the QR\n // scannable even when stdout is non-interactive (Claude Code `!` / CI),\n // because the browser is opened by URL — not via captured stdout.\n //\n // Headless decision: we start the server even in --headless mode so the\n // printed stderr URL can be opened manually. The existing\n // canOpenBrowser: () => !headless gate inside renderAndMaybeWait prevents\n // the auto-open; headless users see only the stderr URL.\n //\n // On failure we fall back gracefully to the text-QR path — do NOT crash.\n // SECRET-HANDLING: only http://127.0.0.1:<port>/ (no secrets) goes to stderr.\n try {\n const { startQrHttpServer } = await import('../mcp/qr-http-server.js');\n\n const getDashboardState = () => ({\n tunnel: attachDeps.getTunnelStatus(),\n pages: null, // CLI/pool: no page-list introspection needed\n attachUrl: lastAttachParts ? mintAttachUrl(attachDeps, lastAttachParts) : null,\n mode: 'relay-dev' as const,\n });\n\n qrServer = await startQrHttpServer(getDashboardState);\n\n // Wire the QR server into attachDeps BEFORE prepareAttach is called so\n // renderAndMaybeWait sees it and takes Path 2/3 (web-QR) instead of Path 4.\n attachDeps.qrHttpServer = qrServer;\n\n // Capture attach parts via onAttachUrlBuilt so getDashboardState can\n // mint a fresh URL (fresh TOTP at= code) on every dashboard render.\n attachDeps.onAttachUrlBuilt = (parts: AttachUrlParts) => {\n lastAttachParts = parts;\n qrServer?.notifyStateChange();\n };\n\n // Print the loopback dashboard URL to stderr — it carries no secrets\n // (TOTP codes and relay wss live only in the in-memory HTTP response).\n process.stderr.write(`devtools-test: QR dashboard: http://127.0.0.1:${qrServer.port}/\\n`);\n } catch {\n // startQrHttpServer failed (e.g. port conflict, import error). Fall back\n // to the existing text-QR path by leaving qrHttpServer: undefined.\n // qrServer remains undefined; close() handles that with optional chaining.\n }\n\n const prep = await prepareAttach(\n attachDeps,\n 'relay-dev',\n { scheme_url: opts.schemeUrl },\n booted.connection,\n );\n if (!prep.ok) {\n booted.stop();\n family = undefined;\n // SECRET-HANDLING: do NOT surface `prep.error.content` in the thrown\n // message. Some prep error paths build their text from attach\n // components, and the CLI catch writes `e.message` to stderr — embedding\n // that text risks leaking the scheme/relay wss URL. The detailed\n // diagnostic is the daemon/dashboard's job; the factory throws a\n // secret-free message only.\n throw new Error(\n 'createRelayConnectionFactory: attach preparation failed — check the scheme_url and that the relay tunnel is up',\n );\n }\n\n // Render the QR + wait for the phone to attach. SECRET-HANDLING: this\n // function never logs scheme/wss/TOTP values.\n const waitResult = await renderAndMaybeWait(\n attachDeps,\n prep,\n true,\n timeoutMs,\n booted.connection,\n );\n if (waitResult.isError) {\n booted.stop();\n family = undefined;\n // SECRET-HANDLING (BLOCKER fix): `waitResult.content` is the timeout\n // result built from `buildTimeoutError(baseText, ...)`, and `baseText`\n // is `JSON.stringify({ attachUrl, relayUrl, ... })` — `attachUrl` carries\n // the TOTP `at=` code and `relayUrl` is the relay `wss://` URL. The CLI\n // catch writes `e.message` to stderr, so extracting that text here would\n // leak the relay wss + TOTP code on every timeout. Throw a secret-free\n // message with only the timeout duration.\n const timeoutSec = Math.round(timeoutMs / 1000);\n throw new Error(\n `createRelayConnectionFactory: attach timed out after ${timeoutSec}s — phone did not scan the QR within the timeout`,\n );\n }\n\n // Surface the QR/attach render content to the caller, which owns the\n // stdout decision (suppress on non-interactive stdout). There is no\n // fallback that prints here itself: `onQrContent` is a required option so\n // the secret-bearing block (attachUrl TOTP + relayUrl wss) can never be\n // emitted without an explicit caller decision. SECRET-HANDLING.\n const qrChunks = waitResult.content\n .filter((c): c is { type: 'text'; text: string } => c.type === 'text')\n .map((c) => c.text);\n opts.onQrContent(qrChunks);\n\n // Debugger attached — show the on-phone \"Debugger Connected\" badge.\n await injectDebugIndicator(booted.connection);\n\n // Inject the cell globals before any test bundle runs (session-global).\n if (opts.cell !== undefined) {\n await injectGlobals(booted.connection, { __AIT_CELL__: opts.cell });\n }\n\n // Open the CDP client websocket + enable domains so the first run's\n // Runtime.evaluate and console stream are live. enableDomains is\n // idempotent; runTestFilesOverRelay also calls it defensively.\n await booted.connection.enableDomains();\n\n return booted.connection;\n },\n\n async close(_connection: CdpConnection): Promise<void> {\n // family.stop() is synchronous best-effort: closes the CDP connection and\n // shuts down the relay + cloudflared child.\n family?.stop();\n family = undefined;\n // Close the web-QR HTTP server if one was started during open().\n // close() is idempotent via optional chaining + reassignment to undefined.\n await qrServer?.close();\n qrServer = undefined;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AA6GA,SAAgB,6BACd,MACwB;CACxB,MAAM,cAAc,KAAK,eAAe,QAAQ,KAAK;CACrD,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,WAAW,KAAK,aAAa;CAGnC,IAAI;CAEJ,IAAI;AAEJ,QAAO;EACL,MAAM,OAA+B;GAGnC,MAAM,EAAE,eAAe,oBAAoB,kBAAkB,MAAM,OACjE;GAEF,MAAM,EAAE,sBAAsB,kBAAkB,MAAM,OAAO;GAC7D,MAAM,EAAE,4BAA4B,MAAM,OAAO;GACjD,MAAM,EAAE,iBAAiB,yBAAyB,MAAM,OAAO;AAM/D,SAAM,wBAAwB,EAAE,aAAa,CAAC;GAE9C,MAAM,SAAS,MAAM,gBAAgB,EAAE,YAAY,sBAAsB,EAAE,CAAC;AAC5E,YAAS;GAKT,IAAI;GAIJ,MAAM,aAAyB;IAC7B,iBAAiB,OAAO,2BAA2B;KAAE,IAAI;KAAO,QAAQ;KAAM;IAC9E,qBAAqB,QAAQ,IAAI;IACjC,cAAc,KAAA;IACd,kBAAkB,KAAA;IAClB,sBAAsB,CAAC;IACxB;AAcD,OAAI;IACF,MAAM,EAAE,sBAAsB,MAAM,OAAO;IAE3C,MAAM,2BAA2B;KAC/B,QAAQ,WAAW,iBAAiB;KACpC,OAAO;KACP,WAAW,kBAAkB,cAAc,YAAY,gBAAgB,GAAG;KAC1E,MAAM;KACP;AAED,eAAW,MAAM,kBAAkB,kBAAkB;AAIrD,eAAW,eAAe;AAI1B,eAAW,oBAAoB,UAA0B;AACvD,uBAAkB;AAClB,eAAU,mBAAmB;;AAK/B,YAAQ,OAAO,MAAM,iDAAiD,SAAS,KAAK,KAAK;WACnF;GAMR,MAAM,OAAO,MAAM,cACjB,YACA,aACA,EAAE,YAAY,KAAK,WAAW,EAC9B,OAAO,WACR;AACD,OAAI,CAAC,KAAK,IAAI;AACZ,WAAO,MAAM;AACb,aAAS,KAAA;AAOT,UAAM,IAAI,MACR,iHACD;;GAKH,MAAM,aAAa,MAAM,mBACvB,YACA,MACA,MACA,WACA,OAAO,WACR;AACD,OAAI,WAAW,SAAS;AACtB,WAAO,MAAM;AACb,aAAS,KAAA;IAQT,MAAM,aAAa,KAAK,MAAM,YAAY,IAAK;AAC/C,UAAM,IAAI,MACR,wDAAwD,WAAW,kDACpE;;GAQH,MAAM,WAAW,WAAW,QACzB,QAAQ,MAA2C,EAAE,SAAS,OAAO,CACrE,KAAK,MAAM,EAAE,KAAK;AACrB,QAAK,YAAY,SAAS;AAG1B,SAAM,qBAAqB,OAAO,WAAW;AAG7C,OAAI,KAAK,SAAS,KAAA,EAChB,OAAM,cAAc,OAAO,YAAY,EAAE,cAAc,KAAK,MAAM,CAAC;AAMrE,SAAM,OAAO,WAAW,eAAe;AAEvC,UAAO,OAAO;;EAGhB,MAAM,MAAM,aAA2C;AAGrD,WAAQ,MAAM;AACd,YAAS,KAAA;AAGT,SAAM,UAAU,OAAO;AACvB,cAAW,KAAA;;EAEd"}
1
+ {"version":3,"file":"relay-factory.js","names":[],"sources":["../../src/test-runner/relay-factory.ts"],"sourcesContent":["/**\n * Relay connection factory for the Vitest custom pool (devtools#696).\n *\n * The Vitest pool (`pool.ts`) takes a {@link RelayConnectionFactory} that knows\n * how to `open()` a live CDP relay connection (boot relay → QR → phone scan →\n * cell inject → enableDomains) and `close()` it. Before this module that exact\n * assembly lived only inside the `devtools-test` CLI's `main()`; the standalone\n * CLI and the Vitest pool would otherwise each hand-roll it and drift.\n *\n * This is the single source of that assembly. `cli.ts` is refactored to call\n * `createRelayConnectionFactory(...).open()`, and `definePhoneVitestConfig`\n * (config.ts) exposes it so a downstream `vitest.config.ts` can wire the pool:\n *\n * import { createRelayConnectionFactory } from '@ait-co/devtools/test-runner';\n * const connection = createRelayConnectionFactory({\n * schemeUrl,\n * cell,\n * // REQUIRED — own the stdout decision (the chunks carry the relay wss +\n * // TOTP code). Suppress on non-interactive stdout; print otherwise.\n * onQrContent: (chunks) => {\n * if (!process.stdout.isTTY) return;\n * for (const c of chunks) process.stdout.write(`${c}\\n`);\n * },\n * });\n * export default defineConfig({ test: definePhoneVitestConfig({ connection }) });\n *\n * The heavy boot graph (chii relay, cloudflared, ws, debug-server) is pulled via\n * DYNAMIC import inside `open()` — so merely importing this module (or the\n * `@ait-co/devtools/test-runner` barrel) does NOT statically drag that graph in.\n *\n * SECRET-HANDLING: relay wss URLs, scheme URLs, and the TOTP secret/code are\n * never logged. `open()` reads the project-local `.ait_relay` secret read-only\n * (never mints) and the minted TOTP code rides only inside the QR `at=` param.\n *\n * Node-only. react-free (CdpConnection + lazily-imported MCP boot helpers only).\n */\n\nimport type { AttachDeps, AttachUrlParts } from '../mcp/attach-orchestrator.js';\nimport type { CdpConnection } from '../mcp/cdp-connection.js';\nimport type { QrHttpServer } from '../mcp/qr-http-server.js';\nimport type { RelayConnectionFactory } from './pool.js';\n\n// NOTE: every value import below is a DYNAMIC import inside `open()`. This module\n// keeps ONLY type-level static imports so that re-exporting it from the\n// `@ait-co/devtools/test-runner` barrel (config.ts) does NOT statically drag the\n// heavy MCP graph (cell.ts → attach-orchestrator.ts → tools.ts → server-lock,\n// plus chii/cloudflared via debug-server) onto that Node-config entry.\n\n/** Options for {@link createRelayConnectionFactory}. */\nexport interface RelayConnectionFactoryOptions {\n /**\n * intoss-private:// scheme URL from `ait deploy --scheme-only` (env3). The\n * phone cold-loads the candidate bundle this URL points at. SECRET-HANDLING:\n * never logged.\n */\n schemeUrl: string;\n /**\n * Project root for the `.ait_relay` secret lookup (read-only). Defaults to\n * `process.cwd()`.\n */\n projectRoot?: string;\n /**\n * Attach wait timeout in ms — how long `open()` waits for the phone to scan\n * the QR and attach. Defaults to 600 000 (10 min) to give time for a manual\n * scan. (The per-file evaluate timeout is separate, passed via the pool's\n * `run` options.)\n */\n timeoutMs?: number;\n /** Disable browser auto-open of the QR dashboard (text QR only). */\n headless?: boolean;\n /**\n * Cell axes injected as `__AIT_CELL__` before the first test bundle runs, so\n * sdk-example's capture picks up the correct sdkLine/platform. Optional — when\n * omitted no cell is injected. The values are not secrets.\n */\n cell?: { sdkLine: string; platform: string };\n /**\n * Receives the QR/attach render content (text chunks) from\n * `renderAndMaybeWait`, so the caller decides whether to print them — e.g.\n * suppress on non-interactive stdout.\n *\n * REQUIRED (not optional) on purpose: these chunks contain the QR payload\n * (which encodes the relay wss + TOTP `at=` code) and the attach JSON block.\n * Making the hook mandatory means the factory never falls back to printing\n * them itself — a downstream `vitest.config.ts` consumer that wires this via\n * the `@ait-co/devtools/test-runner` barrel is forced to make an explicit\n * stdout decision rather than silently leaking the secret-bearing block.\n *\n * SECRET-HANDLING: when a non-interactive caller is detected the hook MUST\n * suppress the WHOLE chunk (not just `attachUrl`), since `relayUrl` rides in\n * the same block.\n */\n onQrContent: (textChunks: string[]) => void;\n}\n\n/**\n * Builds a {@link RelayConnectionFactory} that opens a standalone env3 relay\n * connection.\n *\n * `open()` performs the full attach lifecycle and BLOCKS for tens of seconds (up\n * to `timeoutMs`) while a human scans the rendered QR with their phone — there\n * is no way around the manual scan for env3. It resolves with the live\n * `CdpConnection` once a matching page attaches; `close()` tears the relay\n * family down.\n *\n * The factory holds the booted relay family in a closure so `close()` can stop\n * it. A second `open()` on the same factory boots a fresh family (the previous\n * one should have been `close()`d first).\n */\nexport function createRelayConnectionFactory(\n opts: RelayConnectionFactoryOptions,\n): RelayConnectionFactory {\n const projectRoot = opts.projectRoot ?? process.cwd();\n const timeoutMs = opts.timeoutMs ?? 600_000;\n const headless = opts.headless === true;\n\n // Captured so close() can stop the family that open() booted.\n let family: { connection: CdpConnection; stop(): void } | undefined;\n // QR HTTP server — started during open() when not headless, closed in close().\n let qrServer: QrHttpServer | undefined;\n\n return {\n async open(): Promise<CdpConnection> {\n // Dynamic imports: keep the chii/cloudflared/debug-server graph OFF the\n // static import graph of this module (and the test-runner config barrel).\n const { prepareAttach, renderAndMaybeWait, mintAttachUrl } = await import(\n '../mcp/attach-orchestrator.js'\n );\n const { injectDebugIndicator, injectGlobals } = await import('./cell.js');\n const { loadRelaySecretReadOnly } = await import('../mcp/relay-secret-store.js');\n const { bootRelayFamily, buildRelayVerifyAuth } = await import('../mcp/debug-server.js');\n\n // Load the project-local .ait_relay secret into AIT_DEBUG_TOTP_SECRET\n // BEFORE booting the relay so assertRelayAuthConfigured()/buildRelayVerifyAuth()\n // at the boot site see it. Read-only — never mints. SECRET-HANDLING: the\n // value is never logged here.\n await loadRelaySecretReadOnly({ projectRoot });\n\n // PRIMARY FIX (devtools#714): bootRelayFamily starts the cloudflared tunnel\n // as a background promise and returns immediately with tunnel.up === false.\n // We must NOT call prepareAttach until the tunnel is up — it fails fast when\n // tunnel.up is false (attach-orchestrator.ts tunnel-down guard).\n //\n // Wire onWssUrl (mirroring the MCP daemon path in debug-server.ts) to:\n // 1. resolve a caller-held tunnel-ready promise so open() can await it, and\n // 2. re-push dashboard SSE on late tunnel-up events (notifyStateChange).\n //\n // SECRET-HANDLING: onWssUrl receives the relay wss URL — never log it.\n let resolveTunnelUp!: () => void;\n const tunnelReady = new Promise<void>((resolve) => {\n resolveTunnelUp = resolve;\n });\n\n const booted = await bootRelayFamily({\n verifyAuth: buildRelayVerifyAuth(),\n onWssUrl: () => {\n // Resolve the tunnel-ready gate so the prepareAttach call below is\n // unblocked. qrServer may not be set yet at call time (it's started\n // after bootRelayFamily), so we use optional chaining — if the tunnel\n // happens to come up after qrServer is started, notifyStateChange pushes\n // the freshly minted attachUrl to any waiting dashboard SSE clients.\n // SECRET-HANDLING: wssUrl is NOT forwarded here — the value travels only\n // inside the closure via getTunnelStatus().wssUrl (used by mintAttachUrl).\n resolveTunnelUp();\n qrServer?.notifyStateChange();\n },\n });\n family = booted;\n\n // If the tunnel is already up (extremely fast boot or test double), resolve\n // immediately so we don't stall on a promise that will never fire.\n if (booted.getTunnelStatus?.().up) {\n resolveTunnelUp();\n }\n\n // Track the last-captured attach parts so getDashboardState can mint a\n // fresh attach URL on every dashboard request/SSE push (fresh TOTP at=).\n // SECRET-HANDLING: parts contain the relay wss + scheme URL — never logged.\n let lastAttachParts: AttachUrlParts | undefined;\n\n // Assemble AttachDeps. We set qrHttpServer and onAttachUrlBuilt below\n // (before prepareAttach) after optionally starting the web-QR server.\n const attachDeps: AttachDeps = {\n getTunnelStatus: booted.getTunnelStatus ?? (() => ({ up: false, wssUrl: null })),\n getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,\n qrHttpServer: undefined, // filled in below if web-QR server started\n onAttachUrlBuilt: undefined, // filled in below\n canOpenBrowser: () => !headless,\n };\n\n // Web-QR server: reuse the same loopback HTTP dashboard that the MCP\n // start_attach path uses (src/mcp/qr-http-server.ts). This makes the QR\n // scannable even when stdout is non-interactive (Claude Code `!` / CI),\n // because the browser is opened by URL — not via captured stdout.\n //\n // Headless decision: we start the server even in --headless mode so the\n // printed stderr URL can be opened manually. The existing\n // canOpenBrowser: () => !headless gate inside renderAndMaybeWait prevents\n // the auto-open; headless users see only the stderr URL.\n //\n // On failure we fall back gracefully to the text-QR path — do NOT crash.\n // SECRET-HANDLING: only http://127.0.0.1:<port>/ (no secrets) goes to stderr.\n try {\n const { startQrHttpServer } = await import('../mcp/qr-http-server.js');\n\n const getDashboardState = () => ({\n tunnel: attachDeps.getTunnelStatus(),\n pages: null, // CLI/pool: no page-list introspection needed\n attachUrl: lastAttachParts ? mintAttachUrl(attachDeps, lastAttachParts) : null,\n mode: 'relay-dev' as const,\n });\n\n qrServer = await startQrHttpServer(getDashboardState);\n\n // Wire the QR server into attachDeps BEFORE prepareAttach is called so\n // renderAndMaybeWait sees it and takes Path 2/3 (web-QR) instead of Path 4.\n attachDeps.qrHttpServer = qrServer;\n\n // Capture attach parts via onAttachUrlBuilt so getDashboardState can\n // mint a fresh URL (fresh TOTP at= code) on every dashboard render.\n attachDeps.onAttachUrlBuilt = (parts: AttachUrlParts) => {\n lastAttachParts = parts;\n qrServer?.notifyStateChange();\n };\n\n // Print the loopback dashboard URL to stderr — it carries no secrets\n // (TOTP codes and relay wss live only in the in-memory HTTP response).\n process.stderr.write(`devtools-test: QR dashboard: http://127.0.0.1:${qrServer.port}/\\n`);\n } catch {\n // startQrHttpServer failed (e.g. port conflict, import error). Fall back\n // to the existing text-QR path by leaving qrHttpServer: undefined.\n // qrServer remains undefined; close() handles that with optional chaining.\n }\n\n // PRIMARY FIX (devtools#714): await tunnel readiness before calling\n // prepareAttach. Race: a 15 s timeout is generous — cloudflared typically\n // comes up in < 5 s. On timeout we still call prepareAttach; it will hit\n // the tunnel-down guard and throw a secret-free error (same as before,\n // but now with a clear diagnostic instead of a silent WAITING freeze).\n //\n // Implementation: Promise.race against a 15 000 ms timeout signal. We do\n // NOT use a timer-based early-exit because the existing code already\n // surface-fails on tunnel-down inside prepareAttach with a secret-free\n // message. The timeout here is purely a \"give the tunnel a fair chance\"\n // gate — not a correctness boundary.\n const TUNNEL_BOOT_TIMEOUT_MS = 15_000;\n await Promise.race([\n tunnelReady,\n new Promise<void>((resolve) => setTimeout(resolve, TUNNEL_BOOT_TIMEOUT_MS)),\n ]);\n\n const prep = await prepareAttach(\n attachDeps,\n 'relay-dev',\n { scheme_url: opts.schemeUrl },\n booted.connection,\n );\n if (!prep.ok) {\n booted.stop();\n family = undefined;\n // SECONDARY FIX (devtools#714): close the QR server on the failure path\n // so the loopback port listener does not leak. The normal-exit path is\n // handled by close(); this mirrors that cleanup for the error path.\n await qrServer?.close();\n qrServer = undefined;\n // SECRET-HANDLING: do NOT surface `prep.error.content` in the thrown\n // message. Some prep error paths build their text from attach\n // components, and the CLI catch writes `e.message` to stderr — embedding\n // that text risks leaking the scheme/relay wss URL. The detailed\n // diagnostic is the daemon/dashboard's job; the factory throws a\n // secret-free message only.\n throw new Error(\n 'createRelayConnectionFactory: attach preparation failed — check the scheme_url and that the relay tunnel is up',\n );\n }\n\n // Render the QR + wait for the phone to attach. SECRET-HANDLING: this\n // function never logs scheme/wss/TOTP values.\n const waitResult = await renderAndMaybeWait(\n attachDeps,\n prep,\n true,\n timeoutMs,\n booted.connection,\n );\n if (waitResult.isError) {\n booted.stop();\n family = undefined;\n // SECONDARY FIX (devtools#714): close the QR server on the timeout path\n // (mirrors the prep.ok failure path above).\n await qrServer?.close();\n qrServer = undefined;\n // SECRET-HANDLING (BLOCKER fix): `waitResult.content` is the timeout\n // result built from `buildTimeoutError(baseText, ...)`, and `baseText`\n // is `JSON.stringify({ attachUrl, relayUrl, ... })` — `attachUrl` carries\n // the TOTP `at=` code and `relayUrl` is the relay `wss://` URL. The CLI\n // catch writes `e.message` to stderr, so extracting that text here would\n // leak the relay wss + TOTP code on every timeout. Throw a secret-free\n // message with only the timeout duration.\n const timeoutSec = Math.round(timeoutMs / 1000);\n throw new Error(\n `createRelayConnectionFactory: attach timed out after ${timeoutSec}s — phone did not scan the QR within the timeout`,\n );\n }\n\n // Surface the QR/attach render content to the caller, which owns the\n // stdout decision (suppress on non-interactive stdout). There is no\n // fallback that prints here itself: `onQrContent` is a required option so\n // the secret-bearing block (attachUrl TOTP + relayUrl wss) can never be\n // emitted without an explicit caller decision. SECRET-HANDLING.\n const qrChunks = waitResult.content\n .filter((c): c is { type: 'text'; text: string } => c.type === 'text')\n .map((c) => c.text);\n opts.onQrContent(qrChunks);\n\n // Debugger attached — show the on-phone \"Debugger Connected\" badge.\n await injectDebugIndicator(booted.connection);\n\n // Inject the cell globals before any test bundle runs (session-global).\n if (opts.cell !== undefined) {\n await injectGlobals(booted.connection, { __AIT_CELL__: opts.cell });\n }\n\n // Open the CDP client websocket + enable domains so the first run's\n // Runtime.evaluate and console stream are live. enableDomains is\n // idempotent; runTestFilesOverRelay also calls it defensively.\n await booted.connection.enableDomains();\n\n return booted.connection;\n },\n\n async close(_connection: CdpConnection): Promise<void> {\n // family.stop() is synchronous best-effort: closes the CDP connection and\n // shuts down the relay + cloudflared child.\n family?.stop();\n family = undefined;\n // Close the web-QR HTTP server if one was started during open().\n // close() is idempotent via optional chaining + reassignment to undefined.\n await qrServer?.close();\n qrServer = undefined;\n },\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;AA6GA,SAAgB,6BACd,MACwB;CACxB,MAAM,cAAc,KAAK,eAAe,QAAQ,KAAK;CACrD,MAAM,YAAY,KAAK,aAAa;CACpC,MAAM,WAAW,KAAK,aAAa;CAGnC,IAAI;CAEJ,IAAI;AAEJ,QAAO;EACL,MAAM,OAA+B;GAGnC,MAAM,EAAE,eAAe,oBAAoB,kBAAkB,MAAM,OACjE;GAEF,MAAM,EAAE,sBAAsB,kBAAkB,MAAM,OAAO;GAC7D,MAAM,EAAE,4BAA4B,MAAM,OAAO;GACjD,MAAM,EAAE,iBAAiB,yBAAyB,MAAM,OAAO;AAM/D,SAAM,wBAAwB,EAAE,aAAa,CAAC;GAY9C,IAAI;GACJ,MAAM,cAAc,IAAI,SAAe,YAAY;AACjD,sBAAkB;KAClB;GAEF,MAAM,SAAS,MAAM,gBAAgB;IACnC,YAAY,sBAAsB;IAClC,gBAAgB;AAQd,sBAAiB;AACjB,eAAU,mBAAmB;;IAEhC,CAAC;AACF,YAAS;AAIT,OAAI,OAAO,mBAAmB,CAAC,GAC7B,kBAAiB;GAMnB,IAAI;GAIJ,MAAM,aAAyB;IAC7B,iBAAiB,OAAO,2BAA2B;KAAE,IAAI;KAAO,QAAQ;KAAM;IAC9E,qBAAqB,QAAQ,IAAI;IACjC,cAAc,KAAA;IACd,kBAAkB,KAAA;IAClB,sBAAsB,CAAC;IACxB;AAcD,OAAI;IACF,MAAM,EAAE,sBAAsB,MAAM,OAAO;IAE3C,MAAM,2BAA2B;KAC/B,QAAQ,WAAW,iBAAiB;KACpC,OAAO;KACP,WAAW,kBAAkB,cAAc,YAAY,gBAAgB,GAAG;KAC1E,MAAM;KACP;AAED,eAAW,MAAM,kBAAkB,kBAAkB;AAIrD,eAAW,eAAe;AAI1B,eAAW,oBAAoB,UAA0B;AACvD,uBAAkB;AAClB,eAAU,mBAAmB;;AAK/B,YAAQ,OAAO,MAAM,iDAAiD,SAAS,KAAK,KAAK;WACnF;GAiBR,MAAM,yBAAyB;AAC/B,SAAM,QAAQ,KAAK,CACjB,aACA,IAAI,SAAe,YAAY,WAAW,SAAS,uBAAuB,CAAC,CAC5E,CAAC;GAEF,MAAM,OAAO,MAAM,cACjB,YACA,aACA,EAAE,YAAY,KAAK,WAAW,EAC9B,OAAO,WACR;AACD,OAAI,CAAC,KAAK,IAAI;AACZ,WAAO,MAAM;AACb,aAAS,KAAA;AAIT,UAAM,UAAU,OAAO;AACvB,eAAW,KAAA;AAOX,UAAM,IAAI,MACR,iHACD;;GAKH,MAAM,aAAa,MAAM,mBACvB,YACA,MACA,MACA,WACA,OAAO,WACR;AACD,OAAI,WAAW,SAAS;AACtB,WAAO,MAAM;AACb,aAAS,KAAA;AAGT,UAAM,UAAU,OAAO;AACvB,eAAW,KAAA;IAQX,MAAM,aAAa,KAAK,MAAM,YAAY,IAAK;AAC/C,UAAM,IAAI,MACR,wDAAwD,WAAW,kDACpE;;GAQH,MAAM,WAAW,WAAW,QACzB,QAAQ,MAA2C,EAAE,SAAS,OAAO,CACrE,KAAK,MAAM,EAAE,KAAK;AACrB,QAAK,YAAY,SAAS;AAG1B,SAAM,qBAAqB,OAAO,WAAW;AAG7C,OAAI,KAAK,SAAS,KAAA,EAChB,OAAM,cAAc,OAAO,YAAY,EAAE,cAAc,KAAK,MAAM,CAAC;AAMrE,SAAM,OAAO,WAAW,eAAe;AAEvC,UAAO,OAAO;;EAGhB,MAAM,MAAM,aAA2C;AAGrD,WAAQ,MAAM;AACd,YAAS,KAAA;AAGT,SAAM,UAAU,OAAO;AACvB,cAAW,KAAA;;EAEd"}
@@ -0,0 +1,3 @@
1
+ #!/usr/bin/env node
2
+ import { a as isValidRelayAuthSecret } from "./totp-WY6l0ysP.js";
3
+ export { isValidRelayAuthSecret };
@@ -187,4 +187,4 @@ function buildRelayVerifyAuth(env = process.env) {
187
187
  //#endregion
188
188
  export { isValidRelayAuthSecret as a, generateTotp as i, assertRelayAuthConfigured as n, verifyTotp as o, buildRelayVerifyAuth as r, RELAY_AUTH_SECRET_MISSING_MESSAGE as t };
189
189
 
190
- //# sourceMappingURL=totp-0bvlo9Yb.js.map
190
+ //# sourceMappingURL=totp-WY6l0ysP.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"totp-0bvlo9Yb.js","names":[],"sources":["../src/mcp/totp.ts"],"sourcesContent":["/**\n * RFC 6238 TOTP implementation (Node.js, node:crypto only).\n *\n * External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used\n * to keep the dependency surface minimal. This hand-roll is ~30 lines and\n * covers exactly what relay-side auth needs.\n *\n * Algorithm summary (RFC 6238 + RFC 4226):\n * T = floor(now / 30) — 30-second time step counter\n * K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)\n * MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)\n * offset = MAC[19] & 0x0f\n * code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits\n *\n * Security note (keep this comment accurate):\n * The baked-in secret in a dog-food build is extractable from the bundle by a\n * determined reverse engineer. This mechanism raises the bar from\n * \"anyone with the URL\" to \"URL + bundle extraction + live TOTP calculation\".\n * Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are\n * blocked; deliberate reverse engineering is not. See threat model in\n * src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.\n *\n * SECRET-HANDLING: secret values and computed codes MUST NOT appear in any\n * log, error message, or string visible outside this module. Only boolean\n * pass/fail and reason enum values are safe to surface.\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** Time step window in seconds (RFC 6238 default). */\nconst TIME_STEP = 30;\n\n/** Number of digits in the generated code. */\nconst DIGITS = 6;\n\n/**\n * Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-\n * clock time.\n *\n * @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32\n * bytes). Must be the output of `generateAttachToken()` or compatible.\n * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.\n * @returns A zero-padded 6-digit decimal string, e.g. `\"042193\"`.\n */\nexport function generateTotp(secret: string, when: number = Date.now()): string {\n const key = Buffer.from(secret, 'hex');\n // Clamp to 0 so negative timestamps (e.g. in ±skew checks near epoch) do not\n // produce a negative counter, which would cause writeUInt32BE to throw.\n const counter = Math.max(0, Math.floor(when / 1000 / TIME_STEP));\n\n // Encode counter as 8-byte big-endian unsigned integer.\n const counterBuf = Buffer.alloc(8);\n // JavaScript numbers are safe integers up to 2^53; counter is ~7.5×10^10 at\n // year 9999 — well within safe range so standard bitwise ops are fine.\n const hi = Math.floor(counter / 0x100000000);\n const lo = counter >>> 0;\n counterBuf.writeUInt32BE(hi, 0);\n counterBuf.writeUInt32BE(lo, 4);\n\n const mac = createHmac('sha1', key).update(counterBuf).digest();\n\n // Dynamic truncation (RFC 4226 §5.4).\n const offset = mac[19] & 0x0f;\n const binCode =\n ((mac[offset] & 0x7f) << 24) |\n ((mac[offset + 1] & 0xff) << 16) |\n ((mac[offset + 2] & 0xff) << 8) |\n (mac[offset + 3] & 0xff);\n\n const otp = binCode % 10 ** DIGITS;\n return otp.toString().padStart(DIGITS, '0');\n}\n\n/**\n * Verifies a TOTP code against the secret, accepting ±`skew` time steps to\n * tolerate clock drift between the relay host and the client device.\n *\n * Uses `timingSafeEqual` for constant-time comparison to prevent timing\n * side-channel attacks.\n *\n * @param secret - Hex-encoded shared secret.\n * @param code - The 6-digit code to verify (string or numeric).\n * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.\n * @param skew - Number of adjacent steps to accept on either side. Default 1\n * (accepts T-1, T, T+1 — a 90-second acceptance window).\n * @returns `true` if the code matches any accepted step, `false` otherwise.\n */\nexport function verifyTotp(\n secret: string,\n code: string,\n when: number = Date.now(),\n skew: number = 1,\n): boolean {\n const normalised = String(code).padStart(DIGITS, '0');\n if (normalised.length !== DIGITS || !/^\\d{6}$/.test(normalised)) {\n return false;\n }\n\n const candidateBuf = Buffer.from(normalised, 'utf8');\n\n for (let delta = -skew; delta <= skew; delta++) {\n const stepWhen = when + delta * TIME_STEP * 1000;\n const expected = generateTotp(secret, stepWhen);\n const expectedBuf = Buffer.from(expected, 'utf8');\n if (timingSafeEqual(expectedBuf, candidateBuf)) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Minimum length (in hex characters) accepted for `AIT_DEBUG_TOTP_SECRET`.\n *\n * The secret is hex-encoded (see {@link generateTotp} — `Buffer.from(secret,\n * 'hex')`). 32 hex chars = 16 bytes = 128 bits, the floor for an HMAC-SHA1 key\n * we are willing to gate a public relay behind. `generateAttachToken()` emits\n * 64 hex chars (32 bytes), comfortably above this bar.\n */\nconst MIN_SECRET_HEX_CHARS = 32;\n\n/** Hex string: one or more hex digits, case-insensitive (RFC 4648 base16). */\nconst HEX_RE = /^[0-9a-fA-F]+$/;\n\n/**\n * Human-facing guidance printed when {@link assertRelayAuthConfigured} fails.\n *\n * SECRET-HANDLING: this message states only the REQUIREMENT (≥32 hex chars) and\n * how to mint one. It NEVER echoes the configured value, its length, or any\n * fragment derived from it — see {@link assertRelayAuthConfigured}.\n *\n * Note on encoding: the secret is hex (base16), not base32 — `generateTotp`\n * decodes it with `Buffer.from(secret, 'hex')`. A base32 string would be\n * silently mis-decoded and every TOTP code would fail to match, so the minting\n * command emits hex.\n */\nexport const RELAY_AUTH_SECRET_MISSING_MESSAGE = [\n '[ait-debug] AIT_DEBUG_TOTP_SECRET이 필수입니다. 32자 이상 16진수(hex) 문자열을 설정하세요.',\n '발급: openssl rand -hex 32',\n '데몬은 start_debug의 projectRoot 인자로 받은 디렉토리에서 .ait_relay 파일을 읽어 이 시크릿을 채웁니다.',\n '프로젝트에서 pnpm dev:phone:cdp를 한 번 띄우면 unplugin이 .ait_relay를 자동 생성하니(tunnel.cdp 옵션 필요), projectRoot를 전달하세요.',\n '자세히: https://docs.aitc.dev/guides/relay-auth-totp',\n].join('\\n');\n\n/**\n * Whether `secret` is a well-formed relay-auth TOTP secret: a hex string of at\n * least {@link MIN_SECRET_HEX_CHARS} characters with an even length (an odd\n * length would have its trailing nibble silently dropped by `Buffer.from(...,\n * 'hex')`, weakening the key without warning).\n *\n * Pure predicate so callers can test the validation independently of the\n * fail-fast side effect in {@link assertRelayAuthConfigured}.\n *\n * SECRET-HANDLING: returns only a boolean — the input value is never returned,\n * logged, or echoed.\n */\nexport function isValidRelayAuthSecret(secret: string | undefined): secret is string {\n if (secret === undefined || secret === '') return false;\n if (secret.length < MIN_SECRET_HEX_CHARS) return false;\n if (secret.length % 2 !== 0) return false;\n return HEX_RE.test(secret);\n}\n\n/**\n * Fail-fast guard enforcing that a relay-auth TOTP secret is configured before\n * a public-internet-exposed relay is booted (issue #250).\n *\n * Relay-auth (the §4 Layer C TOTP gate) is the only fail-fast layer that closes\n * the real gap: a leaked `wss://…trycloudflare.com` URL otherwise lets a third\n * party attach a debugger to a dog-food/live mini-app. Without a secret the relay\n * comes up unauthenticated, so this guard is called at every relay-boot site —\n * `bootRelayFamily` (intoss env 3/4) and `bootExternalRelayFamily` (env-2 PWA),\n * both eager and lazy. Local-only sessions never boot a relay and so never reach\n * this guard, matching the issue's exemption for non-relay debugging.\n *\n * Throws when the secret is unset, empty, too short, or not a valid hex string.\n * The thrown message is the bin entry's fatal stderr (see `cli.ts` `main().catch`)\n * — the same fatal model as the missing-`AIT_RELAY_BASE_URL` path.\n *\n * SECRET-HANDLING: the env value is read once, passed ONLY to the boolean\n * predicate, and never logged. The thrown message names the requirement, never\n * the value, its length, or any derived fragment.\n *\n * @param env - Environment to read from. Defaults to `process.env`; injectable\n * for tests so they never mutate the real process environment.\n */\nexport function assertRelayAuthConfigured(env: NodeJS.ProcessEnv = process.env): void {\n if (!isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) {\n throw new Error(RELAY_AUTH_SECRET_MISSING_MESSAGE);\n }\n}\n\n/**\n * Gate-specific skew for the relay WebSocket upgrade TOTP check.\n *\n * Rationale (why 6, not the RFC default of 1):\n * - Each step is 30 s, so ±6 steps = past 6 steps accepted = 180–210 s of\n * backwards acceptance. This means a code generated at issuance time is\n * guaranteed valid for at least 3 minutes (180 s) after it was minted.\n * - The real-world attach flow (QR issued on desktop → developer picks up\n * phone → camera scan → launcher PWA loads → attach) routinely exceeds\n * the 90 s window of the RFC default (skew=1), especially when the launcher\n * PWA needs to reinstall or when the phone is not immediately at hand.\n * - Expanding to ~3.5 min reachability is acceptable under the §4 threat\n * model: the adversary we guard against is \"someone who got the URL but\n * does NOT have the secret\". Without the secret they cannot compute a TOTP\n * code regardless of the window size — security theater is explicitly\n * forbidden by the project principle. An attacker WITH the secret (bundle\n * extractor) is out of scope per CLAUDE.md §4.\n *\n * `verifyTotp`'s own default (skew=1) is deliberately left unchanged — it is\n * the RFC primitive. Only this relay-gate call site is widened.\n */\nexport const RELAY_VERIFY_SKEW_STEPS = 6;\n\n/**\n * Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a\n * `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.\n *\n * The predicate checks the `at` query parameter against the current and\n * adjacent TOTP time steps (±{@link RELAY_VERIFY_SKEW_STEPS} skew) using\n * {@link verifyTotp}. This gives the issued code a minimum validity of ~3\n * minutes, which is enough to cover the QR-scan → launcher-attach flow even\n * when the launcher PWA needs to load or reinstall (#490).\n *\n * Returns `undefined` when the env var is not set — callers treat that as\n * \"auth disabled\" (no predicate registered on the relay). Note that since\n * issue #250 the secret is MANDATORY at every relay-boot site (enforced by\n * {@link assertRelayAuthConfigured} BEFORE the relay starts), so in production\n * this never returns `undefined` for a relay that actually boots; the\n * `undefined` branch only matters for the no-relay local path and tests.\n *\n * Lives here (not in the MCP server) so the unplugin's env-2 relay can wire the\n * same gate without importing the heavy MCP server module graph. Re-exported\n * from `debug-server.ts` for back-compat.\n *\n * SECRET-HANDLING: The secret value read from env is captured in a closure and\n * is NEVER written to any log, error message, or process output.\n */\nexport function buildRelayVerifyAuth(\n env: NodeJS.ProcessEnv = process.env,\n): ((req: import('node:http').IncomingMessage) => boolean) | undefined {\n const secret = env.AIT_DEBUG_TOTP_SECRET;\n if (!secret) return undefined;\n\n return (req) => {\n // Parse the `at` query param from the upgrade request URL.\n // req.url is the raw request path + query, e.g. `/client/id?target=…&at=123456`\n const rawUrl = req.url ?? '';\n const qIndex = rawUrl.indexOf('?');\n const queryStr = qIndex === -1 ? '' : rawUrl.slice(qIndex + 1);\n const params = new URLSearchParams(queryStr);\n const code = params.get('at') ?? '';\n\n // Do NOT log `code`, `secret`, or any derived value here.\n // Use RELAY_VERIFY_SKEW_STEPS (±6) for a ~3-minute acceptance window (#490).\n // verifyTotp's own default (skew=1) is unchanged — only this call site is widened.\n return verifyTotp(secret, code, undefined, RELAY_VERIFY_SKEW_STEPS);\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,YAAY;;AAGlB,MAAM,SAAS;;;;;;;;;;AAWf,SAAgB,aAAa,QAAgB,OAAe,KAAK,KAAK,EAAU;CAC9E,MAAM,MAAM,OAAO,KAAK,QAAQ,MAAM;CAGtC,MAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,MAAO,UAAU,CAAC;CAGhE,MAAM,aAAa,OAAO,MAAM,EAAE;CAGlC,MAAM,KAAK,KAAK,MAAM,UAAU,WAAY;CAC5C,MAAM,KAAK,YAAY;AACvB,YAAW,cAAc,IAAI,EAAE;AAC/B,YAAW,cAAc,IAAI,EAAE;CAE/B,MAAM,MAAM,WAAW,QAAQ,IAAI,CAAC,OAAO,WAAW,CAAC,QAAQ;CAG/D,MAAM,SAAS,IAAI,MAAM;AAQzB,WANI,IAAI,UAAU,QAAS,MACvB,IAAI,SAAS,KAAK,QAAS,MAC3B,IAAI,SAAS,KAAK,QAAS,IAC5B,IAAI,SAAS,KAAK,OAEC,MAAM,QACjB,UAAU,CAAC,SAAS,QAAQ,IAAI;;;;;;;;;;;;;;;;AAiB7C,SAAgB,WACd,QACA,MACA,OAAe,KAAK,KAAK,EACzB,OAAe,GACN;CACT,MAAM,aAAa,OAAO,KAAK,CAAC,SAAS,QAAQ,IAAI;AACrD,KAAI,WAAW,WAAW,UAAU,CAAC,UAAU,KAAK,WAAW,CAC7D,QAAO;CAGT,MAAM,eAAe,OAAO,KAAK,YAAY,OAAO;AAEpD,MAAK,IAAI,QAAQ,CAAC,MAAM,SAAS,MAAM,SAAS;EAE9C,MAAM,WAAW,aAAa,QADb,OAAO,QAAQ,YAAY,IACG;AAE/C,MAAI,gBADgB,OAAO,KAAK,UAAU,OAAO,EAChB,aAAa,CAC5C,QAAO;;AAIX,QAAO;;;;;;;;;;AAWT,MAAM,uBAAuB;;AAG7B,MAAM,SAAS;;;;;;;;;;;;;AAcf,MAAa,oCAAoC;CAC/C;CACA;CACA;CACA;CACA;CACD,CAAC,KAAK,KAAK;;;;;;;;;;;;;AAcZ,SAAgB,uBAAuB,QAA8C;AACnF,KAAI,WAAW,KAAA,KAAa,WAAW,GAAI,QAAO;AAClD,KAAI,OAAO,SAAS,qBAAsB,QAAO;AACjD,KAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AACpC,QAAO,OAAO,KAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;AA0B5B,SAAgB,0BAA0B,MAAyB,QAAQ,KAAW;AACpF,KAAI,CAAC,uBAAuB,IAAI,sBAAsB,CACpD,OAAM,IAAI,MAAM,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDtD,SAAgB,qBACd,MAAyB,QAAQ,KACoC;CACrE,MAAM,SAAS,IAAI;AACnB,KAAI,CAAC,OAAQ,QAAO,KAAA;AAEpB,SAAQ,QAAQ;EAGd,MAAM,SAAS,IAAI,OAAO;EAC1B,MAAM,SAAS,OAAO,QAAQ,IAAI;EAClC,MAAM,WAAW,WAAW,KAAK,KAAK,OAAO,MAAM,SAAS,EAAE;AAO9D,SAAO,WAAW,QANH,IAAI,gBAAgB,SAAS,CACxB,IAAI,KAAK,IAAI,IAKD,KAAA,GAAA,EAAmC"}
1
+ {"version":3,"file":"totp-WY6l0ysP.js","names":[],"sources":["../src/mcp/totp.ts"],"sourcesContent":["/**\n * RFC 6238 TOTP implementation (Node.js, node:crypto only).\n *\n * External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used\n * to keep the dependency surface minimal. This hand-roll is ~30 lines and\n * covers exactly what relay-side auth needs.\n *\n * Algorithm summary (RFC 6238 + RFC 4226):\n * T = floor(now / 30) — 30-second time step counter\n * K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)\n * MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)\n * offset = MAC[19] & 0x0f\n * code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits\n *\n * Security note (keep this comment accurate):\n * The baked-in secret in a dog-food build is extractable from the bundle by a\n * determined reverse engineer. This mechanism raises the bar from\n * \"anyone with the URL\" to \"URL + bundle extraction + live TOTP calculation\".\n * Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are\n * blocked; deliberate reverse engineering is not. See threat model in\n * src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.\n *\n * SECRET-HANDLING: secret values and computed codes MUST NOT appear in any\n * log, error message, or string visible outside this module. Only boolean\n * pass/fail and reason enum values are safe to surface.\n */\n\nimport { createHmac, timingSafeEqual } from 'node:crypto';\n\n/** Time step window in seconds (RFC 6238 default). */\nconst TIME_STEP = 30;\n\n/** Number of digits in the generated code. */\nconst DIGITS = 6;\n\n/**\n * Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-\n * clock time.\n *\n * @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32\n * bytes). Must be the output of `generateAttachToken()` or compatible.\n * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.\n * @returns A zero-padded 6-digit decimal string, e.g. `\"042193\"`.\n */\nexport function generateTotp(secret: string, when: number = Date.now()): string {\n const key = Buffer.from(secret, 'hex');\n // Clamp to 0 so negative timestamps (e.g. in ±skew checks near epoch) do not\n // produce a negative counter, which would cause writeUInt32BE to throw.\n const counter = Math.max(0, Math.floor(when / 1000 / TIME_STEP));\n\n // Encode counter as 8-byte big-endian unsigned integer.\n const counterBuf = Buffer.alloc(8);\n // JavaScript numbers are safe integers up to 2^53; counter is ~7.5×10^10 at\n // year 9999 — well within safe range so standard bitwise ops are fine.\n const hi = Math.floor(counter / 0x100000000);\n const lo = counter >>> 0;\n counterBuf.writeUInt32BE(hi, 0);\n counterBuf.writeUInt32BE(lo, 4);\n\n const mac = createHmac('sha1', key).update(counterBuf).digest();\n\n // Dynamic truncation (RFC 4226 §5.4).\n const offset = mac[19] & 0x0f;\n const binCode =\n ((mac[offset] & 0x7f) << 24) |\n ((mac[offset + 1] & 0xff) << 16) |\n ((mac[offset + 2] & 0xff) << 8) |\n (mac[offset + 3] & 0xff);\n\n const otp = binCode % 10 ** DIGITS;\n return otp.toString().padStart(DIGITS, '0');\n}\n\n/**\n * Verifies a TOTP code against the secret, accepting ±`skew` time steps to\n * tolerate clock drift between the relay host and the client device.\n *\n * Uses `timingSafeEqual` for constant-time comparison to prevent timing\n * side-channel attacks.\n *\n * @param secret - Hex-encoded shared secret.\n * @param code - The 6-digit code to verify (string or numeric).\n * @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.\n * @param skew - Number of adjacent steps to accept on either side. Default 1\n * (accepts T-1, T, T+1 — a 90-second acceptance window).\n * @returns `true` if the code matches any accepted step, `false` otherwise.\n */\nexport function verifyTotp(\n secret: string,\n code: string,\n when: number = Date.now(),\n skew: number = 1,\n): boolean {\n const normalised = String(code).padStart(DIGITS, '0');\n if (normalised.length !== DIGITS || !/^\\d{6}$/.test(normalised)) {\n return false;\n }\n\n const candidateBuf = Buffer.from(normalised, 'utf8');\n\n for (let delta = -skew; delta <= skew; delta++) {\n const stepWhen = when + delta * TIME_STEP * 1000;\n const expected = generateTotp(secret, stepWhen);\n const expectedBuf = Buffer.from(expected, 'utf8');\n if (timingSafeEqual(expectedBuf, candidateBuf)) {\n return true;\n }\n }\n\n return false;\n}\n\n/**\n * Minimum length (in hex characters) accepted for `AIT_DEBUG_TOTP_SECRET`.\n *\n * The secret is hex-encoded (see {@link generateTotp} — `Buffer.from(secret,\n * 'hex')`). 32 hex chars = 16 bytes = 128 bits, the floor for an HMAC-SHA1 key\n * we are willing to gate a public relay behind. `generateAttachToken()` emits\n * 64 hex chars (32 bytes), comfortably above this bar.\n */\nconst MIN_SECRET_HEX_CHARS = 32;\n\n/** Hex string: one or more hex digits, case-insensitive (RFC 4648 base16). */\nconst HEX_RE = /^[0-9a-fA-F]+$/;\n\n/**\n * Human-facing guidance printed when {@link assertRelayAuthConfigured} fails.\n *\n * SECRET-HANDLING: this message states only the REQUIREMENT (≥32 hex chars) and\n * how to mint one. It NEVER echoes the configured value, its length, or any\n * fragment derived from it — see {@link assertRelayAuthConfigured}.\n *\n * Note on encoding: the secret is hex (base16), not base32 — `generateTotp`\n * decodes it with `Buffer.from(secret, 'hex')`. A base32 string would be\n * silently mis-decoded and every TOTP code would fail to match, so the minting\n * command emits hex.\n */\nexport const RELAY_AUTH_SECRET_MISSING_MESSAGE = [\n '[ait-debug] AIT_DEBUG_TOTP_SECRET이 필수입니다. 32자 이상 16진수(hex) 문자열을 설정하세요.',\n '발급: openssl rand -hex 32',\n '데몬은 start_debug의 projectRoot 인자로 받은 디렉토리에서 .ait_relay 파일을 읽어 이 시크릿을 채웁니다.',\n '프로젝트에서 pnpm dev:phone:cdp를 한 번 띄우면 unplugin이 .ait_relay를 자동 생성하니(tunnel.cdp 옵션 필요), projectRoot를 전달하세요.',\n '자세히: https://docs.aitc.dev/guides/relay-auth-totp',\n].join('\\n');\n\n/**\n * Whether `secret` is a well-formed relay-auth TOTP secret: a hex string of at\n * least {@link MIN_SECRET_HEX_CHARS} characters with an even length (an odd\n * length would have its trailing nibble silently dropped by `Buffer.from(...,\n * 'hex')`, weakening the key without warning).\n *\n * Pure predicate so callers can test the validation independently of the\n * fail-fast side effect in {@link assertRelayAuthConfigured}.\n *\n * SECRET-HANDLING: returns only a boolean — the input value is never returned,\n * logged, or echoed.\n */\nexport function isValidRelayAuthSecret(secret: string | undefined): secret is string {\n if (secret === undefined || secret === '') return false;\n if (secret.length < MIN_SECRET_HEX_CHARS) return false;\n if (secret.length % 2 !== 0) return false;\n return HEX_RE.test(secret);\n}\n\n/**\n * Fail-fast guard enforcing that a relay-auth TOTP secret is configured before\n * a public-internet-exposed relay is booted (issue #250).\n *\n * Relay-auth (the §4 Layer C TOTP gate) is the only fail-fast layer that closes\n * the real gap: a leaked `wss://…trycloudflare.com` URL otherwise lets a third\n * party attach a debugger to a dog-food/live mini-app. Without a secret the relay\n * comes up unauthenticated, so this guard is called at every relay-boot site —\n * `bootRelayFamily` (intoss env 3/4) and `bootExternalRelayFamily` (env-2 PWA),\n * both eager and lazy. Local-only sessions never boot a relay and so never reach\n * this guard, matching the issue's exemption for non-relay debugging.\n *\n * Throws when the secret is unset, empty, too short, or not a valid hex string.\n * The thrown message is the bin entry's fatal stderr (see `cli.ts` `main().catch`)\n * — the same fatal model as the missing-`AIT_RELAY_BASE_URL` path.\n *\n * SECRET-HANDLING: the env value is read once, passed ONLY to the boolean\n * predicate, and never logged. The thrown message names the requirement, never\n * the value, its length, or any derived fragment.\n *\n * @param env - Environment to read from. Defaults to `process.env`; injectable\n * for tests so they never mutate the real process environment.\n */\nexport function assertRelayAuthConfigured(env: NodeJS.ProcessEnv = process.env): void {\n if (!isValidRelayAuthSecret(env.AIT_DEBUG_TOTP_SECRET)) {\n throw new Error(RELAY_AUTH_SECRET_MISSING_MESSAGE);\n }\n}\n\n/**\n * Gate-specific skew for the relay WebSocket upgrade TOTP check.\n *\n * Rationale (why 6, not the RFC default of 1):\n * - Each step is 30 s, so ±6 steps = past 6 steps accepted = 180–210 s of\n * backwards acceptance. This means a code generated at issuance time is\n * guaranteed valid for at least 3 minutes (180 s) after it was minted.\n * - The real-world attach flow (QR issued on desktop → developer picks up\n * phone → camera scan → launcher PWA loads → attach) routinely exceeds\n * the 90 s window of the RFC default (skew=1), especially when the launcher\n * PWA needs to reinstall or when the phone is not immediately at hand.\n * - Expanding to ~3.5 min reachability is acceptable under the §4 threat\n * model: the adversary we guard against is \"someone who got the URL but\n * does NOT have the secret\". Without the secret they cannot compute a TOTP\n * code regardless of the window size — security theater is explicitly\n * forbidden by the project principle. An attacker WITH the secret (bundle\n * extractor) is out of scope per CLAUDE.md §4.\n *\n * `verifyTotp`'s own default (skew=1) is deliberately left unchanged — it is\n * the RFC primitive. Only this relay-gate call site is widened.\n */\nexport const RELAY_VERIFY_SKEW_STEPS = 6;\n\n/**\n * Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a\n * `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.\n *\n * The predicate checks the `at` query parameter against the current and\n * adjacent TOTP time steps (±{@link RELAY_VERIFY_SKEW_STEPS} skew) using\n * {@link verifyTotp}. This gives the issued code a minimum validity of ~3\n * minutes, which is enough to cover the QR-scan → launcher-attach flow even\n * when the launcher PWA needs to load or reinstall (#490).\n *\n * Returns `undefined` when the env var is not set — callers treat that as\n * \"auth disabled\" (no predicate registered on the relay). Note that since\n * issue #250 the secret is MANDATORY at every relay-boot site (enforced by\n * {@link assertRelayAuthConfigured} BEFORE the relay starts), so in production\n * this never returns `undefined` for a relay that actually boots; the\n * `undefined` branch only matters for the no-relay local path and tests.\n *\n * Lives here (not in the MCP server) so the unplugin's env-2 relay can wire the\n * same gate without importing the heavy MCP server module graph. Re-exported\n * from `debug-server.ts` for back-compat.\n *\n * SECRET-HANDLING: The secret value read from env is captured in a closure and\n * is NEVER written to any log, error message, or process output.\n */\nexport function buildRelayVerifyAuth(\n env: NodeJS.ProcessEnv = process.env,\n): ((req: import('node:http').IncomingMessage) => boolean) | undefined {\n const secret = env.AIT_DEBUG_TOTP_SECRET;\n if (!secret) return undefined;\n\n return (req) => {\n // Parse the `at` query param from the upgrade request URL.\n // req.url is the raw request path + query, e.g. `/client/id?target=…&at=123456`\n const rawUrl = req.url ?? '';\n const qIndex = rawUrl.indexOf('?');\n const queryStr = qIndex === -1 ? '' : rawUrl.slice(qIndex + 1);\n const params = new URLSearchParams(queryStr);\n const code = params.get('at') ?? '';\n\n // Do NOT log `code`, `secret`, or any derived value here.\n // Use RELAY_VERIFY_SKEW_STEPS (±6) for a ~3-minute acceptance window (#490).\n // verifyTotp's own default (skew=1) is unchanged — only this call site is widened.\n return verifyTotp(secret, code, undefined, RELAY_VERIFY_SKEW_STEPS);\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA8BA,MAAM,YAAY;;AAGlB,MAAM,SAAS;;;;;;;;;;AAWf,SAAgB,aAAa,QAAgB,OAAe,KAAK,KAAK,EAAU;CAC9E,MAAM,MAAM,OAAO,KAAK,QAAQ,MAAM;CAGtC,MAAM,UAAU,KAAK,IAAI,GAAG,KAAK,MAAM,OAAO,MAAO,UAAU,CAAC;CAGhE,MAAM,aAAa,OAAO,MAAM,EAAE;CAGlC,MAAM,KAAK,KAAK,MAAM,UAAU,WAAY;CAC5C,MAAM,KAAK,YAAY;AACvB,YAAW,cAAc,IAAI,EAAE;AAC/B,YAAW,cAAc,IAAI,EAAE;CAE/B,MAAM,MAAM,WAAW,QAAQ,IAAI,CAAC,OAAO,WAAW,CAAC,QAAQ;CAG/D,MAAM,SAAS,IAAI,MAAM;AAQzB,WANI,IAAI,UAAU,QAAS,MACvB,IAAI,SAAS,KAAK,QAAS,MAC3B,IAAI,SAAS,KAAK,QAAS,IAC5B,IAAI,SAAS,KAAK,OAEC,MAAM,QACjB,UAAU,CAAC,SAAS,QAAQ,IAAI;;;;;;;;;;;;;;;;AAiB7C,SAAgB,WACd,QACA,MACA,OAAe,KAAK,KAAK,EACzB,OAAe,GACN;CACT,MAAM,aAAa,OAAO,KAAK,CAAC,SAAS,QAAQ,IAAI;AACrD,KAAI,WAAW,WAAW,UAAU,CAAC,UAAU,KAAK,WAAW,CAC7D,QAAO;CAGT,MAAM,eAAe,OAAO,KAAK,YAAY,OAAO;AAEpD,MAAK,IAAI,QAAQ,CAAC,MAAM,SAAS,MAAM,SAAS;EAE9C,MAAM,WAAW,aAAa,QADb,OAAO,QAAQ,YAAY,IACG;AAE/C,MAAI,gBADgB,OAAO,KAAK,UAAU,OAAO,EAChB,aAAa,CAC5C,QAAO;;AAIX,QAAO;;;;;;;;;;AAWT,MAAM,uBAAuB;;AAG7B,MAAM,SAAS;;;;;;;;;;;;;AAcf,MAAa,oCAAoC;CAC/C;CACA;CACA;CACA;CACA;CACD,CAAC,KAAK,KAAK;;;;;;;;;;;;;AAcZ,SAAgB,uBAAuB,QAA8C;AACnF,KAAI,WAAW,KAAA,KAAa,WAAW,GAAI,QAAO;AAClD,KAAI,OAAO,SAAS,qBAAsB,QAAO;AACjD,KAAI,OAAO,SAAS,MAAM,EAAG,QAAO;AACpC,QAAO,OAAO,KAAK,OAAO;;;;;;;;;;;;;;;;;;;;;;;;;AA0B5B,SAAgB,0BAA0B,MAAyB,QAAQ,KAAW;AACpF,KAAI,CAAC,uBAAuB,IAAI,sBAAsB,CACpD,OAAM,IAAI,MAAM,kCAAkC;;;;;;;;;;;;;;;;;;;;;;;;;;AAmDtD,SAAgB,qBACd,MAAyB,QAAQ,KACoC;CACrE,MAAM,SAAS,IAAI;AACnB,KAAI,CAAC,OAAQ,QAAO,KAAA;AAEpB,SAAQ,QAAQ;EAGd,MAAM,SAAS,IAAI,OAAO;EAC1B,MAAM,SAAS,OAAO,QAAQ,IAAI;EAClC,MAAM,WAAW,WAAW,KAAK,KAAK,OAAO,MAAM,SAAS,EAAE;AAO9D,SAAO,WAAW,QANH,IAAI,gBAAgB,SAAS,CACxB,IAAI,KAAK,IAAI,IAKD,KAAA,GAAA,EAAmC"}
@@ -154,7 +154,7 @@ async function startTunnelDashboard(opts) {
154
154
  if (opts.qr === false) return void 0;
155
155
  const { isAutoDevtoolsDisabled } = await import("./devtools-opener-B8nxrxqu.js");
156
156
  if (!(opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser()))()) return void 0;
157
- const { startQrHttpServer } = await import("./qr-http-server-BsWlOA85.js");
157
+ const { startQrHttpServer } = await import("./qr-http-server-CrGohOwC.js");
158
158
  const { buildLauncherAttachUrl } = await import("./deeplink-B5-Hxu0Q.js");
159
159
  const { generateTotp } = await import("./totp-DIbrZtI7.js");
160
160
  const getDashboardState = () => {
@@ -288,4 +288,4 @@ async function startQuickTunnel(port) {
288
288
  //#endregion
289
289
  export { printTunnelBanner, startQuickTunnel, startTunnelDashboard };
290
290
 
291
- //# sourceMappingURL=tunnel-B7U5k1xa.js.map
291
+ //# sourceMappingURL=tunnel-BFwOSwT4.js.map