@ait-co/devtools 0.1.60 → 0.1.61

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 (36) hide show
  1. package/dist/in-app/index.js +4 -0
  2. package/dist/in-app/index.js.map +1 -1
  3. package/dist/mcp/cli.js +79 -30
  4. package/dist/mcp/cli.js.map +1 -1
  5. package/dist/mcp/server.js +1 -1
  6. package/dist/panel/index.js +10 -4
  7. package/dist/panel/index.js.map +1 -1
  8. package/dist/{qr-http-server-N4mX8GaC.js → qr-http-server-BLQScKGq.js} +24 -6
  9. package/dist/qr-http-server-BLQScKGq.js.map +1 -0
  10. package/dist/{qr-http-server-kYvmlXlg.js → qr-http-server-BTFj0LYB.js} +24 -6
  11. package/dist/qr-http-server-BTFj0LYB.js.map +1 -0
  12. package/dist/{qr-http-server-Byk0Yjk_.cjs → qr-http-server-CSoE5MdF.cjs} +24 -6
  13. package/dist/qr-http-server-CSoE5MdF.cjs.map +1 -0
  14. package/dist/{qr-http-server-D_Aj5Vq6.cjs → qr-http-server-DgyGxjET.cjs} +24 -6
  15. package/dist/qr-http-server-DgyGxjET.cjs.map +1 -0
  16. package/dist/{relay-secret-store-5A7_7zOp.js → relay-secret-store-DBcKWUl9.js} +2 -2
  17. package/dist/{relay-secret-store-5A7_7zOp.js.map → relay-secret-store-DBcKWUl9.js.map} +1 -1
  18. package/dist/{relay-url-store-qaoe0zOD.js → relay-url-store-Dq3vpd95.js} +2 -2
  19. package/dist/{relay-url-store-qaoe0zOD.js.map → relay-url-store-Dq3vpd95.js.map} +1 -1
  20. package/dist/totp-CQFmgOhM.js +3 -0
  21. package/dist/{totp-BIrJHsQn.js → totp-D0a8VwoR.js} +1 -1
  22. package/dist/{totp-BIrJHsQn.js.map → totp-D0a8VwoR.js.map} +1 -1
  23. package/dist/{tunnel-JuZ5_Pci.js → tunnel-CArP5y9b.js} +2 -2
  24. package/dist/{tunnel-JuZ5_Pci.js.map → tunnel-CArP5y9b.js.map} +1 -1
  25. package/dist/{tunnel-GieyWa22.cjs → tunnel-DwL0xizq.cjs} +2 -2
  26. package/dist/{tunnel-GieyWa22.cjs.map → tunnel-DwL0xizq.cjs.map} +1 -1
  27. package/dist/unplugin/index.cjs +1 -1
  28. package/dist/unplugin/index.js +1 -1
  29. package/dist/unplugin/tunnel.cjs +1 -1
  30. package/dist/unplugin/tunnel.js +1 -1
  31. package/package.json +1 -1
  32. package/dist/qr-http-server-Byk0Yjk_.cjs.map +0 -1
  33. package/dist/qr-http-server-D_Aj5Vq6.cjs.map +0 -1
  34. package/dist/qr-http-server-N4mX8GaC.js.map +0 -1
  35. package/dist/qr-http-server-kYvmlXlg.js.map +0 -1
  36. package/dist/totp-86i_CNqh.js +0 -3
