@ait-co/devtools 0.1.59 → 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 (37) 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 +85 -34
  4. package/dist/mcp/cli.js.map +1 -1
  5. package/dist/mcp/server.js +5 -1
  6. package/dist/mcp/server.js.map +1 -1
  7. package/dist/panel/index.js +12 -8
  8. package/dist/panel/index.js.map +1 -1
  9. package/dist/{qr-http-server-Bk-9AO9Y.js → qr-http-server-BLQScKGq.js} +26 -10
  10. package/dist/qr-http-server-BLQScKGq.js.map +1 -0
  11. package/dist/{qr-http-server-C536UmTm.js → qr-http-server-BTFj0LYB.js} +26 -10
  12. package/dist/qr-http-server-BTFj0LYB.js.map +1 -0
  13. package/dist/{qr-http-server-CQZnEAcl.cjs → qr-http-server-CSoE5MdF.cjs} +26 -10
  14. package/dist/qr-http-server-CSoE5MdF.cjs.map +1 -0
  15. package/dist/{qr-http-server-GwRt-B9_.cjs → qr-http-server-DgyGxjET.cjs} +26 -10
  16. package/dist/qr-http-server-DgyGxjET.cjs.map +1 -0
  17. package/dist/{relay-secret-store-5A7_7zOp.js → relay-secret-store-DBcKWUl9.js} +2 -2
  18. package/dist/{relay-secret-store-5A7_7zOp.js.map → relay-secret-store-DBcKWUl9.js.map} +1 -1
  19. package/dist/{relay-url-store-qaoe0zOD.js → relay-url-store-Dq3vpd95.js} +2 -2
  20. package/dist/{relay-url-store-qaoe0zOD.js.map → relay-url-store-Dq3vpd95.js.map} +1 -1
  21. package/dist/totp-CQFmgOhM.js +3 -0
  22. package/dist/{totp-BIrJHsQn.js → totp-D0a8VwoR.js} +1 -1
  23. package/dist/{totp-BIrJHsQn.js.map → totp-D0a8VwoR.js.map} +1 -1
  24. package/dist/{tunnel-COMs-wZU.js → tunnel-CArP5y9b.js} +2 -2
  25. package/dist/{tunnel-COMs-wZU.js.map → tunnel-CArP5y9b.js.map} +1 -1
  26. package/dist/{tunnel-CAaBFOro.cjs → tunnel-DwL0xizq.cjs} +2 -2
  27. package/dist/{tunnel-CAaBFOro.cjs.map → tunnel-DwL0xizq.cjs.map} +1 -1
  28. package/dist/unplugin/index.cjs +1 -1
  29. package/dist/unplugin/index.js +1 -1
  30. package/dist/unplugin/tunnel.cjs +1 -1
  31. package/dist/unplugin/tunnel.js +1 -1
  32. package/package.json +1 -1
  33. package/dist/qr-http-server-Bk-9AO9Y.js.map +0 -1
  34. package/dist/qr-http-server-C536UmTm.js.map +0 -1
  35. package/dist/qr-http-server-CQZnEAcl.cjs.map +0 -1
  36. package/dist/qr-http-server-GwRt-B9_.cjs.map +0 -1
  37. 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());