@@ -185,6 +185,10 @@ let attached = false;
185
185
  function maybeAttach(gateResult = checkDebugGate()) {
186
186
  if (!gateResult.attach) {
187
187
  console.debug(`[@ait-co/devtools] debug attach skipped — gate blocked (reason: ${gateResult.reason})`);
188
+ if (gateResult.reason === "auth" && typeof window !== "undefined" && window.parent !== window) window.parent.postMessage({
189
+ type: "ait:debug-attach-blocked",
190
+ reason: "auth"
191
+ }, "*");
188
192
  return;
189
193
  }
190
194
  if (attached) return;
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","names":[],"sources":["../../src/in-app/gate.ts","../../src/in-app/attach.ts","../../src/in-app/index.ts"],"sourcesContent":["/**\n * Runtime activation gate for the in-app debug surface.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n * \"3-layer activation gate\". This is the pure gate decision; the Chii client,\n * WebSocket transport, MCP server, and CLI that consume it live in src/mcp/.\n *\n * This function evaluates the two RUNTIME layers, B and C. Layer A — the\n * build-time gate — is NOT evaluated here, and deliberately so: it is enforced\n * entirely by the consumer's `if (__DEBUG_BUILD__) { … }` guard around the\n * import site (see sdk-example `src/main.tsx`). `__DEBUG_BUILD__` is a\n * consumer-build-time constant; a release consumer build folds it to `false`\n * and dead-code-eliminates the whole import of `@ait-co/devtools/in-app`, so\n * this code is simply absent from release bundles. A pre-built npm package\n * cannot re-check that flag — it was already baked at devtools' own publish\n * time — so any `isDebugBuild` check inside this function would be permanently\n * `false` and could never pass. Layer A is the consumer guard; B and C are\n * here.\n *\n * Layer B has two parts:\n * B1 — host allowlist: `hostname` must be a `*.private-apps.tossmini.com`\n * subdomain (Toss dogfood entry) OR a `*.trycloudflare.com` host (env 2\n * PWA dev tunnel). The Toss app serves dogfood / private mini-apps from\n * a separate `private-apps` host; a production (`intoss://`) entry is\n * served from `*.apps.tossmini.com` WITHOUT the `private-apps` segment.\n * This is the security gate against a dogfood build that somehow lands\n * on a production entry — see the comment on {@link isPrivateAppsHost}.\n * The env 2 tunnel host is allowed because it has no production runtime\n * (mock SDK, the developer's own dev server) — see {@link\n * isTrycloudflareHost}.\n * B2 — entry query: `_deploymentId` must be present and non-empty. Applies to\n * the Toss path only; the env 2 tunnel has no deployed bundle, so B2 is\n * skipped for `*.trycloudflare.com` hosts.\n *\n * Layer C — opt-in + relay + optional TOTP auth:\n * C1 — opt-in: `debug=1` must be present.\n * C2 — relay URL: `relay=<wss-url>` must be a valid `wss:` URL.\n * C3 — TOTP auth: When `verifyTotpCode` is provided (consumer injected the\n * baked secret at build time via `__DEBUG_TOTP_SECRET__`),\n * `at=<code>` is checked. Invalid or absent code → BLOCKED.\n * When no verifier is provided (TOTP disabled), `at` is\n * ignored (backward compatible).\n *\n * Security note on baked secrets:\n * The TOTP secret baked in via `__DEBUG_TOTP_SECRET__` is present in the\n * dogfood bundle and is extractable by a determined reverse engineer.\n * The practical bar raised is: \"URL leak\" (Slack paste, QR screenshot) →\n * blocked; \"URL + bundle extraction + live TOTP code\" → not blocked.\n * This is the intended threat model. Do not overpromise on this guarantee.\n *\n * SECRET-HANDLING: `verifyTotpCode` is a black-box predicate. This module\n * does NOT log the secret, any code value, or pass/fail details beyond the\n * `'auth'` reason enum.\n *\n * Decision matrix (gate only runs in a debug build — Layer A already passed):\n *\n * host | _deploymentId | debug=1 | relay ok | TOTP ok* | result\n * neither | (any) | (any) | (any) | (any) | BLOCKED (host)\n * private-apps| absent | (any) | (any) | (any) | BLOCKED (entry)\n * private-apps| present | absent | (any) | (any) | BLOCKED (opt-in)\n * private-apps| present | present | invalid | (any) | BLOCKED (invalid-relay)\n * private-apps| present | present | valid | fail* | BLOCKED (auth)\n * private-apps| present | present | valid | pass/n/a | ATTACH\n * trycloudflare| (skipped) | absent | (any) | (any) | BLOCKED (opt-in)\n * trycloudflare| (skipped) | present | invalid | (any) | BLOCKED (invalid-relay)\n * trycloudflare| (skipped) | present | valid | fail* | BLOCKED (auth)\n * trycloudflare| (skipped) | present | valid | pass/n/a | ATTACH\n *\n * * \"TOTP ok\" column only applies when `verifyTotpCode` is provided.\n * When no verifier is injected, TOTP check is skipped entirely.\n * For trycloudflare (env 2 tunnel) hosts B1 is bypassed and B2 is skipped;\n * C1/C2/C3 still apply identically. The ATTACH result carries\n * `deploymentId: ''` for tunnel hosts.\n */\n\n/** Shape returned when the gate allows attachment. */\nexport interface GateResultAttach {\n readonly attach: true;\n /** The validated `wss:` relay URL from the `relay` query param. */\n readonly relayUrl: string;\n /** The deployment ID extracted from the `_deploymentId` query param. */\n readonly deploymentId: string;\n}\n\n/** Shape returned when the gate blocks attachment, with a reason code. */\nexport interface GateResultBlocked {\n readonly attach: false;\n /**\n * - `'host'` Layer B1: `hostname` is not a `*.private-apps.tossmini.com` host.\n * - `'entry'` Layer B2: `_deploymentId` param is absent or empty.\n * - `'opt-in'` Layer C1: `debug=1` param is absent.\n * - `'invalid-relay'` Layer C2: `relay` param is absent, empty, or not a `wss:` URL.\n * - `'auth'` Layer C3: TOTP `at=` code is absent, invalid, or expired\n * (only when a `verifyTotpCode` predicate is injected).\n *\n * There is no `'build'` reason: Layer A is enforced by the consumer's\n * `if (__DEBUG_BUILD__)` guard, not by this function.\n *\n * SECRET-HANDLING: `'auth'` is the only value surfaced for auth failures —\n * no code value, expected value, or secret fragment is ever exposed.\n */\n readonly reason: 'host' | 'entry' | 'opt-in' | 'invalid-relay' | 'auth';\n}\n\nexport type GateResult = GateResultAttach | GateResultBlocked;\n\n/**\n * Input for {@link evaluateDebugGate}.\n *\n * All fields are explicit so the function is trivially testable without\n * touching `window`.\n */\nexport interface GateInput {\n /**\n * The host the page is served from — `window.location.hostname`.\n *\n * This is the Layer B1 security signal. Why hostname and not the entry\n * scheme: the Toss SDK normalises `intoss-private://` to `intoss://` in\n * `getSchemeUri()`, and `getOperationalEnvironment()` / `getWebViewType()`\n * return the same value (`\"toss\"` / `\"partner\"`) for both dogfood and\n * production entries — none of them distinguish a dogfood entry. The host\n * does: a dogfood / private-apps entry is served from\n * `*.private-apps.tossmini.com`, a production entry is not. This was\n * confirmed live over CDP against mini-app 31146 (see spec open question 2).\n */\n readonly hostname: string;\n\n /**\n * The URL search params to inspect for gate signals (Layers B2 and C).\n *\n * Prefer `URLSearchParams` so callers can pass `new URLSearchParams(location.search)`\n * without coupling the pure function to `window`.\n */\n readonly searchParams: URLSearchParams;\n\n /**\n * Optional TOTP code verifier for Layer C3 auth gate.\n *\n * When provided, `evaluateDebugGate` reads the `at` query param and passes\n * it to this predicate. Return `true` to allow, `false` to block with\n * `reason: 'auth'`.\n *\n * Inject via the consumer's build define, e.g.:\n * ```ts\n * // dogfood build entry — consumer's build injects __DEBUG_TOTP_SECRET__\n * declare const __DEBUG_TOTP_SECRET__: string | undefined;\n * const verifyTotpCode = typeof __DEBUG_TOTP_SECRET__ !== 'undefined'\n * ? (code: string) => verifyTotp(__DEBUG_TOTP_SECRET__, code)\n * : undefined;\n * maybeAttach(evaluateDebugGate({ ...params, verifyTotpCode }));\n * ```\n *\n * Security note: this predicate is a black-box from the gate's perspective.\n * The gate only surfaces pass/fail and the `'auth'` reason code — no code\n * value or secret fragment is ever logged or returned.\n *\n * When `undefined` (TOTP disabled), `at=` is silently ignored and the gate\n * proceeds to ATTACH if all other layers pass.\n */\n readonly verifyTotpCode?: (code: string) => boolean;\n}\n\n/**\n * The host suffix the Toss app uses to serve dogfood / private mini-apps.\n *\n * A `intoss-private://` (dogfood) entry maps to a host such as\n * `aitc-sdk-example.private-apps.tossmini.com`. A production `intoss://`\n * entry is served from `*.apps.tossmini.com` — the `.private-apps.` segment\n * is absent. Confirmed live over CDP for mini-app 31146; the exact production\n * host is to be re-confirmed once 31146 passes review (spec open question 2).\n */\nconst PRIVATE_APPS_HOST_SUFFIX = '.private-apps.tossmini.com';\n\n/**\n * The host suffix Cloudflare quick-tunnels serve from — the env 2 (PWA) entry.\n * See {@link isTrycloudflareHost} for why this host kind bypasses Layer B1.\n */\nconst TRYCLOUDFLARE_HOST_SUFFIX = '.trycloudflare.com';\n\n/**\n * Returns whether `hostname` is a `*.private-apps.tossmini.com` subdomain —\n * the host the Toss app reserves for dogfood / private mini-app entries.\n *\n * The match is an exact suffix check, not a substring `.includes()`: a\n * substring test would also accept an attacker-controlled host like\n * `private-apps.tossmini.com.evil.example`, which ends in `.example`, not in\n * `.tossmini.com`. Requiring the string to END with the suffix closes that.\n * The leading `.` in the suffix also forces a real subdomain label, so a\n * bare `private-apps.tossmini.com` (no mini-app subdomain) does not match.\n */\nexport function isPrivateAppsHost(hostname: string): boolean {\n return hostname.endsWith(PRIVATE_APPS_HOST_SUFFIX);\n}\n\n/**\n * The host suffix Cloudflare quick-tunnels use — the env 2 (PWA) entry.\n *\n * Env 2 serves the local Vite dev server through a `*.trycloudflare.com` quick\n * tunnel (`src/unplugin/tunnel.ts`). It has no Toss app, no `intoss-private://`\n * scheme, and — critically — no production runtime: the SDK is the devtools\n * mock, and the page is the developer's own dev build. The Layer B1 safety net\n * (which stops a dogfood build that lands on a Toss *production* host from\n * attaching) has nothing to protect against here, because env 2 has no\n * production host. So a trycloudflare host is allowed past B1 — but ONLY past\n * B1: the remaining layers (C1 opt-in, C2 relay, C3 TOTP) still apply, so a\n * leaked tunnel URL is still blocked by TOTP exactly as on the Toss path.\n *\n * The match is the same exact-suffix `endsWith` check as\n * {@link isPrivateAppsHost} — never a substring `.includes()`, which would\n * accept an attacker-controlled `evil.trycloudflare.com.example.com`. The\n * leading `.` forces a real subdomain label, so a bare `trycloudflare.com`\n * (no tunnel subdomain) does not match.\n */\nexport function isTrycloudflareHost(hostname: string): boolean {\n return hostname.endsWith(TRYCLOUDFLARE_HOST_SUFFIX);\n}\n\n/**\n * Pure function that evaluates the runtime debug activation layers (B and C).\n *\n * Has no side effects. The input is explicit. Returns a discriminated union\n * so callers can pattern-match on `result.attach`.\n *\n * Layer A (build-time) is intentionally not evaluated here — see the file-level\n * comment. By the time this function runs, the consumer's `if (__DEBUG_BUILD__)`\n * guard has already passed; this function only decides B and C.\n *\n * @example\n * ```ts\n * const result = evaluateDebugGate({\n * hostname: window.location.hostname,\n * searchParams: new URLSearchParams(window.location.search),\n * });\n * if (result.attach) {\n * // Proceed to load Chii client\n * }\n * ```\n */\nexport function evaluateDebugGate(input: GateInput): GateResult {\n // Layer B1 — host allowlist (the security gate).\n // Two host kinds are allowed past B1:\n // - Toss dogfood: `*.private-apps.tossmini.com`. A production `intoss://`\n // entry is served from `*.apps.tossmini.com` and is rejected here. This\n // is what stops a dogfood build that somehow reaches a production entry\n // from attaching: Layer A keeps debug code out of release bundles, and\n // this layer keeps a dogfood bundle that lands on a production host from\n // attaching even though its code is present.\n // - Env 2 PWA tunnel: `*.trycloudflare.com`. This is the developer's own\n // local dev server (mock SDK, no production runtime), so the\n // production-entry hazard B1 guards against cannot occur. It bypasses B1\n // but NOT the remaining layers — C1/C2/C3 (incl. TOTP) still apply, so a\n // leaked tunnel URL is blocked exactly as on the Toss path. See\n // {@link isTrycloudflareHost}.\n const isTunnel = isTrycloudflareHost(input.hostname);\n if (!isPrivateAppsHost(input.hostname) && !isTunnel) {\n return { attach: false, reason: 'host' };\n }\n\n // Layer B2 — runtime entry query gate (Toss path only).\n // `_deploymentId` must be present and non-empty. The `intoss-private://`\n // scheme used for dogfood entries includes this param; general user entry\n // paths do not. The env 2 tunnel has no deployed bundle and therefore no\n // `_deploymentId` — B2 is skipped for it, and `deploymentId` is reported as\n // the empty string on a tunnel attach (no consumer reads it; see attach.ts).\n let deploymentId = '';\n if (!isTunnel) {\n deploymentId = input.searchParams.get('_deploymentId') ?? '';\n if (deploymentId === '') {\n return { attach: false, reason: 'entry' };\n }\n }\n\n // Layer C — explicit opt-in gate.\n // Require `debug=1` so that an operator who opens a dogfood URL by accident\n // does not inadvertently trigger the debug surface.\n const debugParam = input.searchParams.get('debug');\n if (debugParam !== '1') {\n return { attach: false, reason: 'opt-in' };\n }\n\n // Layer C continued — relay URL validation.\n // `relay=<wss-url>` must be present and must use the `wss:` scheme.\n // Plain `ws:` is rejected (no TLS). `http:`/`https:` are rejected.\n const relayRaw = input.searchParams.get('relay') ?? '';\n if (relayRaw === '') {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n let relayUrl: URL;\n try {\n relayUrl = new URL(relayRaw);\n } catch {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n if (relayUrl.protocol !== 'wss:') {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n // Layer C3 — TOTP auth gate (fail-fast, only when a verifier is injected).\n // The `at` query param carries the current TOTP code. Absent or invalid code\n // → BLOCKED. When no verifier is provided (TOTP disabled), this check is\n // skipped entirely for backward compatibility.\n //\n // SECRET-HANDLING: we do NOT log `code`, the verifier's result, or anything\n // derived from the secret. Only the `'auth'` enum is surfaced on failure.\n if (input.verifyTotpCode !== undefined) {\n const code = input.searchParams.get('at') ?? '';\n if (!input.verifyTotpCode(code)) {\n return { attach: false, reason: 'auth' };\n }\n }\n\n return { attach: true, relayUrl: relayUrl.href, deploymentId };\n}\n","/**\n * In-app Chii target injection for the debug attach flow.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n * \"MCP attach\" topology section — Phase 1 browser-side implementation.\n *\n * This module bridges the 3-layer gate result to a Chii `target.js` script\n * injection. The Chii npm package is the relay SERVER — the in-app side is\n * a plain `<script src=\"…/target.js\">` pointing at the relay host. No chii\n * npm dependency is needed here.\n */\n\nimport { setScreenAwakeMode } from '@apps-in-toss/web-framework';\nimport { checkDebugGate, type GateResult } from './index.js';\n\n/**\n * Converts a validated `wss:` relay URL into the Chii `target.js` script URL.\n *\n * Scheme is mapped `wss:` → `https:`. Host and port are preserved.\n * Pathname is set to `/target.js` regardless of the relay path.\n * Query params and hash from the relay URL are dropped — the target script\n * URL is a static asset path on the same host.\n *\n * @example\n * deriveTargetScriptUrl('wss://abc.trycloudflare.com/relay')\n * // → 'https://abc.trycloudflare.com/target.js'\n *\n * deriveTargetScriptUrl('wss://h.example.com:9100/')\n * // → 'https://h.example.com:9100/target.js'\n */\nexport function deriveTargetScriptUrl(relayUrl: string): string {\n const u = new URL(relayUrl);\n u.protocol = 'https:';\n u.pathname = '/target.js';\n u.search = '';\n u.hash = '';\n return u.toString();\n}\n\n/** Module-level guard against double-injection within a page lifecycle. */\nlet attached = false;\n\n/**\n * Evaluates the 3-layer debug gate and, if the gate passes, injects the Chii\n * `target.js` script into `document.head`.\n *\n * Idempotent — calling more than once is safe. The second call is a no-op if\n * a script with the same `src` is already present in the document, and the\n * module-level `attached` flag prevents redundant DOM queries after the first\n * successful injection.\n *\n * Safe to call even if `document` is somehow unavailable (defensive boundary\n * guard — in practice this always runs in a real WebView).\n *\n * **keepAwake side effect**: on a successful attach, `setScreenAwakeMode({\n * enabled: true })` is called so the phone screen stays awake during the debug\n * session. A `beforeunload` handler restores normal sleep on page unload.\n * Opt out by adding `noKeepAwake=1` to the page URL query string — the check\n * reads `window.location.search` directly, consistent with other guards in\n * this file.\n *\n * @param gateResult - Optional pre-evaluated gate result for testability.\n * Defaults to `checkDebugGate()` which reads the current page URL. Passing a\n * custom value avoids the need to manipulate `window.location` in tests.\n */\nexport function maybeAttach(gateResult: GateResult = checkDebugGate()): void {\n if (!gateResult.attach) {\n console.debug(\n `[@ait-co/devtools] debug attach skipped — gate blocked (reason: ${gateResult.reason})`,\n );\n return;\n }\n\n // Guard against double-injection across repeated calls.\n if (attached) {\n return;\n }\n\n // Defensive: if document is not available (unusual, but possible in some\n // SSR-adjacent edge cases), bail silently rather than throwing.\n if (typeof document === 'undefined') {\n return;\n }\n\n const src = deriveTargetScriptUrl(gateResult.relayUrl);\n\n // Also guard against a script with the same src already in the DOM\n // (e.g. injected by a different code path or a page reload within SPA).\n const existing = document.querySelector<HTMLScriptElement>(`script[src=\"${src}\"]`);\n if (existing !== null) {\n attached = true;\n return;\n }\n\n const script = document.createElement('script');\n script.src = src;\n script.async = true;\n (document.head ?? document.documentElement).appendChild(script);\n\n attached = true;\n\n // keepAwake — keep phone screen on during the debug session.\n // Opt out via noKeepAwake=1 in the URL (consistent with direct window reads\n // used throughout this file).\n if (\n typeof window !== 'undefined' &&\n new URLSearchParams(window.location.search).get('noKeepAwake') === '1'\n ) {\n return;\n }\n\n setScreenAwakeMode({ enabled: true })\n .then(() => {\n // Restore normal sleep on page unload — only if the enable call succeeded\n // (nothing to restore if it failed).\n window.addEventListener(\n 'beforeunload',\n () => {\n setScreenAwakeMode({ enabled: false }).catch(() => {});\n },\n { once: true },\n );\n })\n .catch((err) => {\n // Swallow rejection so attach never breaks — some platforms/mock reject.\n console.debug('[@ait-co/devtools] setScreenAwakeMode failed:', err);\n });\n}\n","/**\n * @ait-co/devtools/in-app entry point.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n *\n * Phase 1 — gate + browser-side Chii target injection.\n * WebSocket relay, QR/paste UI, and AI-host MCP bin are later phases that\n * require real-device validation and are not included here.\n *\n * This thin entry reads `window.location` and calls the pure\n * {@link evaluateDebugGate} function. All testable logic lives in `./gate.ts`\n * and `./attach.ts`, not here.\n *\n * Layer A of the activation gate (build-time) is NOT enforced in this module.\n * It is the consumer's responsibility: the consumer wraps its\n * `import('@ait-co/devtools/in-app')` call site in `if (__DEBUG_BUILD__) { … }`\n * (see sdk-example `src/main.tsx`), where `__DEBUG_BUILD__` is a\n * consumer-build-time constant. A release consumer build folds that constant\n * to `false` and dead-code-eliminates this whole module. This package is\n * pre-built and ships with `__DEBUG_BUILD__` already resolved at devtools'\n * publish time, so it could never re-evaluate the consumer's build channel —\n * which is exactly why Layer A lives at the consumer guard, not here.\n */\n\nimport { evaluateDebugGate, type GateResult } from './gate.js';\n\nexport { deriveTargetScriptUrl, maybeAttach } from './attach.js';\nexport type { GateInput, GateResult, GateResultAttach, GateResultBlocked } from './gate.js';\nexport { evaluateDebugGate, isPrivateAppsHost, isTrycloudflareHost } from './gate.js';\n\n/**\n * Evaluates the runtime debug activation layers (B and C) against the current\n * page URL.\n *\n * Returns the gate result. Callers can check `result.attach` to decide whether\n * to proceed with debug surface attachment.\n *\n * This function reads `window.location` only — both the hostname (Layer B1\n * host allowlist) and the search params (Layers B2 and C). Layer A\n * (build-time) is enforced by the consumer's `if (__DEBUG_BUILD__)` guard\n * around the import site, not here — see the file-level comment. Consumers\n * call this with no arguments, so the Layer B1 host check is picked up with\n * no change at the call site.\n */\nexport function checkDebugGate(): GateResult {\n return evaluateDebugGate({\n hostname: window.location.hostname,\n searchParams: new URLSearchParams(window.location.search),\n });\n}\n"],"mappings":";;;;;;;;;;;AA2KA,MAAM,2BAA2B;;;;;AAMjC,MAAM,4BAA4B;;;;;;;;;;;;AAalC,SAAgB,kBAAkB,UAA2B;AAC3D,QAAO,SAAS,SAAS,yBAAyB;;;;;;;;;;;;;;;;;;;;;AAsBpD,SAAgB,oBAAoB,UAA2B;AAC7D,QAAO,SAAS,SAAS,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;AAwBrD,SAAgB,kBAAkB,OAA8B;CAe9D,MAAM,WAAW,oBAAoB,MAAM,SAAS;AACpD,KAAI,CAAC,kBAAkB,MAAM,SAAS,IAAI,CAAC,SACzC,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAQ;CAS1C,IAAI,eAAe;AACnB,KAAI,CAAC,UAAU;AACb,iBAAe,MAAM,aAAa,IAAI,gBAAgB,IAAI;AAC1D,MAAI,iBAAiB,GACnB,QAAO;GAAE,QAAQ;GAAO,QAAQ;GAAS;;AAQ7C,KADmB,MAAM,aAAa,IAAI,QAAQ,KAC/B,IACjB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAU;CAM5C,MAAM,WAAW,MAAM,aAAa,IAAI,QAAQ,IAAI;AACpD,KAAI,aAAa,GACf,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAiB;CAGnD,IAAI;AACJ,KAAI;AACF,aAAW,IAAI,IAAI,SAAS;SACtB;AACN,SAAO;GAAE,QAAQ;GAAO,QAAQ;GAAiB;;AAGnD,KAAI,SAAS,aAAa,OACxB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAiB;AAUnD,KAAI,MAAM,mBAAmB,KAAA,GAAW;EACtC,MAAM,OAAO,MAAM,aAAa,IAAI,KAAK,IAAI;AAC7C,MAAI,CAAC,MAAM,eAAe,KAAK,CAC7B,QAAO;GAAE,QAAQ;GAAO,QAAQ;GAAQ;;AAI5C,QAAO;EAAE,QAAQ;EAAM,UAAU,SAAS;EAAM;EAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC3RhE,SAAgB,sBAAsB,UAA0B;CAC9D,MAAM,IAAI,IAAI,IAAI,SAAS;AAC3B,GAAE,WAAW;AACb,GAAE,WAAW;AACb,GAAE,SAAS;AACX,GAAE,OAAO;AACT,QAAO,EAAE,UAAU;;;AAIrB,IAAI,WAAW;;;;;;;;;;;;;;;;;;;;;;;;AAyBf,SAAgB,YAAY,aAAyB,gBAAgB,EAAQ;AAC3E,KAAI,CAAC,WAAW,QAAQ;AACtB,UAAQ,MACN,mEAAmE,WAAW,OAAO,GACtF;AACD;;AAIF,KAAI,SACF;AAKF,KAAI,OAAO,aAAa,YACtB;CAGF,MAAM,MAAM,sBAAsB,WAAW,SAAS;AAKtD,KADiB,SAAS,cAAiC,eAAe,IAAI,IAAI,KACjE,MAAM;AACrB,aAAW;AACX;;CAGF,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,QAAO,MAAM;AACb,QAAO,QAAQ;AACf,EAAC,SAAS,QAAQ,SAAS,iBAAiB,YAAY,OAAO;AAE/D,YAAW;AAKX,KACE,OAAO,WAAW,eAClB,IAAI,gBAAgB,OAAO,SAAS,OAAO,CAAC,IAAI,cAAc,KAAK,IAEnE;AAGF,oBAAmB,EAAE,SAAS,MAAM,CAAC,CAClC,WAAW;AAGV,SAAO,iBACL,sBACM;AACJ,sBAAmB,EAAE,SAAS,OAAO,CAAC,CAAC,YAAY,GAAG;KAExD,EAAE,MAAM,MAAM,CACf;GACD,CACD,OAAO,QAAQ;AAEd,UAAQ,MAAM,iDAAiD,IAAI;GACnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AClFN,SAAgB,iBAA6B;AAC3C,QAAO,kBAAkB;EACvB,UAAU,OAAO,SAAS;EAC1B,cAAc,IAAI,gBAAgB,OAAO,SAAS,OAAO;EAC1D,CAAC"}
1
+ {"version":3,"file":"index.js","names":[],"sources":["../../src/in-app/gate.ts","../../src/in-app/attach.ts","../../src/in-app/index.ts"],"sourcesContent":["/**\n * Runtime activation gate for the in-app debug surface.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n * \"3-layer activation gate\". This is the pure gate decision; the Chii client,\n * WebSocket transport, MCP server, and CLI that consume it live in src/mcp/.\n *\n * This function evaluates the two RUNTIME layers, B and C. Layer A — the\n * build-time gate — is NOT evaluated here, and deliberately so: it is enforced\n * entirely by the consumer's `if (__DEBUG_BUILD__) { … }` guard around the\n * import site (see sdk-example `src/main.tsx`). `__DEBUG_BUILD__` is a\n * consumer-build-time constant; a release consumer build folds it to `false`\n * and dead-code-eliminates the whole import of `@ait-co/devtools/in-app`, so\n * this code is simply absent from release bundles. A pre-built npm package\n * cannot re-check that flag — it was already baked at devtools' own publish\n * time — so any `isDebugBuild` check inside this function would be permanently\n * `false` and could never pass. Layer A is the consumer guard; B and C are\n * here.\n *\n * Layer B has two parts:\n * B1 — host allowlist: `hostname` must be a `*.private-apps.tossmini.com`\n * subdomain (Toss dogfood entry) OR a `*.trycloudflare.com` host (env 2\n * PWA dev tunnel). The Toss app serves dogfood / private mini-apps from\n * a separate `private-apps` host; a production (`intoss://`) entry is\n * served from `*.apps.tossmini.com` WITHOUT the `private-apps` segment.\n * This is the security gate against a dogfood build that somehow lands\n * on a production entry — see the comment on {@link isPrivateAppsHost}.\n * The env 2 tunnel host is allowed because it has no production runtime\n * (mock SDK, the developer's own dev server) — see {@link\n * isTrycloudflareHost}.\n * B2 — entry query: `_deploymentId` must be present and non-empty. Applies to\n * the Toss path only; the env 2 tunnel has no deployed bundle, so B2 is\n * skipped for `*.trycloudflare.com` hosts.\n *\n * Layer C — opt-in + relay + optional TOTP auth:\n * C1 — opt-in: `debug=1` must be present.\n * C2 — relay URL: `relay=<wss-url>` must be a valid `wss:` URL.\n * C3 — TOTP auth: When `verifyTotpCode` is provided (consumer injected the\n * baked secret at build time via `__DEBUG_TOTP_SECRET__`),\n * `at=<code>` is checked. Invalid or absent code → BLOCKED.\n * When no verifier is provided (TOTP disabled), `at` is\n * ignored (backward compatible).\n *\n * Security note on baked secrets:\n * The TOTP secret baked in via `__DEBUG_TOTP_SECRET__` is present in the\n * dogfood bundle and is extractable by a determined reverse engineer.\n * The practical bar raised is: \"URL leak\" (Slack paste, QR screenshot) →\n * blocked; \"URL + bundle extraction + live TOTP code\" → not blocked.\n * This is the intended threat model. Do not overpromise on this guarantee.\n *\n * SECRET-HANDLING: `verifyTotpCode` is a black-box predicate. This module\n * does NOT log the secret, any code value, or pass/fail details beyond the\n * `'auth'` reason enum.\n *\n * Decision matrix (gate only runs in a debug build — Layer A already passed):\n *\n * host | _deploymentId | debug=1 | relay ok | TOTP ok* | result\n * neither | (any) | (any) | (any) | (any) | BLOCKED (host)\n * private-apps| absent | (any) | (any) | (any) | BLOCKED (entry)\n * private-apps| present | absent | (any) | (any) | BLOCKED (opt-in)\n * private-apps| present | present | invalid | (any) | BLOCKED (invalid-relay)\n * private-apps| present | present | valid | fail* | BLOCKED (auth)\n * private-apps| present | present | valid | pass/n/a | ATTACH\n * trycloudflare| (skipped) | absent | (any) | (any) | BLOCKED (opt-in)\n * trycloudflare| (skipped) | present | invalid | (any) | BLOCKED (invalid-relay)\n * trycloudflare| (skipped) | present | valid | fail* | BLOCKED (auth)\n * trycloudflare| (skipped) | present | valid | pass/n/a | ATTACH\n *\n * * \"TOTP ok\" column only applies when `verifyTotpCode` is provided.\n * When no verifier is injected, TOTP check is skipped entirely.\n * For trycloudflare (env 2 tunnel) hosts B1 is bypassed and B2 is skipped;\n * C1/C2/C3 still apply identically. The ATTACH result carries\n * `deploymentId: ''` for tunnel hosts.\n */\n\n/** Shape returned when the gate allows attachment. */\nexport interface GateResultAttach {\n readonly attach: true;\n /** The validated `wss:` relay URL from the `relay` query param. */\n readonly relayUrl: string;\n /** The deployment ID extracted from the `_deploymentId` query param. */\n readonly deploymentId: string;\n}\n\n/** Shape returned when the gate blocks attachment, with a reason code. */\nexport interface GateResultBlocked {\n readonly attach: false;\n /**\n * - `'host'` Layer B1: `hostname` is not a `*.private-apps.tossmini.com` host.\n * - `'entry'` Layer B2: `_deploymentId` param is absent or empty.\n * - `'opt-in'` Layer C1: `debug=1` param is absent.\n * - `'invalid-relay'` Layer C2: `relay` param is absent, empty, or not a `wss:` URL.\n * - `'auth'` Layer C3: TOTP `at=` code is absent, invalid, or expired\n * (only when a `verifyTotpCode` predicate is injected).\n *\n * There is no `'build'` reason: Layer A is enforced by the consumer's\n * `if (__DEBUG_BUILD__)` guard, not by this function.\n *\n * SECRET-HANDLING: `'auth'` is the only value surfaced for auth failures —\n * no code value, expected value, or secret fragment is ever exposed.\n */\n readonly reason: 'host' | 'entry' | 'opt-in' | 'invalid-relay' | 'auth';\n}\n\nexport type GateResult = GateResultAttach | GateResultBlocked;\n\n/**\n * Input for {@link evaluateDebugGate}.\n *\n * All fields are explicit so the function is trivially testable without\n * touching `window`.\n */\nexport interface GateInput {\n /**\n * The host the page is served from — `window.location.hostname`.\n *\n * This is the Layer B1 security signal. Why hostname and not the entry\n * scheme: the Toss SDK normalises `intoss-private://` to `intoss://` in\n * `getSchemeUri()`, and `getOperationalEnvironment()` / `getWebViewType()`\n * return the same value (`\"toss\"` / `\"partner\"`) for both dogfood and\n * production entries — none of them distinguish a dogfood entry. The host\n * does: a dogfood / private-apps entry is served from\n * `*.private-apps.tossmini.com`, a production entry is not. This was\n * confirmed live over CDP against mini-app 31146 (see spec open question 2).\n */\n readonly hostname: string;\n\n /**\n * The URL search params to inspect for gate signals (Layers B2 and C).\n *\n * Prefer `URLSearchParams` so callers can pass `new URLSearchParams(location.search)`\n * without coupling the pure function to `window`.\n */\n readonly searchParams: URLSearchParams;\n\n /**\n * Optional TOTP code verifier for Layer C3 auth gate.\n *\n * When provided, `evaluateDebugGate` reads the `at` query param and passes\n * it to this predicate. Return `true` to allow, `false` to block with\n * `reason: 'auth'`.\n *\n * Inject via the consumer's build define, e.g.:\n * ```ts\n * // dogfood build entry — consumer's build injects __DEBUG_TOTP_SECRET__\n * declare const __DEBUG_TOTP_SECRET__: string | undefined;\n * const verifyTotpCode = typeof __DEBUG_TOTP_SECRET__ !== 'undefined'\n * ? (code: string) => verifyTotp(__DEBUG_TOTP_SECRET__, code)\n * : undefined;\n * maybeAttach(evaluateDebugGate({ ...params, verifyTotpCode }));\n * ```\n *\n * Security note: this predicate is a black-box from the gate's perspective.\n * The gate only surfaces pass/fail and the `'auth'` reason code — no code\n * value or secret fragment is ever logged or returned.\n *\n * When `undefined` (TOTP disabled), `at=` is silently ignored and the gate\n * proceeds to ATTACH if all other layers pass.\n */\n readonly verifyTotpCode?: (code: string) => boolean;\n}\n\n/**\n * The host suffix the Toss app uses to serve dogfood / private mini-apps.\n *\n * A `intoss-private://` (dogfood) entry maps to a host such as\n * `aitc-sdk-example.private-apps.tossmini.com`. A production `intoss://`\n * entry is served from `*.apps.tossmini.com` — the `.private-apps.` segment\n * is absent. Confirmed live over CDP for mini-app 31146; the exact production\n * host is to be re-confirmed once 31146 passes review (spec open question 2).\n */\nconst PRIVATE_APPS_HOST_SUFFIX = '.private-apps.tossmini.com';\n\n/**\n * The host suffix Cloudflare quick-tunnels serve from — the env 2 (PWA) entry.\n * See {@link isTrycloudflareHost} for why this host kind bypasses Layer B1.\n */\nconst TRYCLOUDFLARE_HOST_SUFFIX = '.trycloudflare.com';\n\n/**\n * Returns whether `hostname` is a `*.private-apps.tossmini.com` subdomain —\n * the host the Toss app reserves for dogfood / private mini-app entries.\n *\n * The match is an exact suffix check, not a substring `.includes()`: a\n * substring test would also accept an attacker-controlled host like\n * `private-apps.tossmini.com.evil.example`, which ends in `.example`, not in\n * `.tossmini.com`. Requiring the string to END with the suffix closes that.\n * The leading `.` in the suffix also forces a real subdomain label, so a\n * bare `private-apps.tossmini.com` (no mini-app subdomain) does not match.\n */\nexport function isPrivateAppsHost(hostname: string): boolean {\n return hostname.endsWith(PRIVATE_APPS_HOST_SUFFIX);\n}\n\n/**\n * The host suffix Cloudflare quick-tunnels use — the env 2 (PWA) entry.\n *\n * Env 2 serves the local Vite dev server through a `*.trycloudflare.com` quick\n * tunnel (`src/unplugin/tunnel.ts`). It has no Toss app, no `intoss-private://`\n * scheme, and — critically — no production runtime: the SDK is the devtools\n * mock, and the page is the developer's own dev build. The Layer B1 safety net\n * (which stops a dogfood build that lands on a Toss *production* host from\n * attaching) has nothing to protect against here, because env 2 has no\n * production host. So a trycloudflare host is allowed past B1 — but ONLY past\n * B1: the remaining layers (C1 opt-in, C2 relay, C3 TOTP) still apply, so a\n * leaked tunnel URL is still blocked by TOTP exactly as on the Toss path.\n *\n * The match is the same exact-suffix `endsWith` check as\n * {@link isPrivateAppsHost} — never a substring `.includes()`, which would\n * accept an attacker-controlled `evil.trycloudflare.com.example.com`. The\n * leading `.` forces a real subdomain label, so a bare `trycloudflare.com`\n * (no tunnel subdomain) does not match.\n */\nexport function isTrycloudflareHost(hostname: string): boolean {\n return hostname.endsWith(TRYCLOUDFLARE_HOST_SUFFIX);\n}\n\n/**\n * Pure function that evaluates the runtime debug activation layers (B and C).\n *\n * Has no side effects. The input is explicit. Returns a discriminated union\n * so callers can pattern-match on `result.attach`.\n *\n * Layer A (build-time) is intentionally not evaluated here — see the file-level\n * comment. By the time this function runs, the consumer's `if (__DEBUG_BUILD__)`\n * guard has already passed; this function only decides B and C.\n *\n * @example\n * ```ts\n * const result = evaluateDebugGate({\n * hostname: window.location.hostname,\n * searchParams: new URLSearchParams(window.location.search),\n * });\n * if (result.attach) {\n * // Proceed to load Chii client\n * }\n * ```\n */\nexport function evaluateDebugGate(input: GateInput): GateResult {\n // Layer B1 — host allowlist (the security gate).\n // Two host kinds are allowed past B1:\n // - Toss dogfood: `*.private-apps.tossmini.com`. A production `intoss://`\n // entry is served from `*.apps.tossmini.com` and is rejected here. This\n // is what stops a dogfood build that somehow reaches a production entry\n // from attaching: Layer A keeps debug code out of release bundles, and\n // this layer keeps a dogfood bundle that lands on a production host from\n // attaching even though its code is present.\n // - Env 2 PWA tunnel: `*.trycloudflare.com`. This is the developer's own\n // local dev server (mock SDK, no production runtime), so the\n // production-entry hazard B1 guards against cannot occur. It bypasses B1\n // but NOT the remaining layers — C1/C2/C3 (incl. TOTP) still apply, so a\n // leaked tunnel URL is blocked exactly as on the Toss path. See\n // {@link isTrycloudflareHost}.\n const isTunnel = isTrycloudflareHost(input.hostname);\n if (!isPrivateAppsHost(input.hostname) && !isTunnel) {\n return { attach: false, reason: 'host' };\n }\n\n // Layer B2 — runtime entry query gate (Toss path only).\n // `_deploymentId` must be present and non-empty. The `intoss-private://`\n // scheme used for dogfood entries includes this param; general user entry\n // paths do not. The env 2 tunnel has no deployed bundle and therefore no\n // `_deploymentId` — B2 is skipped for it, and `deploymentId` is reported as\n // the empty string on a tunnel attach (no consumer reads it; see attach.ts).\n let deploymentId = '';\n if (!isTunnel) {\n deploymentId = input.searchParams.get('_deploymentId') ?? '';\n if (deploymentId === '') {\n return { attach: false, reason: 'entry' };\n }\n }\n\n // Layer C — explicit opt-in gate.\n // Require `debug=1` so that an operator who opens a dogfood URL by accident\n // does not inadvertently trigger the debug surface.\n const debugParam = input.searchParams.get('debug');\n if (debugParam !== '1') {\n return { attach: false, reason: 'opt-in' };\n }\n\n // Layer C continued — relay URL validation.\n // `relay=<wss-url>` must be present and must use the `wss:` scheme.\n // Plain `ws:` is rejected (no TLS). `http:`/`https:` are rejected.\n const relayRaw = input.searchParams.get('relay') ?? '';\n if (relayRaw === '') {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n let relayUrl: URL;\n try {\n relayUrl = new URL(relayRaw);\n } catch {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n if (relayUrl.protocol !== 'wss:') {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n // Layer C3 — TOTP auth gate (fail-fast, only when a verifier is injected).\n // The `at` query param carries the current TOTP code. Absent or invalid code\n // → BLOCKED. When no verifier is provided (TOTP disabled), this check is\n // skipped entirely for backward compatibility.\n //\n // SECRET-HANDLING: we do NOT log `code`, the verifier's result, or anything\n // derived from the secret. Only the `'auth'` enum is surfaced on failure.\n if (input.verifyTotpCode !== undefined) {\n const code = input.searchParams.get('at') ?? '';\n if (!input.verifyTotpCode(code)) {\n return { attach: false, reason: 'auth' };\n }\n }\n\n return { attach: true, relayUrl: relayUrl.href, deploymentId };\n}\n","/**\n * In-app Chii target injection for the debug attach flow.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n * \"MCP attach\" topology section — Phase 1 browser-side implementation.\n *\n * This module bridges the 3-layer gate result to a Chii `target.js` script\n * injection. The Chii npm package is the relay SERVER — the in-app side is\n * a plain `<script src=\"…/target.js\">` pointing at the relay host. No chii\n * npm dependency is needed here.\n */\n\nimport { setScreenAwakeMode } from '@apps-in-toss/web-framework';\nimport { checkDebugGate, type GateResult } from './index.js';\n\n/**\n * Converts a validated `wss:` relay URL into the Chii `target.js` script URL.\n *\n * Scheme is mapped `wss:` → `https:`. Host and port are preserved.\n * Pathname is set to `/target.js` regardless of the relay path.\n * Query params and hash from the relay URL are dropped — the target script\n * URL is a static asset path on the same host.\n *\n * @example\n * deriveTargetScriptUrl('wss://abc.trycloudflare.com/relay')\n * // → 'https://abc.trycloudflare.com/target.js'\n *\n * deriveTargetScriptUrl('wss://h.example.com:9100/')\n * // → 'https://h.example.com:9100/target.js'\n */\nexport function deriveTargetScriptUrl(relayUrl: string): string {\n const u = new URL(relayUrl);\n u.protocol = 'https:';\n u.pathname = '/target.js';\n u.search = '';\n u.hash = '';\n return u.toString();\n}\n\n/** Module-level guard against double-injection within a page lifecycle. */\nlet attached = false;\n\n/**\n * Evaluates the 3-layer debug gate and, if the gate passes, injects the Chii\n * `target.js` script into `document.head`.\n *\n * Idempotent — calling more than once is safe. The second call is a no-op if\n * a script with the same `src` is already present in the document, and the\n * module-level `attached` flag prevents redundant DOM queries after the first\n * successful injection.\n *\n * Safe to call even if `document` is somehow unavailable (defensive boundary\n * guard — in practice this always runs in a real WebView).\n *\n * **keepAwake side effect**: on a successful attach, `setScreenAwakeMode({\n * enabled: true })` is called so the phone screen stays awake during the debug\n * session. A `beforeunload` handler restores normal sleep on page unload.\n * Opt out by adding `noKeepAwake=1` to the page URL query string — the check\n * reads `window.location.search` directly, consistent with other guards in\n * this file.\n *\n * @param gateResult - Optional pre-evaluated gate result for testability.\n * Defaults to `checkDebugGate()` which reads the current page URL. Passing a\n * custom value avoids the need to manipulate `window.location` in tests.\n */\nexport function maybeAttach(gateResult: GateResult = checkDebugGate()): void {\n if (!gateResult.attach) {\n console.debug(\n `[@ait-co/devtools] debug attach skipped — gate blocked (reason: ${gateResult.reason})`,\n );\n // Defect 2: a wrong/expired TOTP code is the ONLY block reason that is a\n // user-actionable failure inside a deliberate debug session — the operator\n // scanned a QR expecting an attach. Surface it to the parent launcher shell\n // so it can show a \"rescan the QR\" banner. Every other reason\n // ('host'/'entry'/'opt-in'/'invalid-relay') fires on ordinary non-debug page\n // loads and must stay silent to avoid a banner on every plain pageview.\n // SECRET-HANDLING: the message carries ONLY the 'auth' reason enum — never\n // the code, secret, host, or relay URL.\n if (gateResult.reason === 'auth' && typeof window !== 'undefined' && window.parent !== window) {\n window.parent.postMessage({ type: 'ait:debug-attach-blocked', reason: 'auth' }, '*');\n }\n return;\n }\n\n // Guard against double-injection across repeated calls.\n if (attached) {\n return;\n }\n\n // Defensive: if document is not available (unusual, but possible in some\n // SSR-adjacent edge cases), bail silently rather than throwing.\n if (typeof document === 'undefined') {\n return;\n }\n\n const src = deriveTargetScriptUrl(gateResult.relayUrl);\n\n // Also guard against a script with the same src already in the DOM\n // (e.g. injected by a different code path or a page reload within SPA).\n const existing = document.querySelector<HTMLScriptElement>(`script[src=\"${src}\"]`);\n if (existing !== null) {\n attached = true;\n return;\n }\n\n const script = document.createElement('script');\n script.src = src;\n script.async = true;\n (document.head ?? document.documentElement).appendChild(script);\n\n attached = true;\n\n // keepAwake — keep phone screen on during the debug session.\n // Opt out via noKeepAwake=1 in the URL (consistent with direct window reads\n // used throughout this file).\n if (\n typeof window !== 'undefined' &&\n new URLSearchParams(window.location.search).get('noKeepAwake') === '1'\n ) {\n return;\n }\n\n setScreenAwakeMode({ enabled: true })\n .then(() => {\n // Restore normal sleep on page unload — only if the enable call succeeded\n // (nothing to restore if it failed).\n window.addEventListener(\n 'beforeunload',\n () => {\n setScreenAwakeMode({ enabled: false }).catch(() => {});\n },\n { once: true },\n );\n })\n .catch((err) => {\n // Swallow rejection so attach never breaks — some platforms/mock reject.\n console.debug('[@ait-co/devtools] setScreenAwakeMode failed:', err);\n });\n}\n","/**\n * @ait-co/devtools/in-app entry point.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n *\n * Phase 1 — gate + browser-side Chii target injection.\n * WebSocket relay, QR/paste UI, and AI-host MCP bin are later phases that\n * require real-device validation and are not included here.\n *\n * This thin entry reads `window.location` and calls the pure\n * {@link evaluateDebugGate} function. All testable logic lives in `./gate.ts`\n * and `./attach.ts`, not here.\n *\n * Layer A of the activation gate (build-time) is NOT enforced in this module.\n * It is the consumer's responsibility: the consumer wraps its\n * `import('@ait-co/devtools/in-app')` call site in `if (__DEBUG_BUILD__) { … }`\n * (see sdk-example `src/main.tsx`), where `__DEBUG_BUILD__` is a\n * consumer-build-time constant. A release consumer build folds that constant\n * to `false` and dead-code-eliminates this whole module. This package is\n * pre-built and ships with `__DEBUG_BUILD__` already resolved at devtools'\n * publish time, so it could never re-evaluate the consumer's build channel —\n * which is exactly why Layer A lives at the consumer guard, not here.\n */\n\nimport { evaluateDebugGate, type GateResult } from './gate.js';\n\nexport { deriveTargetScriptUrl, maybeAttach } from './attach.js';\nexport type { GateInput, GateResult, GateResultAttach, GateResultBlocked } from './gate.js';\nexport { evaluateDebugGate, isPrivateAppsHost, isTrycloudflareHost } from './gate.js';\n\n/**\n * Evaluates the runtime debug activation layers (B and C) against the current\n * page URL.\n *\n * Returns the gate result. Callers can check `result.attach` to decide whether\n * to proceed with debug surface attachment.\n *\n * This function reads `window.location` only — both the hostname (Layer B1\n * host allowlist) and the search params (Layers B2 and C). Layer A\n * (build-time) is enforced by the consumer's `if (__DEBUG_BUILD__)` guard\n * around the import site, not here — see the file-level comment. Consumers\n * call this with no arguments, so the Layer B1 host check is picked up with\n * no change at the call site.\n */\nexport function checkDebugGate(): GateResult {\n return evaluateDebugGate({\n hostname: window.location.hostname,\n searchParams: new URLSearchParams(window.location.search),\n });\n}\n"],"mappings":";;;;;;;;;;;AA2KA,MAAM,2BAA2B;;;;;AAMjC,MAAM,4BAA4B;;;;;;;;;;;;AAalC,SAAgB,kBAAkB,UAA2B;AAC3D,QAAO,SAAS,SAAS,yBAAyB;;;;;;;;;;;;;;;;;;;;;AAsBpD,SAAgB,oBAAoB,UAA2B;AAC7D,QAAO,SAAS,SAAS,0BAA0B;;;;;;;;;;;;;;;;;;;;;;;AAwBrD,SAAgB,kBAAkB,OAA8B;CAe9D,MAAM,WAAW,oBAAoB,MAAM,SAAS;AACpD,KAAI,CAAC,kBAAkB,MAAM,SAAS,IAAI,CAAC,SACzC,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAQ;CAS1C,IAAI,eAAe;AACnB,KAAI,CAAC,UAAU;AACb,iBAAe,MAAM,aAAa,IAAI,gBAAgB,IAAI;AAC1D,MAAI,iBAAiB,GACnB,QAAO;GAAE,QAAQ;GAAO,QAAQ;GAAS;;AAQ7C,KADmB,MAAM,aAAa,IAAI,QAAQ,KAC/B,IACjB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAU;CAM5C,MAAM,WAAW,MAAM,aAAa,IAAI,QAAQ,IAAI;AACpD,KAAI,aAAa,GACf,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAiB;CAGnD,IAAI;AACJ,KAAI;AACF,aAAW,IAAI,IAAI,SAAS;SACtB;AACN,SAAO;GAAE,QAAQ;GAAO,QAAQ;GAAiB;;AAGnD,KAAI,SAAS,aAAa,OACxB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAiB;AAUnD,KAAI,MAAM,mBAAmB,KAAA,GAAW;EACtC,MAAM,OAAO,MAAM,aAAa,IAAI,KAAK,IAAI;AAC7C,MAAI,CAAC,MAAM,eAAe,KAAK,CAC7B,QAAO;GAAE,QAAQ;GAAO,QAAQ;GAAQ;;AAI5C,QAAO;EAAE,QAAQ;EAAM,UAAU,SAAS;EAAM;EAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC3RhE,SAAgB,sBAAsB,UAA0B;CAC9D,MAAM,IAAI,IAAI,IAAI,SAAS;AAC3B,GAAE,WAAW;AACb,GAAE,WAAW;AACb,GAAE,SAAS;AACX,GAAE,OAAO;AACT,QAAO,EAAE,UAAU;;;AAIrB,IAAI,WAAW;;;;;;;;;;;;;;;;;;;;;;;;AAyBf,SAAgB,YAAY,aAAyB,gBAAgB,EAAQ;AAC3E,KAAI,CAAC,WAAW,QAAQ;AACtB,UAAQ,MACN,mEAAmE,WAAW,OAAO,GACtF;AASD,MAAI,WAAW,WAAW,UAAU,OAAO,WAAW,eAAe,OAAO,WAAW,OACrF,QAAO,OAAO,YAAY;GAAE,MAAM;GAA4B,QAAQ;GAAQ,EAAE,IAAI;AAEtF;;AAIF,KAAI,SACF;AAKF,KAAI,OAAO,aAAa,YACtB;CAGF,MAAM,MAAM,sBAAsB,WAAW,SAAS;AAKtD,KADiB,SAAS,cAAiC,eAAe,IAAI,IAAI,KACjE,MAAM;AACrB,aAAW;AACX;;CAGF,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,QAAO,MAAM;AACb,QAAO,QAAQ;AACf,EAAC,SAAS,QAAQ,SAAS,iBAAiB,YAAY,OAAO;AAE/D,YAAW;AAKX,KACE,OAAO,WAAW,eAClB,IAAI,gBAAgB,OAAO,SAAS,OAAO,CAAC,IAAI,cAAc,KAAK,IAEnE;AAGF,oBAAmB,EAAE,SAAS,MAAM,CAAC,CAClC,WAAW;AAGV,SAAO,iBACL,sBACM;AACJ,sBAAmB,EAAE,SAAS,OAAO,CAAC,CAAC,YAAY,GAAG;KAExD,EAAE,MAAM,MAAM,CACf;GACD,CACD,OAAO,QAAQ;AAEd,UAAQ,MAAM,iDAAiD,IAAI;GACnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC7FN,SAAgB,iBAA6B;AAC3C,QAAO,kBAAkB;EACvB,UAAU,OAAO,SAAS;EAC1B,cAAc,IAAI,gBAAgB,OAAO,SAAS,OAAO;EAC1D,CAAC"}
package/dist/mcp/cli.js CHANGED
@@ -1,6 +1,6 @@
1
1
  #!/usr/bin/env node
2
- import { t as loadRelaySecretReadOnly } from "../relay-secret-store-5A7_7zOp.js";
3
- import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-BIrJHsQn.js";
2
+ import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-D0a8VwoR.js";
3
+ import { t as loadRelaySecretReadOnly } from "../relay-secret-store-DBcKWUl9.js";
4
4
  import { createRequire } from "node:module";
5
5
  import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { argv } from "node:process";
@@ -10,12 +10,12 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
10
10
  import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprotocol/sdk/types.js";
11
11
  import { EventEmitter } from "node:events";
12
12
  import { WebSocket } from "ws";
13
+ import { randomBytes } from "node:crypto";
13
14
  import { createServer } from "node:http";
14
15
  import { spawn } from "node:child_process";
15
16
  import net from "node:net";
16
17
  import { homedir, platform } from "node:os";
17
18
  import { join } from "node:path";
18
- import { randomBytes } from "node:crypto";
19
19
  import { Tunnel, bin, install } from "cloudflared";
20
20
  //#region \0rolldown/runtime.js
21
21
  var __require = /* @__PURE__ */ createRequire(import.meta.url);
@@ -285,6 +285,7 @@ var ChiiCdpConnection = class {
285
285
  relayBaseUrl;
286
286
  bufferSize;
287
287
  commandTimeoutMs;
288
+ totpSecret;
288
289
  emitter = new EventEmitter();
289
290
  buffers = /* @__PURE__ */ new Map();
290
291
  targets = /* @__PURE__ */ new Map();
@@ -318,6 +319,7 @@ var ChiiCdpConnection = class {
318
319
  constructor(options) {
319
320
  this.relayBaseUrl = options.relayBaseUrl.replace(/\/$/, "");
320
321
  this.bufferSize = options.bufferSize ?? DEFAULT_BUFFER_SIZE$1;
322
+ this.totpSecret = options.totpSecret;
321
323
  const envMs = process.env.AIT_CDP_COMMAND_TIMEOUT_MS ? Number(process.env.AIT_CDP_COMMAND_TIMEOUT_MS) : void 0;
322
324
  this.commandTimeoutMs = (envMs !== void 0 && Number.isFinite(envMs) && envMs > 0 ? envMs : void 0) ?? options.commandTimeoutMs ?? DEFAULT_COMMAND_TIMEOUT_MS;
323
325
  for (const event of PHASE_1_EVENTS$1) this.buffers.set(event, []);
@@ -455,7 +457,12 @@ var ChiiCdpConnection = class {
455
457
  async _doEnableDomains() {
456
458
  const target = (await this.refreshTargets())[0];
457
459
  if (!target) throw new Error("No mini-app page attached to the Chii relay yet.");
458
- const ws = new WebSocket(`${this.relayBaseUrl.replace(/^http/, "ws")}/client/${`devtools-mcp-${Date.now()}`}?target=${encodeURIComponent(target.id)}`);
460
+ let clientUrl = `${this.relayBaseUrl.replace(/^http/, "ws")}/client/${`devtools-mcp-${Date.now()}`}?target=${encodeURIComponent(target.id)}`;
461
+ if (this.totpSecret) {
462
+ const code = generateTotp(this.totpSecret);
463
+ clientUrl += `&at=${encodeURIComponent(code)}`;
464
+ }
465
+ const ws = new WebSocket(clientUrl);
459
466
  this.ws = ws;
460
467
  await new Promise((resolve, reject) => {
461
468
  ws.once("open", () => resolve());
@@ -1846,7 +1853,10 @@ const en = {
1846
1853
  "launcher.noCamera": "No camera available — paste the URL instead.",
1847
1854
  "launcher.cameraError": "Could not access the camera — paste the URL instead.",
1848
1855
  "launcher.invalidUrlHttps": "Enter a valid https:// URL (the tunnel URL from your terminal).",
1849
- "launcher.invalidUrl": "Enter a valid http(s):// URL."
1856
+ "launcher.invalidUrl": "Enter a valid http(s):// URL.",
1857
+ "launcher.debugAuthFailed": "Debug connection authentication failed",
1858
+ "launcher.debugAuthFailedHint": "The QR code may have expired. Scan a fresh QR code.",
1859
+ "launcher.debugAuthRescanCta": "Scan a new QR"
1850
1860
  };
1851
1861
  //#endregion
1852
1862
  //#region src/i18n/index.ts
@@ -2066,7 +2076,10 @@ const tables = {
2066
2076
  "launcher.noCamera": "카메라를 사용할 수 없습니다 — URL을 직접 붙여넣으세요.",
2067
2077
  "launcher.cameraError": "카메라에 접근할 수 없습니다 — URL을 직접 붙여넣으세요.",
2068
2078
  "launcher.invalidUrlHttps": "올바른 https:// URL을 입력하세요 (터미널의 터널 URL).",
2069
- "launcher.invalidUrl": "올바른 http(s):// URL을 입력하세요."
2079
+ "launcher.invalidUrl": "올바른 http(s):// URL을 입력하세요.",
2080
+ "launcher.debugAuthFailed": "디버그 연결 인증 실패",
2081
+ "launcher.debugAuthFailedHint": "QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.",
2082
+ "launcher.debugAuthRescanCta": "새 QR 스캔하기"
2070
2083
  },
2071
2084
  en
2072
2085
  };
@@ -2177,7 +2190,7 @@ li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
2177
2190
  border-radius: 6px; border: 1px solid #30363d;
2178
2191
  }
2179
2192
  hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
2180
- </style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1><p class="label">deployment: __SAFE_LABEL__</p><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/><section><h2>스캔 절차</h2><ol><li>토스 앱을 실행하세요.</li><li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li><li>팝업이 뜨면 <strong>"토스로 열기"</strong>를 탭하세요.</li><li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li><li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li><li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li></ul></section><hr/><section><h2>URL (fallback)</h2><p class="url-box">__SAFE_ATTACH_URL__</p></section></body></html>`;
2193
+ </style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1><p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>토스 앱을 실행하세요.</li><li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li><li>팝업이 뜨면 <strong>"토스로 열기"</strong>를 탭하세요.</li><li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li><li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li><li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li></ul></section><hr/><section><h2>URL (fallback)</h2><p class="url-box">__SAFE_ATTACH_URL__</p></section></body></html>`;
2181
2194
  const dashboardChromeHtmlEn = `<!DOCTYPE html>
2182
2195
  <html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>AIT Debug Dashboard</title><style>
2183
2196
  *, *::before, *::after { box-sizing: border-box; }
@@ -2243,7 +2256,7 @@ li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
2243
2256
  border-radius: 6px; border: 1px solid #30363d;
2244
2257
  }
2245
2258
  hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
2246
- </style></head><body><h1>AIT Debug Session — QR Scan</h1><p class="label">deployment: __SAFE_LABEL__</p><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/><section><h2>How to scan</h2><ol><li>Open the Toss app.</li><li>Scan the QR code with your phone camera app.</li><li>Tap <strong>"Open in Toss"</strong> when the popup appears.</li><li>The mini-app opens and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)</li><li><strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li><li><strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server</li></ul></section><hr/><section><h2>URL (fallback)</h2><p class="url-box">__SAFE_ATTACH_URL__</p></section></body></html>`;
2259
+ </style></head><body><h1>AIT Debug Session — QR Scan</h1><p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Open the Toss app.</li><li>Scan the QR code with your phone camera app.</li><li>Tap <strong>"Open in Toss"</strong> when the popup appears.</li><li>The mini-app opens and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)</li><li><strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li><li><strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server</li></ul></section><hr/><section><h2>URL (fallback)</h2><p class="url-box">__SAFE_ATTACH_URL__</p></section></body></html>`;
2247
2260
  /** Map from Locale to the precompiled dashboard chrome string. */
2248
2261
  const dashboardChromeByLocale = {
2249
2262
  ko: dashboardChromeHtmlKo,
@@ -2380,10 +2393,22 @@ function buildSseScript(strings) {
2380
2393
  * - __SAFE_LABEL__ : HTML-escaped deploymentId label
2381
2394
  * - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)
2382
2395
  *
2396
+ * SSE 스크립트도 주입 — `#attach-section` hook이 있으면 `/events` push 때 QR이
2397
+ * `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#tunnel-status`·`#pages-list` 등
2398
+ * 나머지 selector는 /attach 페이지에 없으므로 null-guard로 no-op.
2399
+ *
2383
2400
  * SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.
2384
2401
  */
2385
2402
  function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale) {
2386
- return attachChromeByLocale[locale].replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl);
2403
+ const s = resolveLocaleStrings(locale);
2404
+ const filled = attachChromeByLocale[locale].replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl);
2405
+ const sseScript = buildSseScript({
2406
+ tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
2407
+ tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
2408
+ pagesEmpty: JSON.stringify(s("dashboard.pages.empty")),
2409
+ attachHint: JSON.stringify(s("dashboard.attach.hint"))
2410
+ });
2411
+ return filled.replace("</body>", `${sseScript}\n</body>`);
2387
2412
  }
2388
2413
  /**
2389
2414
  * 로컬 HTTP 서버를 127.0.0.1 random port(또는 `AIT_DEBUG_HTTP_PORT` env)로 시작한다.
@@ -3911,7 +3936,7 @@ async function readMcpSdkVersion() {
3911
3936
  * some test environments that skip the build step).
3912
3937
  */
3913
3938
  function readDevtoolsVersion() {
3914
- return "0.1.60";
3939
+ return "0.1.61";
3915
3940
  }
3916
3941
  /**
3917
3942
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -4399,7 +4424,7 @@ function createDebugServer(deps) {
4399
4424
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
4400
4425
  const server = new Server({
4401
4426
  name: "ait-debug",
4402
- version: "0.1.60"
4427
+ version: "0.1.61"
4403
4428
  }, { capabilities: { tools: { listChanged: true } } });
4404
4429
  server.setRequestHandler(ListToolsRequestSchema, () => {
4405
4430
  const conn = router.active;
@@ -4478,7 +4503,7 @@ function createDebugServer(deps) {
4478
4503
  const buildProjectRoot = typeof rawBuildProjectRoot === "string" ? rawBuildProjectRoot : void 0;
4479
4504
  let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
4480
4505
  if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
4481
- const { readRelayUrls } = await import("../relay-url-store-qaoe0zOD.js");
4506
+ const { readRelayUrls } = await import("../relay-url-store-Dq3vpd95.js");
4482
4507
  tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
4483
4508
  }
4484
4509
  if (tunnelHttpUrl === "") return mcpError("build_attach_url(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. 자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.");
@@ -4499,7 +4524,11 @@ function createDebugServer(deps) {
4499
4524
  };
4500
4525
  }
4501
4526
  const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode);
4502
- onAttachUrlBuilt?.(attachUrl);
4527
+ onAttachUrlBuilt?.({
4528
+ kind: "launcher",
4529
+ tunnelHttpUrl,
4530
+ wssUrl: tunnelStatus.wssUrl
4531
+ });
4503
4532
  const relayUrl = tunnelStatus.wssUrl;
4504
4533
  const totp = totpMeta;
4505
4534
  const isMatchingPage = (pages) => pages.length > 0;
@@ -4668,8 +4697,13 @@ function createDebugServer(deps) {
4668
4697
  return `${baseText}\n\nNo page${deploymentId ? ` matching deploymentId=${deploymentId}` : ""} attached within ${timeoutSec}s${observedNote} — call list_pages to retry.`;
4669
4698
  };
4670
4699
  try {
4671
- const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(schemeUrl, getTunnelStatus(), getTotpSecret());
4672
- onAttachUrlBuilt?.(attachUrl);
4700
+ const tunnelForBuild = getTunnelStatus();
4701
+ const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(schemeUrl, tunnelForBuild, getTotpSecret());
4702
+ if (tunnelForBuild.wssUrl !== null) onAttachUrlBuilt?.({
4703
+ kind: "scheme",
4704
+ schemeUrl,
4705
+ wssUrl: tunnelForBuild.wssUrl
4706
+ });
4673
4707
  const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
4674
4708
  const header = "This tool result is shown to the user directly — do NOT re-print the QR below in your reply (it wastes output tokens). Just tell the user to scan the QR in this output (Ctrl+O to expand if collapsed).";
4675
4709
  const guiAvailable = canOpenBrowser();
@@ -4915,6 +4949,18 @@ function makeSingleConnectionRouter(connection) {
4915
4949
  }
4916
4950
  };
4917
4951
  }
4952
+ /**
4953
+ * Re-builds an attach URL from stored components with a FRESHLY-minted TOTP code,
4954
+ * so the dashboard/`/attach` QR is never an expired bake-in (Defect 1).
4955
+ * SECRET-HANDLING: reads AIT_DEBUG_TOTP_SECRET at call time (mirrors tunnel.ts
4956
+ * getDashboardState). The minted code rides inside attachUrl's at= param only —
4957
+ * never logged. generateTotp() relies on its Date.now() default.
4958
+ */
4959
+ function rebuildAttachUrl(parts) {
4960
+ const secret = process.env.AIT_DEBUG_TOTP_SECRET;
4961
+ const code = secret ? generateTotp(secret) : void 0;
4962
+ return parts.kind === "launcher" ? buildLauncherAttachUrl(parts.tunnelHttpUrl, parts.wssUrl, code) : buildDeepLinkAttachUrl(parts.schemeUrl, parts.wssUrl, code);
4963
+ }
4918
4964
  function jsonResult$1(value) {
4919
4965
  return { content: [{
4920
4966
  type: "text",
@@ -5012,7 +5058,10 @@ function startAttachWatcher(connection, server, intervalMs = 1e3, onFirstAttach)
5012
5058
  * the factory is called right after that point (same as before this refactor).
5013
5059
  */
5014
5060
  function createRelayConnection(relayBaseUrl) {
5015
- return new ChiiCdpConnection({ relayBaseUrl });
5061
+ return new ChiiCdpConnection({
5062
+ relayBaseUrl,
5063
+ totpSecret: process.env.AIT_DEBUG_TOTP_SECRET
5064
+ });
5016
5065
  }
5017
5066
  /**
5018
5067
  * AIT source that always forwards over the *currently active* connection
@@ -5201,7 +5250,7 @@ async function readMobileRelayBaseUrl(env = process.env, projectRoot) {
5201
5250
  const envValue = typeof raw === "string" ? raw.trim() : "";
5202
5251
  if (envValue !== "") return envValue;
5203
5252
  if (projectRoot !== void 0) {
5204
- const { readRelayUrls } = await import("../relay-url-store-qaoe0zOD.js");
5253
+ const { readRelayUrls } = await import("../relay-url-store-Dq3vpd95.js");
5205
5254
  const stored = await readRelayUrls({ projectRoot });
5206
5255
  if (stored?.relayBaseUrl !== void 0) return stored.relayBaseUrl;
5207
5256
  }
@@ -5400,7 +5449,7 @@ async function runDebugServer(options = {}) {
5400
5449
  const aitSource = new RoutingAitSource(() => {
5401
5450
  return router.active;
5402
5451
  });
5403
- let lastAttachUrl = null;
5452
+ let lastAttachParts = null;
5404
5453
  const getDashboardState = () => ({
5405
5454
  tunnel: {
5406
5455
  up: router.relayTunnelStatus().up,
@@ -5410,7 +5459,7 @@ async function runDebugServer(options = {}) {
5410
5459
  id: t.id,
5411
5460
  url: t.url
5412
5461
  })),
5413
- attachUrl: lastAttachUrl
5462
+ attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null
5414
5463
  });
5415
5464
  let qrServer;
5416
5465
  try {
@@ -5428,8 +5477,8 @@ async function runDebugServer(options = {}) {
5428
5477
  },
5429
5478
  diagnosticsCollector,
5430
5479
  getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,
5431
- onAttachUrlBuilt: (url) => {
5432
- lastAttachUrl = url;
5480
+ onAttachUrlBuilt: (parts) => {
5481
+ lastAttachParts = parts;
5433
5482
  qrServer?.notifyStateChange();
5434
5483
  }
5435
5484
  });
@@ -5556,7 +5605,7 @@ async function runLocalDebugServer(options = {}) {
5556
5605
  const aitSource = new RoutingAitSource(() => {
5557
5606
  return router.active;
5558
5607
  });
5559
- let lastAttachUrl = null;
5608
+ let lastAttachParts = null;
5560
5609
  const getDashboardState = () => ({
5561
5610
  tunnel: {
5562
5611
  up: router.relayTunnelStatus().up,
@@ -5566,7 +5615,7 @@ async function runLocalDebugServer(options = {}) {
5566
5615
  id: t.id,
5567
5616
  url: t.url
5568
5617
  })),
5569
- attachUrl: lastAttachUrl
5618
+ attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null
5570
5619
  });
5571
5620
  let qrServer;
5572
5621
  try {
@@ -5584,8 +5633,8 @@ async function runLocalDebugServer(options = {}) {
5584
5633
  },
5585
5634
  diagnosticsCollector,
5586
5635
  getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,
5587
- onAttachUrlBuilt: (url) => {
5588
- lastAttachUrl = url;
5636
+ onAttachUrlBuilt: (parts) => {
5637
+ lastAttachParts = parts;
5589
5638
  qrServer?.notifyStateChange();
5590
5639
  }
5591
5640
  });
@@ -5696,7 +5745,7 @@ async function runMobileDebugServer(options = {}) {
5696
5745
  const aitSource = new RoutingAitSource(() => {
5697
5746
  return router.active;
5698
5747
  });
5699
- let lastAttachUrl = null;
5748
+ let lastAttachParts = null;
5700
5749
  const getDashboardState = () => ({
5701
5750
  tunnel: {
5702
5751
  up: router.relayTunnelStatus().up,
@@ -5706,7 +5755,7 @@ async function runMobileDebugServer(options = {}) {
5706
5755
  id: t.id,
5707
5756
  url: t.url
5708
5757
  })),
5709
- attachUrl: lastAttachUrl
5758
+ attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null
5710
5759
  });
5711
5760
  let qrServer;
5712
5761
  try {
@@ -5724,8 +5773,8 @@ async function runMobileDebugServer(options = {}) {
5724
5773
  },
5725
5774
  diagnosticsCollector,
5726
5775
  getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,
5727
- onAttachUrlBuilt: (url) => {
5728
- lastAttachUrl = url;
5776
+ onAttachUrlBuilt: (parts) => {
5777
+ lastAttachParts = parts;
5729
5778
  qrServer?.notifyStateChange();
5730
5779
  }
5731
5780
  });
@@ -6217,7 +6266,7 @@ function createDevServer(deps = {}) {
6217
6266
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
6218
6267
  const server = new Server({
6219
6268
  name: "ait-devtools",
6220
- version: "0.1.60"
6269
+ version: "0.1.61"
6221
6270
  }, { capabilities: { tools: {} } });
6222
6271
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
6223
6272
  server.setRequestHandler(CallToolRequestSchema, async (request) => {