@@ -1837,9 +1844,8 @@ const en = {
1837
1844
  "attach.faq.totp": "<strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server",
1838
1845
  "attach.url.section": "URL (fallback)",
1839
1846
  "launcher.title": "AITC DevTools Launcher",
1840
- "launcher.description": "Point your camera at the QR shown in your dev server’s terminal, or paste the https://…trycloudflare.com URL below. The dev app then opens full-screen inside this launcher.",
1847
+ "launcher.description": "Scan the terminal QR code or paste the tunnel URL.",
1841
1848
  "launcher.installCta": "Install launcher to your phone",
1842
- "launcher.openOnce": "Open this once without installing",
1843
1849
  "launcher.urlPlaceholder": "https://example.trycloudflare.com",
1844
1850
  "launcher.openBtn": "Open",
1845
1851
  "launcher.scanBtn": "Scan QR with camera",
@@ -1847,7 +1853,10 @@ const en = {
1847
1853
  "launcher.noCamera": "No camera available — paste the URL instead.",
1848
1854
  "launcher.cameraError": "Could not access the camera — paste the URL instead.",
1849
1855
  "launcher.invalidUrlHttps": "Enter a valid https:// URL (the tunnel URL from your terminal).",
1850
- "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"
1851
1860
  };
1852
1861
  //#endregion
1853
1862
  //#region src/i18n/index.ts
@@ -2058,9 +2067,8 @@ const tables = {
2058
2067
  "attach.faq.totp": "<strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인",
2059
2068
  "attach.url.section": "URL (fallback)",
2060
2069
  "launcher.title": "AITC DevTools Launcher",
2061
- "launcher.description": "카메라를 개발 서버 터미널의 QR 대거나, 아래에 https://….trycloudflare.com URL을 붙여넣으세요. 개발 앱이 이 런처 안에서 전체 화면으로 열립니다.",
2070
+ "launcher.description": "터미널 QR 스캔하거나 URL을 입력하세요.",
2062
2071
  "launcher.installCta": "폰에 런처 설치하기",
2063
- "launcher.openOnce": "설치 없이 한 번만 열기",
2064
2072
  "launcher.urlPlaceholder": "https://example.trycloudflare.com",
2065
2073
  "launcher.openBtn": "Open",
2066
2074
  "launcher.scanBtn": "QR 카메라로 스캔",
@@ -2068,7 +2076,10 @@ const tables = {
2068
2076
  "launcher.noCamera": "카메라를 사용할 수 없습니다 — URL을 직접 붙여넣으세요.",
2069
2077
  "launcher.cameraError": "카메라에 접근할 수 없습니다 — URL을 직접 붙여넣으세요.",
2070
2078
  "launcher.invalidUrlHttps": "올바른 https:// URL을 입력하세요 (터미널의 터널 URL).",
2071
- "launcher.invalidUrl": "올바른 http(s):// URL을 입력하세요."
2079
+ "launcher.invalidUrl": "올바른 http(s):// URL을 입력하세요.",
2080
+ "launcher.debugAuthFailed": "디버그 연결 인증 실패",
2081
+ "launcher.debugAuthFailedHint": "QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.",
2082
+ "launcher.debugAuthRescanCta": "새 QR 스캔하기"
2072
2083
  },
2073
2084
  en
2074
2085
  };
@@ -2179,7 +2190,7 @@ li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
2179
2190
  border-radius: 6px; border: 1px solid #30363d;
2180
2191
  }
2181
2192
  hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
2182
- </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>`;
2183
2194
  const dashboardChromeHtmlEn = `<!DOCTYPE html>
2184
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>
2185
2196
  *, *::before, *::after { box-sizing: border-box; }
@@ -2245,7 +2256,7 @@ li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
2245
2256
  border-radius: 6px; border: 1px solid #30363d;
2246
2257
  }
2247
2258
  hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
2248
- </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>`;
2249
2260
  /** Map from Locale to the precompiled dashboard chrome string. */
2250
2261
  const dashboardChromeByLocale = {
2251
2262
  ko: dashboardChromeHtmlKo,
@@ -2382,10 +2393,22 @@ function buildSseScript(strings) {
2382
2393
  * - __SAFE_LABEL__ : HTML-escaped deploymentId label
2383
2394
  * - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)
2384
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
+ *
2385
2400
  * SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.
2386
2401
  */
2387
2402
  function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale) {
2388
- 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>`);
2389
2412
  }
2390
2413
  /**
2391
2414
  * 로컬 HTTP 서버를 127.0.0.1 random port(또는 `AIT_DEBUG_HTTP_PORT` env)로 시작한다.
@@ -2926,6 +2949,10 @@ const DEBUG_TOOL_DEFINITIONS = [
2926
2949
  open_in_browser: {
2927
2950
  type: "boolean",
2928
2951
  description: "If true (default), render the QR as a PNG and open it in the OS default browser. Only works when the MCP server is running on a local GUI machine — headless or remote container environments should set this to false to use the text QR fallback."
2952
+ },
2953
+ projectRoot: {
2954
+ type: "string",
2955
+ description: "Absolute path to the mini-app project root (the directory containing its package.json and .ait_urls). When AIT_TUNNEL_BASE_URL is unset (env 2 / relay-mobile only), the daemon reads the app tunnel URL from <projectRoot>/.ait_urls written by the dev server (tunnel:{cdp:true}). Pass this because the daemon's own cwd is fixed at launch. Omit when AIT_TUNNEL_BASE_URL is set explicitly."
2929
2956
  }
2930
2957
  },
2931
2958
  required: []
@@ -3909,7 +3936,7 @@ async function readMcpSdkVersion() {
3909
3936
  * some test environments that skip the build step).
3910
3937
  */
3911
3938
  function readDevtoolsVersion() {
3912
- return "0.1.59";
3939
+ return "0.1.61";
3913
3940
  }
3914
3941
  /**
3915
3942
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -4397,7 +4424,7 @@ function createDebugServer(deps) {
4397
4424
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
4398
4425
  const server = new Server({
4399
4426
  name: "ait-debug",
4400
- version: "0.1.59"
4427
+ version: "0.1.61"
4401
4428
  }, { capabilities: { tools: { listChanged: true } } });
4402
4429
  server.setRequestHandler(ListToolsRequestSchema, () => {
4403
4430
  const conn = router.active;
@@ -4476,7 +4503,7 @@ function createDebugServer(deps) {
4476
4503
  const buildProjectRoot = typeof rawBuildProjectRoot === "string" ? rawBuildProjectRoot : void 0;
4477
4504
  let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
4478
4505
  if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
4479
- const { readRelayUrls } = await import("../relay-url-store-qaoe0zOD.js");
4506
+ const { readRelayUrls } = await import("../relay-url-store-Dq3vpd95.js");
4480
4507
  tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
4481
4508
  }
4482
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 환경변수로 직접 전달하세요.");
@@ -4497,7 +4524,11 @@ function createDebugServer(deps) {
4497
4524
  };
4498
4525
  }
4499
4526
  const attachUrl = buildLauncherAttachUrl(tunnelHttpUrl, tunnelStatus.wssUrl, totpCode);
4500
- onAttachUrlBuilt?.(attachUrl);
4527
+ onAttachUrlBuilt?.({
4528
+ kind: "launcher",
4529
+ tunnelHttpUrl,
4530
+ wssUrl: tunnelStatus.wssUrl
4531
+ });
4501
4532
  const relayUrl = tunnelStatus.wssUrl;
4502
4533
  const totp = totpMeta;
4503
4534
  const isMatchingPage = (pages) => pages.length > 0;
@@ -4666,8 +4697,13 @@ function createDebugServer(deps) {
4666
4697
  return `${baseText}\n\nNo page${deploymentId ? ` matching deploymentId=${deploymentId}` : ""} attached within ${timeoutSec}s${observedNote} — call list_pages to retry.`;
4667
4698
  };
4668
4699
  try {
4669
- const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(schemeUrl, getTunnelStatus(), getTotpSecret());
4670
- 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
+ });
4671
4707
  const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
4672
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).";
4673
4709
  const guiAvailable = canOpenBrowser();
@@ -4913,6 +4949,18 @@ function makeSingleConnectionRouter(connection) {
4913
4949
  }
4914
4950
  };
4915
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
+ }
4916
4964
  function jsonResult$1(value) {
4917
4965
  return { content: [{
4918
4966
  type: "text",
@@ -5010,7 +5058,10 @@ function startAttachWatcher(connection, server, intervalMs = 1e3, onFirstAttach)
5010
5058
  * the factory is called right after that point (same as before this refactor).
5011
5059
  */
5012
5060
  function createRelayConnection(relayBaseUrl) {
5013
- return new ChiiCdpConnection({ relayBaseUrl });
5061
+ return new ChiiCdpConnection({
5062
+ relayBaseUrl,
5063
+ totpSecret: process.env.AIT_DEBUG_TOTP_SECRET
5064
+ });
5014
5065
  }
5015
5066
  /**
5016
5067
  * AIT source that always forwards over the *currently active* connection
@@ -5199,7 +5250,7 @@ async function readMobileRelayBaseUrl(env = process.env, projectRoot) {
5199
5250
  const envValue = typeof raw === "string" ? raw.trim() : "";
5200
5251
  if (envValue !== "") return envValue;
5201
5252
  if (projectRoot !== void 0) {
5202
- const { readRelayUrls } = await import("../relay-url-store-qaoe0zOD.js");
5253
+ const { readRelayUrls } = await import("../relay-url-store-Dq3vpd95.js");
5203
5254
  const stored = await readRelayUrls({ projectRoot });
5204
5255
  if (stored?.relayBaseUrl !== void 0) return stored.relayBaseUrl;
5205
5256
  }
@@ -5398,7 +5449,7 @@ async function runDebugServer(options = {}) {
5398
5449
  const aitSource = new RoutingAitSource(() => {
5399
5450
  return router.active;
5400
5451
  });
5401
- let lastAttachUrl = null;
5452
+ let lastAttachParts = null;
5402
5453
  const getDashboardState = () => ({
5403
5454
  tunnel: {
5404
5455
  up: router.relayTunnelStatus().up,
@@ -5408,7 +5459,7 @@ async function runDebugServer(options = {}) {
5408
5459
  id: t.id,
5409
5460
  url: t.url
5410
5461
  })),
5411
- attachUrl: lastAttachUrl
5462
+ attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null
5412
5463
  });
5413
5464
  let qrServer;
5414
5465
  try {
@@ -5426,8 +5477,8 @@ async function runDebugServer(options = {}) {
5426
5477
  },
5427
5478
  diagnosticsCollector,
5428
5479
  getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,
5429
- onAttachUrlBuilt: (url) => {
5430
- lastAttachUrl = url;
5480
+ onAttachUrlBuilt: (parts) => {
5481
+ lastAttachParts = parts;
5431
5482
  qrServer?.notifyStateChange();
5432
5483
  }
5433
5484
  });
@@ -5554,7 +5605,7 @@ async function runLocalDebugServer(options = {}) {
5554
5605
  const aitSource = new RoutingAitSource(() => {
5555
5606
  return router.active;
5556
5607
  });
5557
- let lastAttachUrl = null;
5608
+ let lastAttachParts = null;
5558
5609
  const getDashboardState = () => ({
5559
5610
  tunnel: {
5560
5611
  up: router.relayTunnelStatus().up,
@@ -5564,7 +5615,7 @@ async function runLocalDebugServer(options = {}) {
5564
5615
  id: t.id,
5565
5616
  url: t.url
5566
5617
  })),
5567
- attachUrl: lastAttachUrl
5618
+ attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null
5568
5619
  });
5569
5620
  let qrServer;
5570
5621
  try {
@@ -5582,8 +5633,8 @@ async function runLocalDebugServer(options = {}) {
5582
5633
  },
5583
5634
  diagnosticsCollector,
5584
5635
  getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,
5585
- onAttachUrlBuilt: (url) => {
5586
- lastAttachUrl = url;
5636
+ onAttachUrlBuilt: (parts) => {
5637
+ lastAttachParts = parts;
5587
5638
  qrServer?.notifyStateChange();
5588
5639
  }
5589
5640
  });
@@ -5694,7 +5745,7 @@ async function runMobileDebugServer(options = {}) {
5694
5745
  const aitSource = new RoutingAitSource(() => {
5695
5746
  return router.active;
5696
5747
  });
5697
- let lastAttachUrl = null;
5748
+ let lastAttachParts = null;
5698
5749
  const getDashboardState = () => ({
5699
5750
  tunnel: {
5700
5751
  up: router.relayTunnelStatus().up,
@@ -5704,7 +5755,7 @@ async function runMobileDebugServer(options = {}) {
5704
5755
  id: t.id,
5705
5756
  url: t.url
5706
5757
  })),
5707
- attachUrl: lastAttachUrl
5758
+ attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null
5708
5759
  });
5709
5760
  let qrServer;
5710
5761
  try {
@@ -5722,8 +5773,8 @@ async function runMobileDebugServer(options = {}) {
5722
5773
  },
5723
5774
  diagnosticsCollector,
5724
5775
  getTotpSecret: () => process.env.AIT_DEBUG_TOTP_SECRET,
5725
- onAttachUrlBuilt: (url) => {
5726
- lastAttachUrl = url;
5776
+ onAttachUrlBuilt: (parts) => {
5777
+ lastAttachParts = parts;
5727
5778
  qrServer?.notifyStateChange();
5728
5779
  }
5729
5780
  });
@@ -6215,7 +6266,7 @@ function createDevServer(deps = {}) {
6215
6266
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
6216
6267
  const server = new Server({
6217
6268
  name: "ait-devtools",
6218
- version: "0.1.59"
6269
+ version: "0.1.61"
6219
6270
  }, { capabilities: { tools: {} } });
6220
6271
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
6221
6272
  server.setRequestHandler(CallToolRequestSchema, async (request) => {