@ait-co/devtools 0.1.79 → 0.1.81
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.
- package/dist/devtools-opener-BbUXBzgA.js.map +1 -1
- package/dist/devtools-opener-Bp671YXu.cjs.map +1 -1
- package/dist/devtools-opener-D84kZFtR.js.map +1 -1
- package/dist/devtools-opener-h6A-UjzC.cjs.map +1 -1
- package/dist/mcp/cli.js +214 -55
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/panel/index.js +6 -2
- package/dist/panel/index.js.map +1 -1
- package/dist/{qr-http-server-D2d44bv7.js → qr-http-server-DJ5K3Odk.js} +34 -1
- package/dist/qr-http-server-DJ5K3Odk.js.map +1 -0
- package/dist/{qr-http-server-Dx7KnQtg.js → qr-http-server-DkOFfZsR.js} +34 -1
- package/dist/qr-http-server-DkOFfZsR.js.map +1 -0
- package/dist/{qr-http-server-DOOLghY0.cjs → qr-http-server-Dkx2-pKF.cjs} +34 -1
- package/dist/qr-http-server-Dkx2-pKF.cjs.map +1 -0
- package/dist/{qr-http-server-oENyLvn9.cjs → qr-http-server-MIUHaiYw.cjs} +34 -1
- package/dist/qr-http-server-MIUHaiYw.cjs.map +1 -0
- package/dist/{relay-url-store-Cx_SqWtl.cjs → relay-url-store-BiEK9BN1.cjs} +3 -1
- package/dist/relay-url-store-BiEK9BN1.cjs.map +1 -0
- package/dist/{relay-url-store-CV8nScsn.js → relay-url-store-DH8-VUFc.js} +3 -1
- package/dist/relay-url-store-DH8-VUFc.js.map +1 -0
- package/dist/{relay-url-store-B_wrNe5A.js → relay-url-store-RKcao_yG.js} +6 -1
- package/dist/relay-url-store-RKcao_yG.js.map +1 -0
- package/dist/{tunnel-8h2r-ouK.js → tunnel-BTlq1mmH.js} +2 -2
- package/dist/{tunnel-8h2r-ouK.js.map → tunnel-BTlq1mmH.js.map} +1 -1
- package/dist/{tunnel-CInRDnKE.cjs → tunnel-C-AFdAVL.cjs} +2 -2
- package/dist/{tunnel-CInRDnKE.cjs.map → tunnel-C-AFdAVL.cjs.map} +1 -1
- package/dist/unplugin/index.cjs +6 -3
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +6 -3
- package/dist/unplugin/index.js.map +1 -1
- package/dist/unplugin/tunnel.cjs +1 -1
- package/dist/unplugin/tunnel.js +1 -1
- package/package.json +1 -1
- package/dist/qr-http-server-D2d44bv7.js.map +0 -1
- package/dist/qr-http-server-DOOLghY0.cjs.map +0 -1
- package/dist/qr-http-server-Dx7KnQtg.js.map +0 -1
- package/dist/qr-http-server-oENyLvn9.cjs.map +0 -1
- package/dist/relay-url-store-B_wrNe5A.js.map +0 -1
- package/dist/relay-url-store-CV8nScsn.js.map +0 -1
- package/dist/relay-url-store-Cx_SqWtl.cjs.map +0 -1
package/dist/unplugin/index.js
CHANGED
|
@@ -220,11 +220,12 @@ const aitDevtoolsPlugin = createUnplugin((options) => {
|
|
|
220
220
|
console.warn("[@ait-co/devtools] tunnel: could not determine the dev server port; skipping.");
|
|
221
221
|
return;
|
|
222
222
|
}
|
|
223
|
-
import("../tunnel-
|
|
223
|
+
import("../tunnel-BTlq1mmH.js").then(async ({ startQuickTunnel, printTunnelBanner, startTunnelDashboard }) => {
|
|
224
224
|
const t = await startQuickTunnel(port);
|
|
225
225
|
tunnel = t;
|
|
226
226
|
let relayWssUrl;
|
|
227
227
|
let relayHttpUrl;
|
|
228
|
+
let relayLocalHttpUrl;
|
|
228
229
|
if (tunnelConfig.cdp) try {
|
|
229
230
|
const { ensureRelaySecret } = await import("../relay-secret-store-6pPzLkUO.js");
|
|
230
231
|
await ensureRelaySecret({ projectRoot: server.config.root });
|
|
@@ -248,6 +249,7 @@ const aitDevtoolsPlugin = createUnplugin((options) => {
|
|
|
248
249
|
relayTunnel = rt;
|
|
249
250
|
relayHttpUrl = rt.url;
|
|
250
251
|
relayWssUrl = rt.url.replace(/^https:/, "wss:");
|
|
252
|
+
relayLocalHttpUrl = `http://127.0.0.1:${r.port}`;
|
|
251
253
|
} catch (err) {
|
|
252
254
|
console.warn(`[@ait-co/devtools] tunnel: CDP relay not started — screen preview works without on-device debugging: ${err instanceof Error ? err.message : String(err)}`);
|
|
253
255
|
}
|
|
@@ -264,11 +266,12 @@ const aitDevtoolsPlugin = createUnplugin((options) => {
|
|
|
264
266
|
relayWssUrl,
|
|
265
267
|
name: tunnelAppName
|
|
266
268
|
});
|
|
267
|
-
const { writeRelayUrls, deleteRelayUrls } = await import("../relay-url-store-
|
|
269
|
+
const { writeRelayUrls, deleteRelayUrls } = await import("../relay-url-store-DH8-VUFc.js");
|
|
268
270
|
await writeRelayUrls({
|
|
269
271
|
projectRoot: server.config.root,
|
|
270
272
|
tunnelBaseUrl: t.url,
|
|
271
|
-
...relayHttpUrl !== void 0 ? { relayBaseUrl: relayHttpUrl } : {}
|
|
273
|
+
...relayHttpUrl !== void 0 ? { relayBaseUrl: relayHttpUrl } : {},
|
|
274
|
+
...relayLocalHttpUrl !== void 0 ? { relayLocalUrl: relayLocalHttpUrl } : {}
|
|
272
275
|
});
|
|
273
276
|
relayUrlDeleteFn = (root) => deleteRelayUrls({ projectRoot: root });
|
|
274
277
|
if (relayWssUrl) qrDashboard = await startTunnelDashboard({
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/shared/parent-watcher.ts","../../src/unplugin/index.ts"],"sourcesContent":["/**\n * Shared parent-PID watcher — used by both the MCP debug daemon and the\n * unplugin tunnel path to self-terminate when the parent process (e.g. Claude\n * Code, vite) has died or been reparented without sending SIGTERM/SIGHUP.\n *\n * Intentionally react-free and Node-stdlib-only so this module is safe to\n * import from the MCP daemon bundle (`dist/mcp/cli.js`) without violating the\n * install-graph invariant.\n */\n\n// ---------------------------------------------------------------------------\n// isPidAlive — extracted from src/mcp/server-lock.ts\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when the given PID refers to a running process.\n *\n * Uses `process.kill(pid, 0)` — a no-op signal that succeeds when the process\n * exists and we have permission to signal it; throws ESRCH when it doesn't exist.\n */\nexport function isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err: unknown) {\n // ESRCH = no such process → stale lock.\n // EPERM = process exists but we can't signal it (still alive).\n if ((err as NodeJS.ErrnoException).code === 'EPERM') return true;\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// startParentWatcher — extracted from src/mcp/debug-server.ts\n// ---------------------------------------------------------------------------\n\n/**\n * Starts a periodic watcher that detects when the parent process (e.g. Claude\n * Code) has died without sending SIGTERM/SIGHUP, and calls `onOrphaned` so the\n * daemon can self-terminate rather than running as a zombie.\n *\n * Mirrors the `startAttachWatcher` pattern: `setInterval`-based, returns\n * `{ stop(): void }`, injectable deps for testability.\n *\n * @param onOrphaned - Called once when the parent is gone.\n * @param opts.intervalMs - Poll interval in milliseconds (default 5 000).\n * @param opts.initialPpid - Parent PID to watch (default `process.ppid`).\n * @param opts.isAlive - Predicate to test if a PID is running (default `isPidAlive`).\n * @param opts.getPpid - Supplier of current ppid (default `() => process.ppid`).\n * Detects ppid changes as well as death.\n * @param opts.log - Logger (default `process.stderr.write`).\n *\n * @returns `stop` — call during shutdown to clear the interval.\n */\nexport function startParentWatcher(\n onOrphaned: () => void,\n opts?: {\n intervalMs?: number;\n initialPpid?: number;\n isAlive?: (pid: number) => boolean;\n getPpid?: () => number;\n log?: (msg: string) => void;\n },\n): { stop(): void } {\n const {\n intervalMs = 5_000,\n initialPpid = process.ppid,\n isAlive = isPidAlive,\n getPpid = () => process.ppid,\n log = (msg: string) => process.stderr.write(msg),\n } = opts ?? {};\n\n // PID 1 is init/launchd — running under a process manager or as a detached\n // daemon. There is no meaningful parent to watch; skip the watcher entirely.\n if (initialPpid <= 1) {\n log('[ait-debug] parent-pid watcher: no parent to watch (ppid<=1), skipping\\n');\n return { stop() {} };\n }\n\n let fired = false;\n\n const handle = setInterval(() => {\n if (fired) return;\n\n const currentPpid = getPpid();\n const orphaned = currentPpid !== initialPpid || !isAlive(initialPpid);\n\n if (orphaned) {\n fired = true;\n clearInterval(handle);\n log(\n `[ait-debug] parent-pid watcher: parent PID ${initialPpid} is gone (currentPpid=${currentPpid}) — shutting down\\n`,\n );\n onOrphaned();\n }\n }, intervalMs);\n\n return {\n stop() {\n clearInterval(handle);\n },\n };\n}\n","/**\n * @ait-co/devtools unplugin\n *\n * 모든 주요 번들러를 지원하는 단일 플러그인.\n * @apps-in-toss/web-framework → @ait-co/devtools/mock 으로 alias 설정.\n *\n * Usage:\n * import aitDevtools from '@ait-co/devtools/unplugin';\n *\n * // Vite\n * export default { plugins: [aitDevtools.vite()] };\n *\n * // Webpack / Next.js\n * config.plugins.push(aitDevtools.webpack());\n *\n * // Rspack\n * config.plugins.push(aitDevtools.rspack());\n *\n * // esbuild\n * { plugins: [aitDevtools.esbuild()] }\n *\n * // Rollup\n * { plugins: [aitDevtools.rollup()] }\n */\n\nimport { fileURLToPath } from 'node:url';\nimport { createUnplugin } from 'unplugin';\nimport { startParentWatcher } from '../shared/parent-watcher.js';\n\n/**\n * Resolve `@ait-co/devtools/mock` to its real file path at plugin-load time.\n *\n * Returning the bare specifier from `resolveId` would stop the bundler from\n * walking node_modules for it — Vite 8+ treats such a non-null string as the\n * final resolved id and serves it via the virtual `/@id/` prefix, which 404s\n * because we don't provide a `load` hook. Resolving to an absolute path here\n * lets every supported bundler load the file the normal way.\n */\nconst MOCK_PATH = (() => {\n try {\n return fileURLToPath(import.meta.resolve('@ait-co/devtools/mock'));\n } catch {\n // Fallback for runtimes where `import.meta.resolve` is unavailable.\n return '@ait-co/devtools/mock';\n }\n})();\n\nexport interface AitDevtoolsOptions {\n /**\n * 패널 자동 주입 여부 (default: true)\n * true이면 진입점에 floating panel import를 자동 추가한다.\n */\n panel?: boolean;\n /**\n * production 환경에서도 devtools를 강제로 활성화 (default: false)\n */\n forceEnable?: boolean;\n /**\n * mock alias 활성화 여부. default: true (development), false (production + forceEnable)\n */\n mock?: boolean;\n /**\n * Vite dev server에 MCP state endpoint를 추가할지 여부 (default: false).\n *\n * `true`로 설정하면:\n * - GET /api/ait-devtools/state — 마지막으로 브라우저가 push한 mock state 스냅샷 반환\n * - POST /api/ait-devtools/state — 브라우저 panel이 상태 변경 시 자동 push (panel 내부 처리)\n *\n * 이 endpoint를 `@ait-co/devtools` MCP stdio server가 읽어 AI 에이전트에 mock state를 노출한다.\n * Vite 전용: webpack/rspack/esbuild/rollup 환경에서는 무시된다.\n */\n mcp?: boolean;\n /**\n * Vite dev 서버를 Cloudflare quick tunnel(`*.trycloudflare.com`, 계정 불필요)로\n * 외부 노출해 실제 폰에서 미리보기. **Vite dev 모드 전용** — production은\n * `forceEnable`이어도 터널을 띄우지 않는다 (의도치 않은 노출 방지). 다른 번들러는\n * 무시. `true`면 기본 동작, 객체로 세부 설정 가능.\n */\n tunnel?:\n | boolean\n | {\n /** 노출할 포트 (미지정 시 dev 서버가 실제 listen한 포트 자동 감지). */\n port?: number;\n /** 터미널 ASCII QR 출력 (default: true). */\n qr?: boolean;\n /**\n * 환경 2(실기기 PWA)에 CDP 디버깅 배선 (default: false).\n *\n * `true`면 dev 서버 HTTP 터널과 **별도로** Chii relay를 띄우고 그 relay에\n * 두 번째 quick tunnel을 붙여, launcher QR deep-link에 `&debug=1&relay=<wss>`를\n * 실어 보낸다. 폰의 PWA iframe이 in-app debug gate를 통과해 target.js를 주입받고,\n * AI host MCP가 그 relay에 client로 붙으면 실기기 WebKit 위에서 CDP 디버깅이 열린다.\n * mock SDK는 그대로라 `call_sdk`는 환경 2에서 mock을 친다 (fidelity 사다리의\n * 설계 의도 — SDK fidelity가 필요하면 환경 3로 올라간다).\n */\n cdp?: boolean;\n };\n}\n\nconst FRAMEWORK_ID = '@apps-in-toss/web-framework';\nconst BRIDGE_ID = '@apps-in-toss/web-bridge'; // back-compat (2.x)\nconst ANALYTICS_ID = '@apps-in-toss/web-analytics'; // back-compat (2.x)\nconst WEBVIEW_BRIDGE_ID = '@apps-in-toss/webview-bridge'; // 3.0+\n\n/** MCP state endpoint path — browser panel POSTs here, MCP server GETs here */\nconst MCP_STATE_PATH = '/api/ait-devtools/state';\n\n/**\n * Resolves the effective tunnel option (#425).\n *\n * An explicit `tunnel` value (including `false`) always takes priority over\n * env vars — the `??` operator means `undefined` (= omitted) falls through,\n * but `false` / `true` / an object are preserved as-is (non-breaking).\n *\n * When the option is omitted:\n * - `AIT_TUNNEL=1` enables the base screen-preview tunnel.\n * - `AIT_TUNNEL_CDP=1` (requires `AIT_TUNNEL`) upgrades to the CDP relay.\n * - Neither set → `false` (disabled).\n *\n * Extracted as a pure function so it can be unit-tested without standing up\n * a full Vite dev server.\n *\n * @param explicit - The `tunnel` option as passed by the consumer (or `undefined` when omitted).\n * @param env - The process environment (injectable for testing).\n */\nexport function resolveTunnelOption(\n explicit: AitDevtoolsOptions['tunnel'],\n env: Record<string, string | undefined>,\n): AitDevtoolsOptions['tunnel'] {\n return explicit ?? (env.AIT_TUNNEL ? { cdp: !!env.AIT_TUNNEL_CDP } : false);\n}\n\nconst aitDevtoolsPlugin = createUnplugin((options?: AitDevtoolsOptions) => {\n const isDev = process.env.NODE_ENV !== 'production';\n const shouldEnable = isDev || (options?.forceEnable ?? false);\n const shouldMock = shouldEnable && (options?.mock ?? isDev);\n const shouldPanel = shouldEnable && (options?.panel ?? true);\n const shouldMcp = shouldEnable && (options?.mcp ?? false);\n\n // In-memory store for the last state snapshot pushed by the browser panel.\n // Only allocated when mcp: true to avoid any overhead in the common case.\n let lastState: string | null = null;\n\n // Tunnel is dev-only and Vite-only. Never under production — even with\n // forceEnable — so a production build can't accidentally expose itself.\n //\n // Tunnel toggle resolution (#425): an explicit `tunnel` option always wins;\n // when omitted, fall back to the AIT_TUNNEL / AIT_TUNNEL_CDP env vars so a\n // consumer needs no `tunnel:` line in vite.config to enable env-2 preview.\n // AIT_TUNNEL gates the base (screen preview); AIT_TUNNEL_CDP upgrades to the\n // CDP relay. Production safety is unchanged — the existing\n // `shouldTunnel = isDev && !!tunnelOpt` guard below still blocks prod builds.\n const tunnelOpt = resolveTunnelOption(options?.tunnel, process.env);\n const shouldTunnel = isDev && !!tunnelOpt;\n const tunnelConfig = typeof tunnelOpt === 'object' ? tunnelOpt : {};\n\n return {\n name: 'ait-co-devtools',\n enforce: 'pre' as const,\n\n resolveId(id: string) {\n if (!shouldMock) return null;\n // @apps-in-toss/web-framework → @ait-co/devtools/mock (absolute path)\n if (\n id === FRAMEWORK_ID ||\n id === WEBVIEW_BRIDGE_ID ||\n id === BRIDGE_ID ||\n id === ANALYTICS_ID\n ) {\n return MOCK_PATH;\n }\n return null;\n },\n\n transformInclude(id: string) {\n if (!shouldPanel) return false;\n // 진입점 파일에만 패널 import를 주입\n return (\n /\\.(tsx?|jsx?)$/.test(id) &&\n /\\/(main|index|entry|app)\\.[tj]sx?$/i.test(id) &&\n !id.includes('node_modules')\n );\n },\n\n transform(code: string) {\n // transformInclude가 이미 shouldPanel을 확인하지만, 안전망으로 유지\n if (!shouldPanel) return null;\n // 이미 패널이 import 되어있으면 스킵\n if (code.includes('@ait-co/devtools/panel')) return null;\n // transformInclude가 진입점 파일만 통과시키므로 바로 prepend\n return `import '@ait-co/devtools/panel';\\n${code}`;\n },\n\n // Vite-only: register the MCP state HTTP endpoint on the dev server, and\n // optionally start a Cloudflare quick tunnel once the dev server is listening.\n // Non-Vite bundlers do not have a dev server concept so this is silently\n // skipped (unplugin passes `vite` key only when building for Vite).\n vite: {\n config() {\n if (!shouldTunnel) return;\n // Vite blocks requests whose Host header isn't in `server.allowedHosts`\n // (defaults to localhost only). The quick-tunnel hostname is random per\n // run, so allow the whole `.trycloudflare.com` suffix while the tunnel\n // is on. (A leading `.` makes Vite match the domain and its subdomains.)\n return { server: { allowedHosts: ['.trycloudflare.com'] } };\n },\n\n configureServer(server: import('vite').ViteDevServer) {\n // MCP state endpoint: browser panel POSTs state here, MCP stdio server GETs it.\n if (shouldMcp) {\n server.middlewares.use(MCP_STATE_PATH, (req, res) => {\n // Allow Claude Code / AI agents (running locally) to read state\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n\n if (req.method === 'OPTIONS') {\n res.writeHead(204);\n res.end();\n return;\n }\n\n if (req.method === 'GET') {\n if (lastState === null) {\n res.writeHead(503, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n error: 'No state received yet. Open the app in a browser first.',\n }),\n );\n return;\n }\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(lastState);\n return;\n }\n\n if (req.method === 'POST') {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n try {\n const body = Buffer.concat(chunks).toString('utf-8');\n // Validate it's parseable JSON before caching\n JSON.parse(body);\n lastState = body;\n res.writeHead(204);\n res.end();\n } catch {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Invalid JSON' }));\n }\n });\n return;\n }\n\n res.writeHead(405, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Method not allowed' }));\n });\n }\n\n // Tunnel: start a Cloudflare quick tunnel once the dev server is listening.\n if (shouldTunnel) {\n let tunnel: { stop: () => void } | null = null;\n // env-2 CDP wiring (tunnel.cdp): a second tunnel + Chii relay, torn\n // down alongside the HTTP tunnel. Fire-and-forget close on teardown.\n let relayTunnel: { stop: () => void } | null = null;\n let relay: { close: () => Promise<void> } | null = null;\n // env-2 HTML dashboard (issue #408): local 127.0.0.1 HTTP server that\n // serves the QR + connect-steps + FAQ page (env 3/4 UX parity), opened\n // in the browser when CDP is wired + GUI present. Torn down with the\n // tunnel. Only set when the dashboard actually started.\n let qrDashboard: { close: () => Promise<void> } | null = null;\n // env-2 URL file store (#424): captured after the first writeRelayUrls\n // call so cleanup() can call deleteRelayUrls without a re-import.\n // SECRET-HANDLING: the stored function reference never carries URL values.\n let relayUrlDeleteFn: ((projectRoot: string) => Promise<void>) | null = null;\n // #420: parent-PID watcher — self-terminate when vite's parent dies so\n // cloudflared children don't become zombies holding stale tunnels.\n let parentWatcher: { stop(): void } | null = null;\n const httpServer = server.httpServer;\n\n httpServer?.once('listening', () => {\n const address = httpServer?.address();\n const port =\n tunnelConfig.port ??\n (address && typeof address === 'object' ? address.port : undefined);\n if (!port) {\n console.warn(\n '[@ait-co/devtools] tunnel: could not determine the dev server port; skipping.',\n );\n return;\n }\n // Dynamic import keeps `cloudflared` / `qrcode-terminal` off the\n // module graph unless the tunnel is actually used.\n import('./tunnel.js')\n .then(async ({ startQuickTunnel, printTunnelBanner, startTunnelDashboard }) => {\n const t = await startQuickTunnel(port);\n tunnel = t;\n\n // env-2 CDP: boot a Chii relay (OS-assigned local port) and a\n // second quick tunnel to it. The relay's https tunnel URL becomes\n // the `wss://` relay the launcher QR carries (&debug=1&relay=).\n let relayWssUrl: string | undefined;\n // SECRET-HANDLING: relayHttpUrl carries the relay host — never logged.\n let relayHttpUrl: string | undefined;\n if (tunnelConfig.cdp) {\n try {\n // Relay-auth baseline (issue #250): the env-2 CDP relay is\n // reachable over a public `*.trycloudflare.com` tunnel, so a\n // configured TOTP secret is MANDATORY and the relay enforces\n // it on every WS upgrade.\n //\n // First-run auto-mint (issue #394, project-local #396): if\n // AIT_DEBUG_TOTP_SECRET is not yet set, ensureRelaySecret()\n // mints a 256-bit random secret, persists it to the project-\n // local file <project>/.ait_relay (0600, anchored at the\n // nearest package.json directory above server.config.root),\n // and injects it into process.env so the following\n // assertRelayAuthConfigured() call succeeds. On subsequent\n // runs the persisted value is loaded silently — no manual\n // export needed. The MCP daemon reads the SAME file read-only\n // via loadRelaySecretReadOnly() when switching to a relay env.\n // SECRET-HANDLING: neither ensureRelaySecret nor the\n // guard/predicate log the secret value.\n const { ensureRelaySecret } = await import('../mcp/relay-secret-store.js');\n await ensureRelaySecret({ projectRoot: server.config.root });\n const { assertRelayAuthConfigured, buildRelayVerifyAuth } = await import(\n '../mcp/totp.js'\n );\n assertRelayAuthConfigured();\n const verifyAuth = buildRelayVerifyAuth();\n const { startChiiRelay } = await import('../mcp/chii-relay.js');\n // Issue #467: this relay lives in the vite process, so the\n // MCP daemon's get_debug_status counter cannot see its 401s.\n // Surface a throttled hint in the vite terminal instead.\n // SECRET-HANDLING: fixed message only — no URL, code, host.\n let lastAuthRejectWarnAt = 0;\n const r = await startChiiRelay({\n port: 0,\n verifyAuth,\n onAuthReject: () => {\n const nowMs = Date.now();\n if (nowMs - lastAuthRejectWarnAt < 10_000) return;\n lastAuthRejectWarnAt = nowMs;\n console.warn(\n '[@ait-co/devtools] tunnel: relay 인증(TOTP) 거부 감지 — 폰에서 QR을 다시 스캔하세요 (코드는 ~3분마다 만료)',\n );\n },\n });\n relay = r;\n const rt = await startQuickTunnel(r.port);\n relayTunnel = rt;\n // SECRET-HANDLING: rt.url is the https relay base — stored in\n // relayHttpUrl for .ait_urls write below; never logged.\n relayHttpUrl = rt.url;\n relayWssUrl = rt.url.replace(/^https:/, 'wss:');\n } catch (err: unknown) {\n console.warn(\n `[@ait-co/devtools] tunnel: CDP relay not started — screen preview works without on-device debugging: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n }\n\n // Read the app name from the project's package.json to add to\n // the launcher deep-link (#498). Failure is silently ignored.\n let tunnelAppName: string | undefined;\n try {\n const { readFileSync } = await import('node:fs');\n const pkgPath = `${server.config.root}/package.json`;\n const pkgRaw = readFileSync(pkgPath, 'utf8');\n const pkg = JSON.parse(pkgRaw) as Record<string, unknown>;\n const rawName = typeof pkg.name === 'string' ? pkg.name : '';\n const stripped = rawName.includes('/')\n ? rawName.slice(rawName.indexOf('/') + 1)\n : rawName;\n tunnelAppName = stripped.trim() || undefined;\n } catch {\n // Silently ignore — fail-open.\n }\n\n await printTunnelBanner(t.url, {\n qr: tunnelConfig.qr,\n relayWssUrl,\n name: tunnelAppName,\n });\n\n // env-2 URL file-based discovery (#424): write .ait_urls so the\n // MCP daemon can discover the relay/tunnel URLs without manual env\n // var copy-paste. SECRET-HANDLING: URL values are never logged.\n // Capture deleteRelayUrls in the outer-scope fn so cleanup() can\n // call it without re-importing (no async in signal handlers).\n const { writeRelayUrls, deleteRelayUrls } = await import(\n '../mcp/relay-url-store.js'\n );\n await writeRelayUrls({\n projectRoot: server.config.root,\n tunnelBaseUrl: t.url,\n ...(relayHttpUrl !== undefined ? { relayBaseUrl: relayHttpUrl } : {}),\n });\n relayUrlDeleteFn = (root: string) => deleteRelayUrls({ projectRoot: root });\n\n // env-2 HTML dashboard (issue #408): when CDP is wired and a GUI\n // is present, serve the same QR+FAQ dashboard env 3/4 uses and\n // open it in the browser. No-op (returns undefined) for the\n // screen-only tunnel, headless, qr:false, or AIT_AUTO_DEVTOOLS=0\n // — the ASCII QR above remains the fallback in those cases.\n if (relayWssUrl) {\n qrDashboard =\n (await startTunnelDashboard({\n tunnelUrl: t.url,\n relayWssUrl,\n qr: tunnelConfig.qr,\n name: tunnelAppName,\n })) ?? null;\n }\n\n // #420: start watching the parent PID now that tunnel resources\n // are allocated. When the parent dies/reparents, clean up\n // synchronously (stops cloudflared children) then exit.\n parentWatcher = startParentWatcher(() => {\n cleanup();\n process.exit(0);\n });\n })\n .catch((err: unknown) => {\n console.warn(\n `[@ait-co/devtools] tunnel failed to start: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n });\n });\n\n const cleanup = () => {\n parentWatcher?.stop();\n tunnel?.stop();\n relayTunnel?.stop();\n void relay?.close();\n void qrDashboard?.close();\n // env-2 URL file cleanup (#424): remove .ait_urls on teardown so a\n // stale file doesn't cause the MCP daemon to attempt a doomed attach.\n // SECRET-HANDLING: relayUrlDeleteFn never logs the path or URL values.\n void relayUrlDeleteFn?.(server.config.root);\n };\n httpServer?.once('close', cleanup);\n process.once('SIGINT', cleanup);\n process.once('SIGTERM', cleanup);\n process.once('SIGHUP', cleanup);\n process.once('exit', cleanup);\n }\n },\n },\n };\n});\n\nexport const vite = aitDevtoolsPlugin.vite;\nexport const webpack = aitDevtoolsPlugin.webpack;\nexport const rollup = aitDevtoolsPlugin.rollup;\nexport const esbuild = aitDevtoolsPlugin.esbuild;\nexport const rspack = aitDevtoolsPlugin.rspack;\n\nexport default aitDevtoolsPlugin;\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/shared/parent-watcher.ts","../../src/unplugin/index.ts"],"sourcesContent":["/**\n * Shared parent-PID watcher — used by both the MCP debug daemon and the\n * unplugin tunnel path to self-terminate when the parent process (e.g. Claude\n * Code, vite) has died or been reparented without sending SIGTERM/SIGHUP.\n *\n * Intentionally react-free and Node-stdlib-only so this module is safe to\n * import from the MCP daemon bundle (`dist/mcp/cli.js`) without violating the\n * install-graph invariant.\n */\n\n// ---------------------------------------------------------------------------\n// isPidAlive — extracted from src/mcp/server-lock.ts\n// ---------------------------------------------------------------------------\n\n/**\n * Returns `true` when the given PID refers to a running process.\n *\n * Uses `process.kill(pid, 0)` — a no-op signal that succeeds when the process\n * exists and we have permission to signal it; throws ESRCH when it doesn't exist.\n */\nexport function isPidAlive(pid: number): boolean {\n try {\n process.kill(pid, 0);\n return true;\n } catch (err: unknown) {\n // ESRCH = no such process → stale lock.\n // EPERM = process exists but we can't signal it (still alive).\n if ((err as NodeJS.ErrnoException).code === 'EPERM') return true;\n return false;\n }\n}\n\n// ---------------------------------------------------------------------------\n// startParentWatcher — extracted from src/mcp/debug-server.ts\n// ---------------------------------------------------------------------------\n\n/**\n * Starts a periodic watcher that detects when the parent process (e.g. Claude\n * Code) has died without sending SIGTERM/SIGHUP, and calls `onOrphaned` so the\n * daemon can self-terminate rather than running as a zombie.\n *\n * Mirrors the `startAttachWatcher` pattern: `setInterval`-based, returns\n * `{ stop(): void }`, injectable deps for testability.\n *\n * @param onOrphaned - Called once when the parent is gone.\n * @param opts.intervalMs - Poll interval in milliseconds (default 5 000).\n * @param opts.initialPpid - Parent PID to watch (default `process.ppid`).\n * @param opts.isAlive - Predicate to test if a PID is running (default `isPidAlive`).\n * @param opts.getPpid - Supplier of current ppid (default `() => process.ppid`).\n * Detects ppid changes as well as death.\n * @param opts.log - Logger (default `process.stderr.write`).\n *\n * @returns `stop` — call during shutdown to clear the interval.\n */\nexport function startParentWatcher(\n onOrphaned: () => void,\n opts?: {\n intervalMs?: number;\n initialPpid?: number;\n isAlive?: (pid: number) => boolean;\n getPpid?: () => number;\n log?: (msg: string) => void;\n },\n): { stop(): void } {\n const {\n intervalMs = 5_000,\n initialPpid = process.ppid,\n isAlive = isPidAlive,\n getPpid = () => process.ppid,\n log = (msg: string) => process.stderr.write(msg),\n } = opts ?? {};\n\n // PID 1 is init/launchd — running under a process manager or as a detached\n // daemon. There is no meaningful parent to watch; skip the watcher entirely.\n if (initialPpid <= 1) {\n log('[ait-debug] parent-pid watcher: no parent to watch (ppid<=1), skipping\\n');\n return { stop() {} };\n }\n\n let fired = false;\n\n const handle = setInterval(() => {\n if (fired) return;\n\n const currentPpid = getPpid();\n const orphaned = currentPpid !== initialPpid || !isAlive(initialPpid);\n\n if (orphaned) {\n fired = true;\n clearInterval(handle);\n log(\n `[ait-debug] parent-pid watcher: parent PID ${initialPpid} is gone (currentPpid=${currentPpid}) — shutting down\\n`,\n );\n onOrphaned();\n }\n }, intervalMs);\n\n return {\n stop() {\n clearInterval(handle);\n },\n };\n}\n","/**\n * @ait-co/devtools unplugin\n *\n * 모든 주요 번들러를 지원하는 단일 플러그인.\n * @apps-in-toss/web-framework → @ait-co/devtools/mock 으로 alias 설정.\n *\n * Usage:\n * import aitDevtools from '@ait-co/devtools/unplugin';\n *\n * // Vite\n * export default { plugins: [aitDevtools.vite()] };\n *\n * // Webpack / Next.js\n * config.plugins.push(aitDevtools.webpack());\n *\n * // Rspack\n * config.plugins.push(aitDevtools.rspack());\n *\n * // esbuild\n * { plugins: [aitDevtools.esbuild()] }\n *\n * // Rollup\n * { plugins: [aitDevtools.rollup()] }\n */\n\nimport { fileURLToPath } from 'node:url';\nimport { createUnplugin } from 'unplugin';\nimport { startParentWatcher } from '../shared/parent-watcher.js';\n\n/**\n * Resolve `@ait-co/devtools/mock` to its real file path at plugin-load time.\n *\n * Returning the bare specifier from `resolveId` would stop the bundler from\n * walking node_modules for it — Vite 8+ treats such a non-null string as the\n * final resolved id and serves it via the virtual `/@id/` prefix, which 404s\n * because we don't provide a `load` hook. Resolving to an absolute path here\n * lets every supported bundler load the file the normal way.\n */\nconst MOCK_PATH = (() => {\n try {\n return fileURLToPath(import.meta.resolve('@ait-co/devtools/mock'));\n } catch {\n // Fallback for runtimes where `import.meta.resolve` is unavailable.\n return '@ait-co/devtools/mock';\n }\n})();\n\nexport interface AitDevtoolsOptions {\n /**\n * 패널 자동 주입 여부 (default: true)\n * true이면 진입점에 floating panel import를 자동 추가한다.\n */\n panel?: boolean;\n /**\n * production 환경에서도 devtools를 강제로 활성화 (default: false)\n */\n forceEnable?: boolean;\n /**\n * mock alias 활성화 여부. default: true (development), false (production + forceEnable)\n */\n mock?: boolean;\n /**\n * Vite dev server에 MCP state endpoint를 추가할지 여부 (default: false).\n *\n * `true`로 설정하면:\n * - GET /api/ait-devtools/state — 마지막으로 브라우저가 push한 mock state 스냅샷 반환\n * - POST /api/ait-devtools/state — 브라우저 panel이 상태 변경 시 자동 push (panel 내부 처리)\n *\n * 이 endpoint를 `@ait-co/devtools` MCP stdio server가 읽어 AI 에이전트에 mock state를 노출한다.\n * Vite 전용: webpack/rspack/esbuild/rollup 환경에서는 무시된다.\n */\n mcp?: boolean;\n /**\n * Vite dev 서버를 Cloudflare quick tunnel(`*.trycloudflare.com`, 계정 불필요)로\n * 외부 노출해 실제 폰에서 미리보기. **Vite dev 모드 전용** — production은\n * `forceEnable`이어도 터널을 띄우지 않는다 (의도치 않은 노출 방지). 다른 번들러는\n * 무시. `true`면 기본 동작, 객체로 세부 설정 가능.\n */\n tunnel?:\n | boolean\n | {\n /** 노출할 포트 (미지정 시 dev 서버가 실제 listen한 포트 자동 감지). */\n port?: number;\n /** 터미널 ASCII QR 출력 (default: true). */\n qr?: boolean;\n /**\n * 환경 2(실기기 PWA)에 CDP 디버깅 배선 (default: false).\n *\n * `true`면 dev 서버 HTTP 터널과 **별도로** Chii relay를 띄우고 그 relay에\n * 두 번째 quick tunnel을 붙여, launcher QR deep-link에 `&debug=1&relay=<wss>`를\n * 실어 보낸다. 폰의 PWA iframe이 in-app debug gate를 통과해 target.js를 주입받고,\n * AI host MCP가 그 relay에 client로 붙으면 실기기 WebKit 위에서 CDP 디버깅이 열린다.\n * mock SDK는 그대로라 `call_sdk`는 환경 2에서 mock을 친다 (fidelity 사다리의\n * 설계 의도 — SDK fidelity가 필요하면 환경 3로 올라간다).\n */\n cdp?: boolean;\n };\n}\n\nconst FRAMEWORK_ID = '@apps-in-toss/web-framework';\nconst BRIDGE_ID = '@apps-in-toss/web-bridge'; // back-compat (2.x)\nconst ANALYTICS_ID = '@apps-in-toss/web-analytics'; // back-compat (2.x)\nconst WEBVIEW_BRIDGE_ID = '@apps-in-toss/webview-bridge'; // 3.0+\n\n/** MCP state endpoint path — browser panel POSTs here, MCP server GETs here */\nconst MCP_STATE_PATH = '/api/ait-devtools/state';\n\n/**\n * Resolves the effective tunnel option (#425).\n *\n * An explicit `tunnel` value (including `false`) always takes priority over\n * env vars — the `??` operator means `undefined` (= omitted) falls through,\n * but `false` / `true` / an object are preserved as-is (non-breaking).\n *\n * When the option is omitted:\n * - `AIT_TUNNEL=1` enables the base screen-preview tunnel.\n * - `AIT_TUNNEL_CDP=1` (requires `AIT_TUNNEL`) upgrades to the CDP relay.\n * - Neither set → `false` (disabled).\n *\n * Extracted as a pure function so it can be unit-tested without standing up\n * a full Vite dev server.\n *\n * @param explicit - The `tunnel` option as passed by the consumer (or `undefined` when omitted).\n * @param env - The process environment (injectable for testing).\n */\nexport function resolveTunnelOption(\n explicit: AitDevtoolsOptions['tunnel'],\n env: Record<string, string | undefined>,\n): AitDevtoolsOptions['tunnel'] {\n return explicit ?? (env.AIT_TUNNEL ? { cdp: !!env.AIT_TUNNEL_CDP } : false);\n}\n\nconst aitDevtoolsPlugin = createUnplugin((options?: AitDevtoolsOptions) => {\n const isDev = process.env.NODE_ENV !== 'production';\n const shouldEnable = isDev || (options?.forceEnable ?? false);\n const shouldMock = shouldEnable && (options?.mock ?? isDev);\n const shouldPanel = shouldEnable && (options?.panel ?? true);\n const shouldMcp = shouldEnable && (options?.mcp ?? false);\n\n // In-memory store for the last state snapshot pushed by the browser panel.\n // Only allocated when mcp: true to avoid any overhead in the common case.\n let lastState: string | null = null;\n\n // Tunnel is dev-only and Vite-only. Never under production — even with\n // forceEnable — so a production build can't accidentally expose itself.\n //\n // Tunnel toggle resolution (#425): an explicit `tunnel` option always wins;\n // when omitted, fall back to the AIT_TUNNEL / AIT_TUNNEL_CDP env vars so a\n // consumer needs no `tunnel:` line in vite.config to enable env-2 preview.\n // AIT_TUNNEL gates the base (screen preview); AIT_TUNNEL_CDP upgrades to the\n // CDP relay. Production safety is unchanged — the existing\n // `shouldTunnel = isDev && !!tunnelOpt` guard below still blocks prod builds.\n const tunnelOpt = resolveTunnelOption(options?.tunnel, process.env);\n const shouldTunnel = isDev && !!tunnelOpt;\n const tunnelConfig = typeof tunnelOpt === 'object' ? tunnelOpt : {};\n\n return {\n name: 'ait-co-devtools',\n enforce: 'pre' as const,\n\n resolveId(id: string) {\n if (!shouldMock) return null;\n // @apps-in-toss/web-framework → @ait-co/devtools/mock (absolute path)\n if (\n id === FRAMEWORK_ID ||\n id === WEBVIEW_BRIDGE_ID ||\n id === BRIDGE_ID ||\n id === ANALYTICS_ID\n ) {\n return MOCK_PATH;\n }\n return null;\n },\n\n transformInclude(id: string) {\n if (!shouldPanel) return false;\n // 진입점 파일에만 패널 import를 주입\n return (\n /\\.(tsx?|jsx?)$/.test(id) &&\n /\\/(main|index|entry|app)\\.[tj]sx?$/i.test(id) &&\n !id.includes('node_modules')\n );\n },\n\n transform(code: string) {\n // transformInclude가 이미 shouldPanel을 확인하지만, 안전망으로 유지\n if (!shouldPanel) return null;\n // 이미 패널이 import 되어있으면 스킵\n if (code.includes('@ait-co/devtools/panel')) return null;\n // transformInclude가 진입점 파일만 통과시키므로 바로 prepend\n return `import '@ait-co/devtools/panel';\\n${code}`;\n },\n\n // Vite-only: register the MCP state HTTP endpoint on the dev server, and\n // optionally start a Cloudflare quick tunnel once the dev server is listening.\n // Non-Vite bundlers do not have a dev server concept so this is silently\n // skipped (unplugin passes `vite` key only when building for Vite).\n vite: {\n config() {\n if (!shouldTunnel) return;\n // Vite blocks requests whose Host header isn't in `server.allowedHosts`\n // (defaults to localhost only). The quick-tunnel hostname is random per\n // run, so allow the whole `.trycloudflare.com` suffix while the tunnel\n // is on. (A leading `.` makes Vite match the domain and its subdomains.)\n return { server: { allowedHosts: ['.trycloudflare.com'] } };\n },\n\n configureServer(server: import('vite').ViteDevServer) {\n // MCP state endpoint: browser panel POSTs state here, MCP stdio server GETs it.\n if (shouldMcp) {\n server.middlewares.use(MCP_STATE_PATH, (req, res) => {\n // Allow Claude Code / AI agents (running locally) to read state\n res.setHeader('Access-Control-Allow-Origin', '*');\n res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS');\n res.setHeader('Access-Control-Allow-Headers', 'Content-Type');\n\n if (req.method === 'OPTIONS') {\n res.writeHead(204);\n res.end();\n return;\n }\n\n if (req.method === 'GET') {\n if (lastState === null) {\n res.writeHead(503, { 'Content-Type': 'application/json' });\n res.end(\n JSON.stringify({\n error: 'No state received yet. Open the app in a browser first.',\n }),\n );\n return;\n }\n res.writeHead(200, { 'Content-Type': 'application/json' });\n res.end(lastState);\n return;\n }\n\n if (req.method === 'POST') {\n const chunks: Buffer[] = [];\n req.on('data', (chunk: Buffer) => chunks.push(chunk));\n req.on('end', () => {\n try {\n const body = Buffer.concat(chunks).toString('utf-8');\n // Validate it's parseable JSON before caching\n JSON.parse(body);\n lastState = body;\n res.writeHead(204);\n res.end();\n } catch {\n res.writeHead(400, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Invalid JSON' }));\n }\n });\n return;\n }\n\n res.writeHead(405, { 'Content-Type': 'application/json' });\n res.end(JSON.stringify({ error: 'Method not allowed' }));\n });\n }\n\n // Tunnel: start a Cloudflare quick tunnel once the dev server is listening.\n if (shouldTunnel) {\n let tunnel: { stop: () => void } | null = null;\n // env-2 CDP wiring (tunnel.cdp): a second tunnel + Chii relay, torn\n // down alongside the HTTP tunnel. Fire-and-forget close on teardown.\n let relayTunnel: { stop: () => void } | null = null;\n let relay: { close: () => Promise<void> } | null = null;\n // env-2 HTML dashboard (issue #408): local 127.0.0.1 HTTP server that\n // serves the QR + connect-steps + FAQ page (env 3/4 UX parity), opened\n // in the browser when CDP is wired + GUI present. Torn down with the\n // tunnel. Only set when the dashboard actually started.\n let qrDashboard: { close: () => Promise<void> } | null = null;\n // env-2 URL file store (#424): captured after the first writeRelayUrls\n // call so cleanup() can call deleteRelayUrls without a re-import.\n // SECRET-HANDLING: the stored function reference never carries URL values.\n let relayUrlDeleteFn: ((projectRoot: string) => Promise<void>) | null = null;\n // #420: parent-PID watcher — self-terminate when vite's parent dies so\n // cloudflared children don't become zombies holding stale tunnels.\n let parentWatcher: { stop(): void } | null = null;\n const httpServer = server.httpServer;\n\n httpServer?.once('listening', () => {\n const address = httpServer?.address();\n const port =\n tunnelConfig.port ??\n (address && typeof address === 'object' ? address.port : undefined);\n if (!port) {\n console.warn(\n '[@ait-co/devtools] tunnel: could not determine the dev server port; skipping.',\n );\n return;\n }\n // Dynamic import keeps `cloudflared` / `qrcode-terminal` off the\n // module graph unless the tunnel is actually used.\n import('./tunnel.js')\n .then(async ({ startQuickTunnel, printTunnelBanner, startTunnelDashboard }) => {\n const t = await startQuickTunnel(port);\n tunnel = t;\n\n // env-2 CDP: boot a Chii relay (OS-assigned local port) and a\n // second quick tunnel to it. The relay's https tunnel URL becomes\n // the `wss://` relay the launcher QR carries (&debug=1&relay=).\n let relayWssUrl: string | undefined;\n // SECRET-HANDLING: relayHttpUrl carries the relay host — never logged.\n let relayHttpUrl: string | undefined;\n // LOCAL relay base — loopback URL, safe to surface (issue #530).\n let relayLocalHttpUrl: string | undefined;\n if (tunnelConfig.cdp) {\n try {\n // Relay-auth baseline (issue #250): the env-2 CDP relay is\n // reachable over a public `*.trycloudflare.com` tunnel, so a\n // configured TOTP secret is MANDATORY and the relay enforces\n // it on every WS upgrade.\n //\n // First-run auto-mint (issue #394, project-local #396): if\n // AIT_DEBUG_TOTP_SECRET is not yet set, ensureRelaySecret()\n // mints a 256-bit random secret, persists it to the project-\n // local file <project>/.ait_relay (0600, anchored at the\n // nearest package.json directory above server.config.root),\n // and injects it into process.env so the following\n // assertRelayAuthConfigured() call succeeds. On subsequent\n // runs the persisted value is loaded silently — no manual\n // export needed. The MCP daemon reads the SAME file read-only\n // via loadRelaySecretReadOnly() when switching to a relay env.\n // SECRET-HANDLING: neither ensureRelaySecret nor the\n // guard/predicate log the secret value.\n const { ensureRelaySecret } = await import('../mcp/relay-secret-store.js');\n await ensureRelaySecret({ projectRoot: server.config.root });\n const { assertRelayAuthConfigured, buildRelayVerifyAuth } = await import(\n '../mcp/totp.js'\n );\n assertRelayAuthConfigured();\n const verifyAuth = buildRelayVerifyAuth();\n const { startChiiRelay } = await import('../mcp/chii-relay.js');\n // Issue #467: this relay lives in the vite process, so the\n // MCP daemon's get_debug_status counter cannot see its 401s.\n // Surface a throttled hint in the vite terminal instead.\n // SECRET-HANDLING: fixed message only — no URL, code, host.\n let lastAuthRejectWarnAt = 0;\n const r = await startChiiRelay({\n port: 0,\n verifyAuth,\n onAuthReject: () => {\n const nowMs = Date.now();\n if (nowMs - lastAuthRejectWarnAt < 10_000) return;\n lastAuthRejectWarnAt = nowMs;\n console.warn(\n '[@ait-co/devtools] tunnel: relay 인증(TOTP) 거부 감지 — 폰에서 QR을 다시 스캔하세요 (코드는 ~3분마다 만료)',\n );\n },\n });\n relay = r;\n const rt = await startQuickTunnel(r.port);\n relayTunnel = rt;\n // SECRET-HANDLING: rt.url is the https relay base — stored in\n // relayHttpUrl for .ait_urls write below; never logged.\n relayHttpUrl = rt.url;\n relayWssUrl = rt.url.replace(/^https:/, 'wss:');\n // LOCAL relay base for MCP inspector URL assembly (issue #530):\n // the relay process runs on this machine, so the inspector\n // front_end + client WS can use the loopback address directly —\n // no tunnel round-trip for the developer's browser.\n // Safe to surface: loopback URL contains no tunnel host.\n relayLocalHttpUrl = `http://127.0.0.1:${r.port}`;\n } catch (err: unknown) {\n console.warn(\n `[@ait-co/devtools] tunnel: CDP relay not started — screen preview works without on-device debugging: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n }\n }\n\n // Read the app name from the project's package.json to add to\n // the launcher deep-link (#498). Failure is silently ignored.\n let tunnelAppName: string | undefined;\n try {\n const { readFileSync } = await import('node:fs');\n const pkgPath = `${server.config.root}/package.json`;\n const pkgRaw = readFileSync(pkgPath, 'utf8');\n const pkg = JSON.parse(pkgRaw) as Record<string, unknown>;\n const rawName = typeof pkg.name === 'string' ? pkg.name : '';\n const stripped = rawName.includes('/')\n ? rawName.slice(rawName.indexOf('/') + 1)\n : rawName;\n tunnelAppName = stripped.trim() || undefined;\n } catch {\n // Silently ignore — fail-open.\n }\n\n await printTunnelBanner(t.url, {\n qr: tunnelConfig.qr,\n relayWssUrl,\n name: tunnelAppName,\n });\n\n // env-2 URL file-based discovery (#424): write .ait_urls so the\n // MCP daemon can discover the relay/tunnel URLs without manual env\n // var copy-paste. SECRET-HANDLING: URL values are never logged.\n // Capture deleteRelayUrls in the outer-scope fn so cleanup() can\n // call it without re-importing (no async in signal handlers).\n const { writeRelayUrls, deleteRelayUrls } = await import(\n '../mcp/relay-url-store.js'\n );\n await writeRelayUrls({\n projectRoot: server.config.root,\n tunnelBaseUrl: t.url,\n ...(relayHttpUrl !== undefined ? { relayBaseUrl: relayHttpUrl } : {}),\n // Issue #530: local relay base for inspector URL (loopback, no tunnel host).\n ...(relayLocalHttpUrl !== undefined ? { relayLocalUrl: relayLocalHttpUrl } : {}),\n });\n relayUrlDeleteFn = (root: string) => deleteRelayUrls({ projectRoot: root });\n\n // env-2 HTML dashboard (issue #408): when CDP is wired and a GUI\n // is present, serve the same QR+FAQ dashboard env 3/4 uses and\n // open it in the browser. No-op (returns undefined) for the\n // screen-only tunnel, headless, qr:false, or AIT_AUTO_DEVTOOLS=0\n // — the ASCII QR above remains the fallback in those cases.\n if (relayWssUrl) {\n qrDashboard =\n (await startTunnelDashboard({\n tunnelUrl: t.url,\n relayWssUrl,\n qr: tunnelConfig.qr,\n name: tunnelAppName,\n })) ?? null;\n }\n\n // #420: start watching the parent PID now that tunnel resources\n // are allocated. When the parent dies/reparents, clean up\n // synchronously (stops cloudflared children) then exit.\n parentWatcher = startParentWatcher(() => {\n cleanup();\n process.exit(0);\n });\n })\n .catch((err: unknown) => {\n console.warn(\n `[@ait-co/devtools] tunnel failed to start: ${\n err instanceof Error ? err.message : String(err)\n }`,\n );\n });\n });\n\n const cleanup = () => {\n parentWatcher?.stop();\n tunnel?.stop();\n relayTunnel?.stop();\n void relay?.close();\n void qrDashboard?.close();\n // env-2 URL file cleanup (#424): remove .ait_urls on teardown so a\n // stale file doesn't cause the MCP daemon to attempt a doomed attach.\n // SECRET-HANDLING: relayUrlDeleteFn never logs the path or URL values.\n void relayUrlDeleteFn?.(server.config.root);\n };\n httpServer?.once('close', cleanup);\n process.once('SIGINT', cleanup);\n process.once('SIGTERM', cleanup);\n process.once('SIGHUP', cleanup);\n process.once('exit', cleanup);\n }\n },\n },\n };\n});\n\nexport const vite = aitDevtoolsPlugin.vite;\nexport const webpack = aitDevtoolsPlugin.webpack;\nexport const rollup = aitDevtoolsPlugin.rollup;\nexport const esbuild = aitDevtoolsPlugin.esbuild;\nexport const rspack = aitDevtoolsPlugin.rspack;\n\nexport default aitDevtoolsPlugin;\n"],"mappings":"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"}
|
package/dist/unplugin/tunnel.cjs
CHANGED
|
@@ -138,7 +138,7 @@ async function startTunnelDashboard(opts) {
|
|
|
138
138
|
if (opts.qr === false) return void 0;
|
|
139
139
|
const { isAutoDevtoolsDisabled } = await Promise.resolve().then(() => require("../devtools-opener-h6A-UjzC.cjs"));
|
|
140
140
|
if (!(opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser()))()) return void 0;
|
|
141
|
-
const { startQrHttpServer } = await Promise.resolve().then(() => require("../qr-http-server-
|
|
141
|
+
const { startQrHttpServer } = await Promise.resolve().then(() => require("../qr-http-server-MIUHaiYw.cjs"));
|
|
142
142
|
const { buildLauncherAttachUrl } = await Promise.resolve().then(() => require("../deeplink-CU6opogq.cjs"));
|
|
143
143
|
const { generateTotp } = await Promise.resolve().then(() => require("../totp-BwDZ6dUT.cjs"));
|
|
144
144
|
const getDashboardState = () => {
|
package/dist/unplugin/tunnel.js
CHANGED
|
@@ -137,7 +137,7 @@ async function startTunnelDashboard(opts) {
|
|
|
137
137
|
if (opts.qr === false) return void 0;
|
|
138
138
|
const { isAutoDevtoolsDisabled } = await import("../devtools-opener-D84kZFtR.js");
|
|
139
139
|
if (!(opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser()))()) return void 0;
|
|
140
|
-
const { startQrHttpServer } = await import("../qr-http-server-
|
|
140
|
+
const { startQrHttpServer } = await import("../qr-http-server-DkOFfZsR.js");
|
|
141
141
|
const { buildLauncherAttachUrl } = await import("../deeplink-CYqDwVYs.js");
|
|
142
142
|
const { generateTotp } = await import("../totp-BmKSPb5d.js");
|
|
143
143
|
const getDashboardState = () => {
|
package/package.json
CHANGED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"qr-http-server-D2d44bv7.js","names":[],"sources":["../src/i18n/en.ts","../src/i18n/ko.ts","../src/i18n/index.ts","../src/mcp/dashboard.generated.ts","../src/mcp/qr-http-server.ts"],"sourcesContent":["import type { StringKey } from './ko.js';\n\n// English translations. Mirrors every key in `ko.ts`; missing keys fall back to\n// the key string at runtime (see `t()` in index.ts), but the `Record<StringKey,\n// string>` type below means a missing key will typecheck-fail.\n\nexport const en: Record<StringKey, string> = {\n // Panel chrome\n 'panel.title': 'AIT DevTools',\n 'panel.toggle.title': 'AIT DevTools',\n 'panel.close': 'Close',\n 'panel.editMode.on': 'EDIT',\n 'panel.editMode.off': 'READ-ONLY',\n 'panel.editMode.toggleTitle': 'Toggle panel edit mode',\n 'panel.tabError': 'Error rendering \"{tab}\" tab.',\n\n // Tab names\n 'panel.tab.env': 'Environment',\n 'panel.tab.presets': 'Presets',\n 'panel.tab.viewport': 'Viewport',\n 'panel.tab.permissions': 'Permissions',\n 'panel.tab.notifications': 'Notifications',\n 'panel.tab.location': 'Location',\n 'panel.tab.device': 'Device',\n 'panel.tab.iap': 'IAP',\n 'panel.tab.ads': 'Ads',\n 'panel.tab.events': 'Events',\n 'panel.tab.analytics': 'Analytics',\n 'panel.tab.storage': 'Storage',\n\n // Common\n 'common.readOnly': 'Read-only — mock responses are controlled at build time.',\n\n // Consent toast\n 'toast.consent.title': 'Send anonymous usage stats?',\n 'toast.consent.body':\n 'We collect anonymous events only, to improve the tool. You can turn this off anytime in the Environment tab.',\n 'toast.consent.learnMore': 'Learn more',\n 'toast.consent.accept': 'Yes, send',\n 'toast.consent.deny': 'No, thanks',\n\n // Environment tab\n 'env.section.platform': 'Platform',\n 'env.row.os': 'OS',\n 'env.row.appVersion': 'App Version',\n 'env.row.environment': 'Environment',\n 'env.row.locale': 'Locale',\n 'env.section.network': 'Network',\n 'env.row.networkStatus': 'Status',\n 'env.section.safeArea': 'Safe Area Insets',\n 'env.row.safeArea.top': 'Top',\n 'env.row.safeArea.bottom': 'Bottom',\n 'env.section.navigation': 'Navigation',\n 'env.row.iosSwipeGesture': 'iOS swipe-back',\n 'env.value.iosSwipeGesture.unset': 'not called',\n 'env.value.iosSwipeGesture.enabled': 'enabled',\n 'env.value.iosSwipeGesture.disabled': 'disabled',\n 'env.hint.iosSwipeGesture':\n 'Last value passed to setIosSwipeGestureEnabled. Switching Environment to toss lets a toss-gated guard toggle this.',\n\n // Environment > Telemetry section\n 'env.telemetry.section': 'Telemetry',\n // Tier 0 — opt-out anonymous signal\n 'env.telemetry.t0Row': 'Anonymous usage signal (Tier 0)',\n 'env.telemetry.t0On': 'On',\n 'env.telemetry.t0Off': 'Off',\n 'env.telemetry.t0TurnOn': 'Turn on',\n 'env.telemetry.t0TurnOff': 'Turn off',\n 'env.telemetry.t0Desc': 'Version + date only, no PII. Once per day. Helps improve the package.',\n // Tier 1 — opt-in extended telemetry\n 'env.telemetry.row': 'Extended telemetry (Tier 1)',\n 'env.telemetry.on': 'On',\n 'env.telemetry.off': 'Off',\n 'env.telemetry.turnOn': 'Turn on',\n 'env.telemetry.turnOff': 'Turn off',\n 'env.telemetry.anonIdLabel': 'anon_id: {value}',\n 'env.telemetry.anonIdNotSet': '(not yet set)',\n 'env.telemetry.anonIdCopyTitle': 'Click to copy full anon_id',\n 'env.telemetry.deleteBtn': 'Delete my data',\n 'env.telemetry.deleting': 'Deleting…',\n 'env.telemetry.deleted': 'Deleted',\n 'env.telemetry.deleteFailedRetry': 'Delete failed (please retry)',\n 'env.telemetry.deleteFailed': 'Delete failed',\n 'env.telemetry.privacyLink': 'Privacy policy →',\n\n // Environment > Language toggle (new)\n 'env.section.language': 'Language',\n 'env.language.row': 'Language',\n 'env.language.ko': '한국어',\n 'env.language.en': 'English',\n\n // Permissions tab\n 'permissions.section.device': 'Device Permissions',\n\n // Location tab\n 'location.section.current': 'Current Location',\n 'location.row.latitude': 'Latitude',\n 'location.row.longitude': 'Longitude',\n 'location.row.accuracy': 'Accuracy',\n\n // Device tab\n 'device.section.modes': 'Device API Modes',\n 'device.row.camera': 'Camera',\n 'device.row.photos': 'Photos',\n 'device.row.location': 'Location',\n 'device.row.network': 'Network',\n 'device.row.clipboard': 'Clipboard',\n 'device.section.mockImages': 'Mock Images ({count})',\n 'device.btn.add': '+ Add',\n 'device.btn.useDefaults': 'Use defaults',\n 'device.btn.clear': 'Clear',\n 'device.prompt.camera.title': 'Camera Prompt — Select an image',\n 'device.prompt.photos.title': 'Photos Prompt — Select images',\n 'device.prompt.location.title': 'Location Prompt — Enter coordinates',\n 'device.prompt.locationUpdate.title': 'Location Update — Send coordinates',\n 'device.prompt.fallbackTitle': 'Prompt: {type}',\n 'device.prompt.label.lat': 'Lat',\n 'device.prompt.label.lng': 'Lng',\n 'device.prompt.send': 'Send',\n 'device.prompt.cancel': 'Cancel',\n\n // Device tab — Haptic section\n 'device.section.haptic': 'Haptic',\n 'device.haptic.lastCall': 'Last haptic',\n 'device.haptic.noneYet': '(none yet)',\n 'device.haptic.trigger': 'Trigger haptic',\n\n // Viewport tab\n 'viewport.section.device': 'Device',\n 'viewport.row.preset': 'Preset',\n 'viewport.row.orientation': 'Orientation',\n 'viewport.row.notchSide': 'Notch side',\n 'viewport.section.custom': 'Custom size',\n 'viewport.row.width': 'Width (px)',\n 'viewport.row.height': 'Height (px)',\n 'viewport.section.appearance': 'Appearance',\n 'viewport.row.showFrame': 'Show frame',\n 'viewport.row.showAitNavBar': 'Show Apps in Toss nav bar',\n 'viewport.row.navBarType': 'Nav bar type',\n 'viewport.status.noConstraint': 'No viewport constraint — body fills the window.',\n 'viewport.status.cssPhysical': 'CSS / physical',\n 'viewport.status.safeArea': 'Safe area',\n 'viewport.status.aitNavBar': 'AIT nav bar',\n 'viewport.status.aitNavBarValue': '{height}px → SafeArea top · {type}',\n 'viewport.orientation.autoSuffix': '{orient} (auto)',\n\n // IAP tab\n 'iap.section.simulator': 'IAP Simulator',\n 'iap.row.nextResult': 'Next Purchase Result',\n 'iap.section.tossPay': 'TossPay',\n 'iap.row.tossPayResult': 'Next Payment Result',\n 'iap.section.pending': 'Pending Orders ({count})',\n 'iap.empty.pending': '(no pending orders)',\n 'iap.section.completed': 'Completed Orders ({count})',\n 'iap.empty.completed': '(no completed orders)',\n 'iap.btn.complete': 'Complete',\n 'iap.label.pending': 'PENDING',\n\n // Events tab\n 'events.section.navigation': 'Navigation Events',\n 'events.btn.triggerBack': 'Trigger Back Event',\n 'events.btn.triggerHome': 'Trigger Home Event',\n 'events.section.login': 'Login',\n 'events.row.loggedIn': 'Logged In',\n 'events.row.tossLoginIntegrated': 'Toss Login Integrated',\n\n // Analytics tab\n 'analytics.section.log': 'Analytics Log ({count})',\n 'analytics.btn.clear': 'Clear',\n 'analytics.calls.section': 'SDK Calls ({count})',\n 'analytics.calls.btn.clear': 'Clear',\n 'analytics.calls.empty': '(no SDK calls yet)',\n\n // Storage tab\n 'storage.section.title': 'Storage ({count} items)',\n 'storage.btn.clearAll': 'Clear All',\n 'storage.empty': 'No items in storage',\n\n // Presets tab\n 'presets.section.builtIn': 'Built-in scenarios',\n 'presets.section.saved': 'Saved presets ({count})',\n 'presets.section.save': 'Save',\n 'presets.save.description': 'Capture network / permissions / auth / IAP / ads / payment slices.',\n 'presets.btn.saveCurrent': 'Save current as preset',\n 'presets.btn.apply': 'Apply',\n 'presets.btn.reApply': 'Re-apply',\n 'presets.btn.delete': 'Delete',\n 'presets.empty.saved': 'No saved presets yet.',\n 'presets.empty.builtIn': 'No built-in presets.',\n 'presets.prompt.label': 'Preset label?',\n 'presets.confirm.delete': 'Delete preset \"{label}\"?',\n\n // Ads tab\n 'ads.section.state': 'Ads State',\n 'ads.row.isLoaded': 'isLoaded',\n 'ads.row.forceNoFill': 'Force \"no fill\"',\n 'ads.empty.events': 'No events yet',\n 'ads.section.googleAdMob': 'GoogleAdMob',\n 'ads.section.tossAds': 'TossAds',\n 'ads.section.fullScreenAd': 'FullScreenAd',\n 'ads.btn.load': 'Load',\n 'ads.btn.show': 'Show',\n 'ads.section.tossAdsBanner': 'TossAds Banner',\n 'ads.row.rewardUnitType': 'Reward unit type',\n 'ads.row.rewardAmount': 'Reward amount',\n 'ads.btn.render': 'Render',\n 'ads.btn.noFill': 'No-fill',\n 'ads.btn.click': 'Click',\n 'ads.btn.destroy': 'Destroy',\n\n // Notifications tab\n 'notifications.section.title': 'requestNotificationAgreement',\n 'notifications.option.newAgreement': 'newAgreement (first-time agree)',\n 'notifications.option.alreadyAgreed': 'alreadyAgreed (already opted-in)',\n 'notifications.option.agreementRejected': 'agreementRejected (user declined)',\n\n // qr-http-server — lang switcher (dashboard / attach pages)\n 'dashboard.lang.ko': '한국어',\n 'dashboard.lang.en': 'English',\n\n // qr-http-server — dashboard page (server-side, Node, per-request)\n 'dashboard.title': 'AIT Debug Dashboard',\n 'dashboard.updated': 'Last updated: {ts}',\n 'dashboard.tunnel.section': 'Tunnel status',\n 'dashboard.tunnel.up': 'Connected',\n 'dashboard.tunnel.down': 'Disconnected',\n 'dashboard.attach.section': 'Attach QR',\n 'dashboard.attach.hint': 'Call the build_attach_url MCP tool to show the QR here.',\n 'dashboard.pages.section': 'Connected Pages',\n 'dashboard.pages.empty': 'No attached pages',\n\n // qr-http-server — url-box copy button\n 'dashboard.url.copy': 'Copy',\n 'dashboard.url.copied': 'Copied',\n\n // qr-http-server — inspector open link (#503)\n 'dashboard.inspector.section': 'Inspector',\n 'dashboard.inspector.open': 'Open inspector',\n 'dashboard.inspector.waiting': 'Inspector URL pending — appears after a page attaches',\n\n // qr-http-server — attach page (server-side, Node, per-request)\n // Copy branches per session mode into sandbox (env 2) / intoss (env 3·4) families (#468).\n 'attach.title': 'AIT Debug Session — QR Scan',\n 'attach.deployment': 'deployment: {label}',\n 'attach.steps.section': 'How to scan',\n 'attach.faq.section': 'Troubleshooting checklist',\n 'attach.url.section': 'URL (fallback)',\n\n // qr-http-server — attach page mode label (environment visibility, #468)\n 'attach.mode.sandbox': 'env 2 — AITC Sandbox App (PWA)',\n 'attach.mode.intossDev': 'env 3 — intoss-private relay dev',\n 'attach.mode.intossLive': 'env 4 — intoss live relay debug',\n\n // attach page — sandbox family (env 2: launcher PWA; no Toss app / _deploymentId concepts)\n 'attach.sandbox.step1':\n 'Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).',\n 'attach.sandbox.step2':\n 'Scan this QR code with <strong>\"Scan QR with camera\"</strong> inside the launcher.',\n 'attach.sandbox.step3':\n 'The mini-app opens fullscreen and the debug session attaches automatically.',\n 'attach.sandbox.faq.notInstalled':\n '<strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen',\n 'attach.sandbox.faq.cameraApp':\n '<strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner',\n 'attach.sandbox.faq.totp':\n '<strong>QR expired (TOTP — 30-second step, ±6 steps (~3 min) accepted)</strong> — scan a fresh QR code',\n 'attach.sandbox.faq.chii':\n '<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import',\n\n // attach page — intoss family (env 3·4: Toss app deep-link)\n 'attach.intoss.step1': 'Open the Toss app.',\n 'attach.intoss.step2': 'Scan the QR code with your phone camera app.',\n 'attach.intoss.step3': 'Tap <strong>\"Open in Toss\"</strong> when the popup appears.',\n 'attach.intoss.step4': 'The mini-app opens and the debug session attaches automatically.',\n 'attach.intoss.faq.appNotOpen':\n '<strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)',\n 'attach.intoss.faq.prepare':\n '<strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter',\n 'attach.intoss.faq.chii':\n '<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import',\n 'attach.intoss.faq.totp':\n '<strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server',\n // env 4 (relay-live) only — appended to the intoss family at runtime (#468).\n 'attach.intoss.faq.liveReadOnly':\n '<strong>LIVE session is read-only</strong> — <code>call_sdk</code>/<code>evaluate</code> require an explicit <code>confirm</code>',\n\n // Launcher PWA\n 'launcher.title': 'AITC DevTools Launcher',\n 'launcher.description': 'Scan the terminal QR code or paste the tunnel URL.',\n 'launcher.installCta': 'Install launcher to your phone',\n 'launcher.urlPlaceholder': 'https://example.trycloudflare.com',\n 'launcher.openBtn': 'Open',\n 'launcher.scanBtn': 'Scan QR with camera',\n 'launcher.noCamera': 'No camera available — paste the URL instead.',\n 'launcher.cameraError': 'Could not access the camera — paste the URL instead.',\n 'launcher.invalidUrlHttps': 'Enter a valid https:// URL (the tunnel URL from your terminal).',\n 'launcher.invalidUrl': 'Enter a valid http(s):// URL.',\n 'launcher.debugAuthFailed': 'Debug connection authentication failed',\n 'launcher.debugAuthFailedHint': 'The QR code may have expired. Scan a fresh QR code.',\n 'launcher.debugAuthExpiredHint':\n 'The debug session has expired. Scan a fresh QR from the attach page on your Mac.',\n 'launcher.debugAuthRescanCta': 'Scan a new QR',\n 'launcher.diagTitle': 'Viewport diagnostics',\n 'launcher.diagYes': 'yes',\n 'launcher.diagNo': 'no',\n 'launcher.letterboxDetected': 'Letterbox correction +{pt}pt applied — using full screen height.',\n // Nav-bar emulation (#495/#510)\n 'launcher.navbar.defaultTitle': 'Mini App',\n 'launcher.navbar.back': 'Back',\n 'launcher.navbar.menu': 'Menu',\n 'launcher.navbar.close': 'Close',\n 'launcher.navbar.menuRescan': 'Rescan',\n 'launcher.navbar.menuDiag': 'Viewport diagnostics',\n 'launcher.navbar.menuLanguage': 'Language',\n};\n","// Korean string catalog (source of truth — keys are typed from this file).\n// Keys follow `<area>.<purpose>` convention. Variable interpolation uses\n// `{name}` placeholders resolved by `t(key, { name: value })`.\n//\n// Some chrome (button labels like \"Load\", \"Show\", \"Clear\", \"Apply\") is left as\n// English in both locales because the panel is an internal devtools surface\n// and these terms are universally recognised by developers in both locales.\n\nexport const ko = {\n // Panel chrome\n 'panel.title': 'AIT DevTools',\n 'panel.toggle.title': 'AIT DevTools',\n 'panel.close': 'Close',\n 'panel.editMode.on': 'EDIT',\n 'panel.editMode.off': 'READ-ONLY',\n 'panel.editMode.toggleTitle': '패널 편집 모드 전환',\n 'panel.tabError': '\"{tab}\" 탭 렌더링 중 오류가 발생했습니다.',\n\n // Tab names\n 'panel.tab.env': 'Environment',\n 'panel.tab.presets': 'Presets',\n 'panel.tab.viewport': 'Viewport',\n 'panel.tab.permissions': 'Permissions',\n 'panel.tab.notifications': 'Notifications',\n 'panel.tab.location': 'Location',\n 'panel.tab.device': 'Device',\n 'panel.tab.iap': 'IAP',\n 'panel.tab.ads': 'Ads',\n 'panel.tab.events': 'Events',\n 'panel.tab.analytics': 'Analytics',\n 'panel.tab.storage': 'Storage',\n\n // Common\n 'common.readOnly': '읽기 전용 — mock 응답은 빌드 타임에 고정됩니다.',\n\n // Consent toast\n 'toast.consent.title': '익명 사용 통계를 보낼까요?',\n 'toast.consent.body': '도구 개선을 위해 익명 이벤트만 수집해요. 언제든 환경 탭에서 끌 수 있어요.',\n 'toast.consent.learnMore': '더 알아보기',\n 'toast.consent.accept': '네, 보낼게요',\n 'toast.consent.deny': '아니요',\n\n // Environment tab\n 'env.section.platform': 'Platform',\n 'env.row.os': 'OS',\n 'env.row.appVersion': 'App Version',\n 'env.row.environment': 'Environment',\n 'env.row.locale': 'Locale',\n 'env.section.network': 'Network',\n 'env.row.networkStatus': 'Status',\n 'env.section.safeArea': 'Safe Area Insets',\n 'env.row.safeArea.top': 'Top',\n 'env.row.safeArea.bottom': 'Bottom',\n 'env.section.navigation': 'Navigation',\n 'env.row.iosSwipeGesture': 'iOS swipe-back',\n 'env.value.iosSwipeGesture.unset': '미호출',\n 'env.value.iosSwipeGesture.enabled': 'enabled',\n 'env.value.iosSwipeGesture.disabled': 'disabled',\n 'env.hint.iosSwipeGesture':\n 'setIosSwipeGestureEnabled의 마지막 호출값. Environment를 toss로 바꾸면 toss-gated 가드가 이 값을 토글합니다.',\n\n // Environment > Telemetry section\n 'env.telemetry.section': 'Telemetry',\n // Tier 0 — opt-out anonymous signal\n 'env.telemetry.t0Row': '익명 사용 신호 (Tier 0)',\n 'env.telemetry.t0On': 'On',\n 'env.telemetry.t0Off': 'Off',\n 'env.telemetry.t0TurnOn': 'Turn on',\n 'env.telemetry.t0TurnOff': 'Turn off',\n 'env.telemetry.t0Desc': '버전·날짜만 수집, PII 없음. 하루 1회. 패키지 개선에 사용됩니다.',\n // Tier 1 — opt-in extended telemetry\n 'env.telemetry.row': '확장 텔레메트리 (Tier 1)',\n 'env.telemetry.on': 'On',\n 'env.telemetry.off': 'Off',\n 'env.telemetry.turnOn': 'Turn on',\n 'env.telemetry.turnOff': 'Turn off',\n 'env.telemetry.anonIdLabel': 'anon_id: {value}',\n 'env.telemetry.anonIdNotSet': '(not yet set)',\n 'env.telemetry.anonIdCopyTitle': '전체 anon_id 복사',\n 'env.telemetry.deleteBtn': '내 데이터 삭제',\n 'env.telemetry.deleting': '삭제 중…',\n 'env.telemetry.deleted': '삭제 완료',\n 'env.telemetry.deleteFailedRetry': '삭제 실패 (다시 시도해주세요)',\n 'env.telemetry.deleteFailed': '삭제 실패',\n 'env.telemetry.privacyLink': '개인정보 처리방침 →',\n\n // Environment > Language toggle (new)\n 'env.section.language': 'Language',\n 'env.language.row': 'Language',\n 'env.language.ko': '한국어',\n 'env.language.en': 'English',\n\n // Permissions tab\n 'permissions.section.device': 'Device Permissions',\n\n // Location tab\n 'location.section.current': 'Current Location',\n 'location.row.latitude': 'Latitude',\n 'location.row.longitude': 'Longitude',\n 'location.row.accuracy': 'Accuracy',\n\n // Device tab\n 'device.section.modes': 'Device API Modes',\n 'device.row.camera': 'Camera',\n 'device.row.photos': 'Photos',\n 'device.row.location': 'Location',\n 'device.row.network': 'Network',\n 'device.row.clipboard': 'Clipboard',\n 'device.section.mockImages': 'Mock Images ({count})',\n 'device.btn.add': '+ Add',\n 'device.btn.useDefaults': 'Use defaults',\n 'device.btn.clear': 'Clear',\n 'device.prompt.camera.title': 'Camera Prompt — 이미지를 선택하세요',\n 'device.prompt.photos.title': 'Photos Prompt — 이미지를 선택하세요',\n 'device.prompt.location.title': 'Location Prompt — 좌표 입력',\n 'device.prompt.locationUpdate.title': 'Location Update — 좌표 전송',\n 'device.prompt.fallbackTitle': 'Prompt: {type}',\n 'device.prompt.label.lat': 'Lat',\n 'device.prompt.label.lng': 'Lng',\n 'device.prompt.send': 'Send',\n 'device.prompt.cancel': 'Cancel',\n\n // Device tab — Haptic section\n 'device.section.haptic': 'Haptic',\n 'device.haptic.lastCall': '마지막 haptic',\n 'device.haptic.noneYet': '(아직 없음)',\n 'device.haptic.trigger': 'Haptic 트리거',\n\n // Viewport tab\n 'viewport.section.device': 'Device',\n 'viewport.row.preset': 'Preset',\n 'viewport.row.orientation': 'Orientation',\n 'viewport.row.notchSide': 'Notch side',\n 'viewport.section.custom': 'Custom size',\n 'viewport.row.width': 'Width (px)',\n 'viewport.row.height': 'Height (px)',\n 'viewport.section.appearance': 'Appearance',\n 'viewport.row.showFrame': 'Show frame',\n 'viewport.row.showAitNavBar': 'Apps in Toss 내비게이션 바 표시',\n 'viewport.row.navBarType': 'Nav bar type',\n 'viewport.status.noConstraint': '뷰포트 제약 없음 — body가 창을 가득 채웁니다.',\n 'viewport.status.cssPhysical': 'CSS / physical',\n 'viewport.status.safeArea': 'Safe area',\n 'viewport.status.aitNavBar': 'AIT nav bar',\n 'viewport.status.aitNavBarValue': '{height}px → SafeArea top · {type}',\n 'viewport.orientation.autoSuffix': '{orient} (auto)',\n\n // IAP tab\n 'iap.section.simulator': 'IAP Simulator',\n 'iap.row.nextResult': 'Next Purchase Result',\n 'iap.section.tossPay': 'TossPay',\n 'iap.row.tossPayResult': 'Next Payment Result',\n 'iap.section.pending': 'Pending Orders ({count})',\n 'iap.empty.pending': '(대기 중인 주문 없음)',\n 'iap.section.completed': 'Completed Orders ({count})',\n 'iap.empty.completed': '(완료된 주문 없음)',\n 'iap.btn.complete': 'Complete',\n 'iap.label.pending': 'PENDING',\n\n // Events tab\n 'events.section.navigation': 'Navigation Events',\n 'events.btn.triggerBack': 'Back 이벤트 발생',\n 'events.btn.triggerHome': 'Home 이벤트 발생',\n 'events.section.login': 'Login',\n 'events.row.loggedIn': 'Logged In',\n 'events.row.tossLoginIntegrated': 'Toss Login Integrated',\n\n // Analytics tab\n 'analytics.section.log': 'Analytics Log ({count})',\n 'analytics.btn.clear': 'Clear',\n 'analytics.calls.section': 'SDK Calls ({count})',\n 'analytics.calls.btn.clear': 'Clear',\n 'analytics.calls.empty': '(아직 SDK 호출 없음)',\n\n // Storage tab\n 'storage.section.title': 'Storage ({count} items)',\n 'storage.btn.clearAll': 'Clear All',\n 'storage.empty': '저장된 항목이 없습니다',\n\n // Presets tab\n 'presets.section.builtIn': 'Built-in scenarios',\n 'presets.section.saved': 'Saved presets ({count})',\n 'presets.section.save': 'Save',\n 'presets.save.description':\n 'network / permissions / auth / IAP / ads / payment 슬라이스를 캡처합니다.',\n 'presets.btn.saveCurrent': '현재 상태를 프리셋으로 저장',\n 'presets.btn.apply': 'Apply',\n 'presets.btn.reApply': 'Re-apply',\n 'presets.btn.delete': 'Delete',\n 'presets.empty.saved': '저장된 프리셋이 아직 없습니다.',\n 'presets.empty.builtIn': '내장 프리셋이 없습니다.',\n 'presets.prompt.label': '프리셋 라벨을 입력하세요',\n 'presets.confirm.delete': '\"{label}\" 프리셋을 삭제할까요?',\n\n // Ads tab\n 'ads.section.state': 'Ads State',\n 'ads.row.isLoaded': 'isLoaded',\n 'ads.row.forceNoFill': '강제 \"no fill\"',\n 'ads.empty.events': '아직 이벤트가 없습니다',\n 'ads.section.googleAdMob': 'GoogleAdMob',\n 'ads.section.tossAds': 'TossAds',\n 'ads.section.fullScreenAd': 'FullScreenAd',\n 'ads.btn.load': 'Load',\n 'ads.btn.show': 'Show',\n 'ads.section.tossAdsBanner': 'TossAds 배너',\n 'ads.row.rewardUnitType': '리워드 단위 타입',\n 'ads.row.rewardAmount': '리워드 수량',\n 'ads.btn.render': 'Render',\n 'ads.btn.noFill': 'No-fill',\n 'ads.btn.click': 'Click',\n 'ads.btn.destroy': 'Destroy',\n\n // Notifications tab\n 'notifications.section.title': 'requestNotificationAgreement',\n 'notifications.option.newAgreement': 'newAgreement (최초 동의)',\n 'notifications.option.alreadyAgreed': 'alreadyAgreed (이미 동의됨)',\n 'notifications.option.agreementRejected': 'agreementRejected (사용자 거절)',\n\n // qr-http-server — lang switcher (dashboard / attach pages)\n 'dashboard.lang.ko': '한국어',\n 'dashboard.lang.en': 'English',\n\n // qr-http-server — dashboard page (server-side, Node, per-request)\n 'dashboard.title': 'AIT 디버그 Dashboard',\n 'dashboard.updated': '마지막 갱신: {ts}',\n 'dashboard.tunnel.section': '터널 상태',\n 'dashboard.tunnel.up': '연결됨',\n 'dashboard.tunnel.down': '끊어짐',\n 'dashboard.attach.section': 'Attach QR',\n 'dashboard.attach.hint': 'build_attach_url MCP tool을 호출하면 QR이 여기에 표시됩니다.',\n 'dashboard.pages.section': '연결된 Pages',\n 'dashboard.pages.empty': 'attach된 페이지 없음',\n\n // qr-http-server — url-box copy button\n 'dashboard.url.copy': '복사',\n 'dashboard.url.copied': '복사됨',\n\n // qr-http-server — inspector open link (#503)\n 'dashboard.inspector.section': '인스펙터',\n 'dashboard.inspector.open': '인스펙터 열기',\n 'dashboard.inspector.waiting': '인스펙터 URL 대기 중 (페이지 attach 후 표시됩니다)',\n\n // qr-http-server — attach page (server-side, Node, per-request)\n // 카피는 세션 mode별로 sandbox(환경 2) / intoss(환경 3·4) family로 분기한다 (#468).\n 'attach.title': 'AIT 디버그 세션 — QR 스캔',\n 'attach.deployment': 'deployment: {label}',\n 'attach.steps.section': '스캔 절차',\n 'attach.faq.section': '진단 체크리스트',\n 'attach.url.section': 'URL (fallback)',\n\n // qr-http-server — attach page mode 라벨 (환경 가시화, #468)\n 'attach.mode.sandbox': '환경 2 — AITC Sandbox App (PWA)',\n 'attach.mode.intossDev': '환경 3 — intoss-private relay dev',\n 'attach.mode.intossLive': '환경 4 — intoss live relay debug',\n\n // attach page — sandbox family (환경 2: launcher PWA, 토스 앱·_deploymentId 개념 없음)\n 'attach.sandbox.step1':\n '홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).',\n 'attach.sandbox.step2':\n 'launcher 안의 <strong>\"QR 카메라로 스캔\"</strong>으로 이 QR 코드를 스캔하세요.',\n 'attach.sandbox.step3': '미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.',\n 'attach.sandbox.faq.notInstalled':\n '<strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요',\n 'attach.sandbox.faq.cameraApp':\n '<strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요',\n 'attach.sandbox.faq.totp':\n '<strong>QR이 만료된 경우 (TOTP — 코드 1개는 30초 창, 만료 후 ~3분(±6 step) 이내 소급 허용)</strong> — 새 QR을 다시 스캔하세요',\n 'attach.sandbox.faq.chii':\n '<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인',\n\n // attach page — intoss family (환경 3·4: 토스 앱 deep-link)\n 'attach.intoss.step1': '토스 앱을 실행하세요.',\n 'attach.intoss.step2': '폰 카메라 앱으로 QR 코드를 스캔하세요.',\n 'attach.intoss.step3': '팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.',\n 'attach.intoss.step4': '미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.',\n 'attach.intoss.faq.appNotOpen':\n '<strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)',\n 'attach.intoss.faq.prepare':\n '<strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인',\n 'attach.intoss.faq.chii':\n '<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인',\n 'attach.intoss.faq.totp':\n '<strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인',\n // 환경 4(relay-live) 전용 — intoss family에 런타임으로 한 줄 추가된다 (#468).\n 'attach.intoss.faq.liveReadOnly':\n '<strong>LIVE 세션은 read-only입니다</strong> — <code>call_sdk</code>/<code>evaluate</code> 실행에는 명시적 <code>confirm</code>이 필요합니다',\n\n // Launcher PWA\n 'launcher.title': 'AITC DevTools Launcher',\n 'launcher.description': '터미널 QR을 스캔하거나 URL을 입력하세요.',\n 'launcher.installCta': '폰에 런처 설치하기',\n 'launcher.urlPlaceholder': 'https://example.trycloudflare.com',\n 'launcher.openBtn': 'Open',\n 'launcher.scanBtn': 'QR 카메라로 스캔',\n 'launcher.noCamera': '카메라를 사용할 수 없습니다 — URL을 직접 붙여넣으세요.',\n 'launcher.cameraError': '카메라에 접근할 수 없습니다 — URL을 직접 붙여넣으세요.',\n 'launcher.invalidUrlHttps': '올바른 https:// URL을 입력하세요 (터미널의 터널 URL).',\n 'launcher.invalidUrl': '올바른 http(s):// URL을 입력하세요.',\n 'launcher.debugAuthFailed': '디버그 연결 인증 실패',\n 'launcher.debugAuthFailedHint': 'QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.',\n 'launcher.debugAuthExpiredHint':\n '디버그 세션이 만료됐어요. Mac의 attach 페이지에서 새 QR을 스캔하세요.',\n 'launcher.debugAuthRescanCta': '새 QR 스캔하기',\n 'launcher.diagTitle': '뷰포트 진단',\n 'launcher.diagYes': '예',\n 'launcher.diagNo': '아니요',\n 'launcher.letterboxDetected': 'letterbox 보정 +{pt}pt 적용됨 — 화면 전체를 사용합니다.',\n // Nav-bar emulation (#495/#510)\n 'launcher.navbar.defaultTitle': '미니앱',\n 'launcher.navbar.back': '뒤로가기',\n 'launcher.navbar.menu': '메뉴',\n 'launcher.navbar.close': '닫기',\n 'launcher.navbar.menuRescan': '다시 스캔',\n 'launcher.navbar.menuDiag': '뷰포트 진단',\n 'launcher.navbar.menuLanguage': '언어',\n} as const;\n\nexport type StringKey = keyof typeof ko;\n","/**\n * Vanilla TS i18n for the floating DevTools panel.\n *\n * Public surface:\n * - `t(key, vars?)` — look up a UI string, with `{name}` placeholder\n * interpolation. Falls back to the key itself if a translation is missing.\n * - `getLocale()` / `setLocale(locale)` — read/persist the active locale.\n * `setLocale` dispatches `__ait:localechange` so the panel can remount.\n * - `detectLocale()` — first-run heuristic from `navigator.language`.\n *\n * `ko` is the source of truth (keys are typed from it). `en` is also a full\n * `Record<StringKey, string>` (devtools is developer-facing, en is a real\n * audience). The `Partial` lookup table preserves the runtime `?? key` safety\n * net even though we ship complete catalogs today.\n */\n\nimport { en } from './en.js';\nimport { ko, type StringKey } from './ko.js';\n\nexport type Locale = 'ko' | 'en';\n\nconst LOCALE_STORAGE_KEY = '__ait_locale';\nconst LOCALE_CHANGE_EVENT = '__ait:localechange';\n\nconst tables: Record<Locale, Partial<Record<StringKey, string>>> = { ko, en };\n\nlet currentLocale: Locale | null = null;\n\nfunction safeReadStorage(): Locale | null {\n if (typeof localStorage === 'undefined') return null;\n try {\n const raw = localStorage.getItem(LOCALE_STORAGE_KEY);\n if (raw === 'ko' || raw === 'en') return raw;\n } catch {\n /* localStorage can throw in privacy modes — fall back silently */\n }\n return null;\n}\n\nfunction safeWriteStorage(locale: Locale): void {\n if (typeof localStorage === 'undefined') return;\n try {\n localStorage.setItem(LOCALE_STORAGE_KEY, locale);\n } catch {\n /* ignore quota / privacy errors */\n }\n}\n\n/**\n * Decide a locale from a BCP-47 language tag. `ko` (and `ko-*`) → `'ko'`,\n * everything else → `'en'`. Shared by the browser (`navigator.language`) and\n * Node (`Accept-Language` header) paths so both resolve identically.\n */\nfunction localeFromLanguageTag(lang: string): Locale {\n return /^ko\\b/i.test(lang) ? 'ko' : 'en';\n}\n\n/**\n * Read `navigator.language` and decide a locale. `ko` (and `ko-*`) → `'ko'`,\n * everything else → `'en'`. Pure function; does not touch storage.\n */\nexport function detectLocale(): Locale {\n if (typeof navigator === 'undefined') return 'en';\n return localeFromLanguageTag(navigator.language ?? '');\n}\n\n/**\n * Decide a locale from an HTTP `Accept-Language` header value. The Node-served\n * surfaces (e.g. the qr-http-server dashboard) have no `navigator`, so the\n * request header is the only language signal. Reads the FIRST language tag\n * (highest priority, ignoring `q=` weights — good enough for ko/en) and feeds\n * it through the same `ko`-vs-`en` heuristic `detectLocale` uses. Returns `'ko'`\n * for an empty/missing header (ko is the primary locale).\n */\nexport function parseAcceptLanguage(header: string | undefined | null): Locale {\n if (!header) return 'ko';\n const first = header.split(',')[0]?.trim().split(';')[0]?.trim() ?? '';\n return localeFromLanguageTag(first);\n}\n\n/**\n * A locale-bound string resolver for surfaces that can't use the in-memory\n * `getLocale()` cache — notably the Node HTTP server, which resolves locale\n * per-request from `Accept-Language` rather than from a process-global. Returns\n * a `t`-compatible closure over the SAME `ko`/`en` tables (single source of\n * truth), so the dashboard/attach HTML shares the exact 169-key catalog the\n * browser surfaces use. The `key: StringKey` signature keeps compile-time key\n * safety on the Node path identical to `t()`.\n */\nexport function resolveLocaleStrings(\n locale: Locale,\n): (key: StringKey, vars?: Record<string, string | number>) => string {\n const table = tables[locale];\n return (key, vars) => {\n const raw = table[key] ?? key;\n if (!vars) return raw;\n return raw.replace(/\\{(\\w+)\\}/g, (match, name: string) => {\n const value = vars[name];\n return value === undefined ? match : String(value);\n });\n };\n}\n\n/**\n * Resolve the active locale, in order:\n * 1. previously set in-memory value (set by `setLocale`)\n * 2. localStorage `__ait_locale`\n * 3. `detectLocale()` from navigator\n */\nexport function getLocale(): Locale {\n if (currentLocale) return currentLocale;\n const stored = safeReadStorage();\n currentLocale = stored ?? detectLocale();\n return currentLocale;\n}\n\n/**\n * Persist a locale choice and notify listeners. The panel listens for\n * `__ait:localechange` and re-mounts so every string re-evaluates.\n */\nexport function setLocale(locale: Locale): void {\n currentLocale = locale;\n safeWriteStorage(locale);\n if (typeof window !== 'undefined') {\n window.dispatchEvent(new CustomEvent(LOCALE_CHANGE_EVENT));\n }\n}\n\n/**\n * Look up a UI string for the current locale. Falls back to the key if missing,\n * so a forgotten key surfaces visibly rather than rendering empty.\n */\nexport function t(key: StringKey, vars?: Record<string, string | number>): string {\n const raw = tables[getLocale()][key] ?? key;\n if (!vars) return raw;\n return raw.replace(/\\{(\\w+)\\}/g, (match, name: string) => {\n const value = vars[name];\n return value === undefined ? match : String(value);\n });\n}\n\nexport type { StringKey };\nexport { LOCALE_CHANGE_EVENT, LOCALE_STORAGE_KEY };\n\n/**\n * Test-only escape hatch — resets the cached in-memory locale so subsequent\n * `getLocale()` calls re-read storage / re-detect. Production code never needs\n * this; tests use it between cases.\n */\nexport function _resetLocaleCacheForTests(): void {\n currentLocale = null;\n}\n","/**\n * dashboard.generated.ts\n *\n * AUTO-GENERATED by scripts/build-dashboard-html.ts — DO NOT EDIT BY HAND.\n * Regenerate: pnpm build:dashboard-html\n *\n * Exports precompiled HTML chrome strings for each locale. Per-request\n * dynamic values are inserted by qr-http-server.ts at runtime via simple\n * string replacement of __PLACEHOLDER__ tokens.\n *\n * Token map (dashboard chrome):\n * __TUNNEL_CLASS__ CSS class: \"status-up\" | \"status-down\"\n * __TUNNEL_STATUS__ localised tunnel status label\n * __ATTACH_SECTION__ QR img+url-box HTML, or hint text\n * __INSPECTOR_SECTION__ inspector link <a> or waiting hint <span> (#503)\n * __PAGES_SECTION__ pages <section> block, or empty string\n * __NOW__ ISO timestamp of current render\n * __LANG_SWITCHER__ ko/en toggle links (href preserves existing query params)\n *\n * Token map (attach chrome — precompiled per locale × copy family, #468):\n * __QR_DATA_URL__ base64 data URL for the QR image\n * __SAFE_LABEL__ HTML-escaped deploymentId label (intoss family only)\n * __SAFE_ATTACH_URL__ HTML-escaped attach URL\n * __MODE_LABEL__ environment badge (<p class=\"mode-label\">…</p>), or empty\n * __LIVE_FAQ__ env-4 LIVE read-only <li>, or empty (intoss family only)\n * __LANG_SWITCHER__ ko/en toggle links (href preserves existing query params)\n *\n * SECRET-HANDLING: wssUrl MUST NOT appear here. If it does, the build script's\n * assertion would have caught it — this file should be react-free and secret-free.\n */\n\nimport type { Locale } from '../i18n/index.js';\n\n/** Copy family of the attach page chrome (#468) — env 2 vs env 3/4. */\nexport type AttachChromeFamily = 'sandbox' | 'intoss';\n\n// ── locale: ko ──────────────────────────────────────────────────────\n\nexport const dashboardChromeHtmlKo =\n`<!DOCTYPE html>\n<html lang=\"ko\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><title>AIT 디버그 Dashboard</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.updated { font-size: 0.75rem; opacity: 0.4; font-family: monospace; margin: 0; }\nsection { width: 100%; max-width: 520px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\n.status { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }\n.status-up { background: #238636; color: #fff; }\n.status-down { background: #6e7681; color: #fff; }\nimg.qr {\n width: min(80vw, 300px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 0.75rem; border-radius: 10px;\n display: block; margin: 0.5rem auto;\n}\n.url-row {\n display: flex; align-items: stretch; gap: 0; margin: 0.5rem 0 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.7rem;\n word-break: break-all; opacity: 0.45;\n background: #161b22; padding: 0.6rem 0.85rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.65; }\n.copy-btn {\n flex-shrink: 0; padding: 0.4rem 0.7rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.7rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\n.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\nul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }\nli.empty { opacity: 0.4; list-style: none; padding-left: 0; }\n.page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }\n.page-url { word-break: break-all; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n</style></head><body><h1>AIT 디버그 Dashboard</h1>__LANG_SWITCHER__<p class=\"updated\" id=\"updated\">마지막 갱신: __NOW__</p><section><h2>터널 상태</h2><span class=\"status __TUNNEL_CLASS__\" id=\"tunnel-status\">__TUNNEL_STATUS__</span></section><hr/><section><h2>Attach QR</h2><div id=\"attach-section\">__ATTACH_SECTION__</div></section><hr/><section id=\"inspector-section\"><h2>인스펙터</h2>__INSPECTOR_SECTION__</section>__PAGES_SECTION__</body></html>`;\n\nexport const attachChromeHtmlKoSandbox =\n`<!DOCTYPE html>\n<html lang=\"ko\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><link rel=\"preload\" as=\"image\" href=\"__QR_DATA_URL__\"/><title>AIT 디버그 세션 — QR 스캔</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.mode-label {\n font-size: 0.78rem; font-weight: 600; color: #79c0ff;\n background: #161b22; border: 1px solid #30363d; border-radius: 999px;\n padding: 0.25rem 0.75rem; margin: 0;\n}\n.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\nimg.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n display: block; margin: 0 auto;\n}\nsection { width: 100%; max-width: 480px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\nol, ul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\n.url-row {\n display: flex; align-items: stretch; gap: 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.72rem;\n word-break: break-all; opacity: 0.4;\n background: #161b22; padding: 0.75rem 1rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.6; }\n.copy-btn {\n flex-shrink: 0; padding: 0.5rem 0.8rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<div id=\"attach-section\"><img class=\"qr\" src=\"__QR_DATA_URL__\" alt=\"attach QR\"/></div><section><h2>스캔 절차</h2><ol><li>홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).</li><li>launcher 안의 <strong>\"QR 카메라로 스캔\"</strong>으로 이 QR 코드를 스캔하세요.</li><li>미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요</li><li><strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요</li><li><strong>QR이 만료된 경우 (TOTP — 코드 1개는 30초 창, 만료 후 ~3분(±6 step) 이내 소급 허용)</strong> — 새 QR을 다시 스캔하세요</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li></ul></section><hr/><section id=\"url-section\"><h2>URL (fallback)</h2><div class=\"url-row\"><p class=\"url-box\" id=\"url-box\">__SAFE_ATTACH_URL__</p><button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"복사\">복사</button></div></section></body></html>`;\n\nexport const attachChromeHtmlKoIntoss =\n`<!DOCTYPE html>\n<html lang=\"ko\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><link rel=\"preload\" as=\"image\" href=\"__QR_DATA_URL__\"/><title>AIT 디버그 세션 — QR 스캔</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.mode-label {\n font-size: 0.78rem; font-weight: 600; color: #79c0ff;\n background: #161b22; border: 1px solid #30363d; border-radius: 999px;\n padding: 0.25rem 0.75rem; margin: 0;\n}\n.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\nimg.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n display: block; margin: 0 auto;\n}\nsection { width: 100%; max-width: 480px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\nol, ul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\n.url-row {\n display: flex; align-items: stretch; gap: 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.72rem;\n word-break: break-all; opacity: 0.4;\n background: #161b22; padding: 0.75rem 1rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.6; }\n.copy-btn {\n flex-shrink: 0; padding: 0.5rem 0.8rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<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>__LIVE_FAQ__</ul></section><hr/><section id=\"url-section\"><h2>URL (fallback)</h2><div class=\"url-row\"><p class=\"url-box\" id=\"url-box\">__SAFE_ATTACH_URL__</p><button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"복사\">복사</button></div></section></body></html>`;\n\n// ── locale: en ──────────────────────────────────────────────────────\n\nexport const dashboardChromeHtmlEn =\n`<!DOCTYPE html>\n<html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><title>AIT Debug Dashboard</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.updated { font-size: 0.75rem; opacity: 0.4; font-family: monospace; margin: 0; }\nsection { width: 100%; max-width: 520px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\n.status { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }\n.status-up { background: #238636; color: #fff; }\n.status-down { background: #6e7681; color: #fff; }\nimg.qr {\n width: min(80vw, 300px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 0.75rem; border-radius: 10px;\n display: block; margin: 0.5rem auto;\n}\n.url-row {\n display: flex; align-items: stretch; gap: 0; margin: 0.5rem 0 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.7rem;\n word-break: break-all; opacity: 0.45;\n background: #161b22; padding: 0.6rem 0.85rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.65; }\n.copy-btn {\n flex-shrink: 0; padding: 0.4rem 0.7rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.7rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\n.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }\n.inspector-link {\n display: inline-block; margin-top: 0.5rem;\n padding: 0.45rem 1rem; border-radius: 6px;\n background: #1f6feb; color: #fff; font-size: 0.85rem; font-weight: 600;\n text-decoration: none; text-align: center;\n}\n.inspector-link:hover { background: #388bfd; }\n.inspector-hint { display: inline-block; margin-top: 0.5rem; font-size: 0.8rem; opacity: 0.45; }\nul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }\nli.empty { opacity: 0.4; list-style: none; padding-left: 0; }\n.page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }\n.page-url { word-break: break-all; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n</style></head><body><h1>AIT Debug Dashboard</h1>__LANG_SWITCHER__<p class=\"updated\" id=\"updated\">Last updated: __NOW__</p><section><h2>Tunnel status</h2><span class=\"status __TUNNEL_CLASS__\" id=\"tunnel-status\">__TUNNEL_STATUS__</span></section><hr/><section><h2>Attach QR</h2><div id=\"attach-section\">__ATTACH_SECTION__</div></section><hr/><section id=\"inspector-section\"><h2>Inspector</h2>__INSPECTOR_SECTION__</section>__PAGES_SECTION__</body></html>`;\n\nexport const attachChromeHtmlEnSandbox =\n`<!DOCTYPE html>\n<html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><link rel=\"preload\" as=\"image\" href=\"__QR_DATA_URL__\"/><title>AIT Debug Session — QR Scan</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.mode-label {\n font-size: 0.78rem; font-weight: 600; color: #79c0ff;\n background: #161b22; border: 1px solid #30363d; border-radius: 999px;\n padding: 0.25rem 0.75rem; margin: 0;\n}\n.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\nimg.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n display: block; margin: 0 auto;\n}\nsection { width: 100%; max-width: 480px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\nol, ul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\n.url-row {\n display: flex; align-items: stretch; gap: 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.72rem;\n word-break: break-all; opacity: 0.4;\n background: #161b22; padding: 0.75rem 1rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.6; }\n.copy-btn {\n flex-shrink: 0; padding: 0.5rem 0.8rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n</style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<div id=\"attach-section\"><img class=\"qr\" src=\"__QR_DATA_URL__\" alt=\"attach QR\"/></div><section><h2>How to scan</h2><ol><li>Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).</li><li>Scan this QR code with <strong>\"Scan QR with camera\"</strong> inside the launcher.</li><li>The mini-app opens fullscreen and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen</li><li><strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner</li><li><strong>QR expired (TOTP — 30-second step, ±6 steps (~3 min) accepted)</strong> — scan a fresh QR code</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li></ul></section><hr/><section id=\"url-section\"><h2>URL (fallback)</h2><div class=\"url-row\"><p class=\"url-box\" id=\"url-box\">__SAFE_ATTACH_URL__</p><button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"Copy\">Copy</button></div></section></body></html>`;\n\nexport const attachChromeHtmlEnIntoss =\n`<!DOCTYPE html>\n<html lang=\"en\"><head><meta charSet=\"utf-8\"/><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\"/><link rel=\"preload\" as=\"image\" href=\"__QR_DATA_URL__\"/><title>AIT Debug Session — QR Scan</title><style>\n*, *::before, *::after { box-sizing: border-box; }\nbody {\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n background: #0d1117; color: #c9d1d9;\n display: flex; flex-direction: column; align-items: center;\n min-height: 100vh; margin: 0; padding: 2rem 1rem;\n gap: 1.5rem;\n}\nh1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }\n.mode-label {\n font-size: 0.78rem; font-weight: 600; color: #79c0ff;\n background: #161b22; border: 1px solid #30363d; border-radius: 999px;\n padding: 0.25rem 0.75rem; margin: 0;\n}\n.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }\nimg.qr {\n width: min(90vw, 360px); height: auto;\n image-rendering: pixelated;\n background: #fff; padding: 1rem; border-radius: 12px;\n display: block; margin: 0 auto;\n}\nsection { width: 100%; max-width: 480px; }\nh2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }\nol, ul { margin: 0; padding-left: 1.25rem; }\nli { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }\n.url-row {\n display: flex; align-items: stretch; gap: 0;\n border-radius: 6px; border: 1px solid #30363d; overflow: hidden;\n}\n.url-box {\n font-family: monospace; font-size: 0.72rem;\n word-break: break-all; opacity: 0.4;\n background: #161b22; padding: 0.75rem 1rem;\n flex: 1; cursor: pointer; border: none; border-radius: 0;\n}\n.url-box:hover { opacity: 0.6; }\n.copy-btn {\n flex-shrink: 0; padding: 0.5rem 0.8rem;\n background: #21262d; border: none; border-left: 1px solid #30363d;\n color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;\n font-family: -apple-system, BlinkMacSystemFont, \"Segoe UI\", sans-serif;\n}\n.copy-btn:hover { background: #30363d; }\nhr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }\n.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }\n.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }\n.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }\n</style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<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>__LIVE_FAQ__</ul></section><hr/><section id=\"url-section\"><h2>URL (fallback)</h2><div class=\"url-row\"><p class=\"url-box\" id=\"url-box\">__SAFE_ATTACH_URL__</p><button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"Copy\">Copy</button></div></section></body></html>`;\n\n/** Map from Locale to the precompiled dashboard chrome string. */\nexport const dashboardChromeByLocale: Record<Locale, string> = {\n ko: dashboardChromeHtmlKo,\n en: dashboardChromeHtmlEn,\n};\n\n/** Map from Locale × copy family to the precompiled attach page chrome string (#468). */\nexport const attachChromeByLocale: Record<Locale, Record<AttachChromeFamily, string>> = {\n ko: { sandbox: attachChromeHtmlKoSandbox, intoss: attachChromeHtmlKoIntoss },\n en: { sandbox: attachChromeHtmlEnSandbox, intoss: attachChromeHtmlEnIntoss },\n};\n","/**\n * 로컬 HTTP 서버 — QR 페이지를 `http://127.0.0.1:<port>` 에서 서빙한다.\n *\n * file:// origin 대신 HTTP origin을 쓰는 이유: 브라우저 보안 정책상 file://에서\n * 로드된 페이지는 외부 fetch/script가 전부 차단되며, file:// 절대 경로를 <img src>에\n * 넣으면 브라우저에 따라 빈 화면이 된다. 127.0.0.1 HTTP는 modern 브라우저가 fully trust.\n *\n * INSTALL-GRAPH INVARIANT:\n * 이 모듈은 react/react-dom을 절대 import하지 않는다. dashboard/attach HTML은\n * scripts/build-dashboard-html.ts가 빌드 타임에 precompile해 dashboard.generated.ts\n * (plain string exports)로 커밋한다. 이 모듈은 그 생성된 string만 import한다.\n * check-mcp-react-free.sh 가드가 dist/mcp/cli.js·server.js의 react 유입을 기계적으로 검증.\n *\n * HTML 조립 전략 (token-fill vs runtime builder):\n * - static chrome (head/style/섹션 레이블) → 빌드타임 precompile, dashboard.generated.ts\n * - 동적 부분 → 런타임 string 조립:\n * __NOW__ : per-request ISO timestamp\n * __TUNNEL_CLASS__ : \"status-up\" | \"status-down\"\n * __TUNNEL_STATUS__ : 로컬라이즈된 tunnel 상태 레이블\n * __ATTACH_SECTION__ : QR img+url-box, 또는 hint 텍스트\n * __PAGES_SECTION__ : pages <section> 블록, 또는 빈 문자열 (null → '')\n * - inline SSE <script> → 런타임 suffix로 append (localised string 포함)\n *\n * i18n:\n * GET / 와 GET /attach 라우트에서 req.headers['accept-language']를 읽어\n * parseAcceptLanguage()로 locale 결정. resolveLocaleStrings()로 동적 부분의\n * localised 문자열을 해결. navigator 없음, React hook 없음 (Node 표면).\n *\n * SECRET-HANDLING:\n * - 127.0.0.1 바인딩만 — 외부 노출 0.\n * - attachUrl은 HTML 본문과 /qr.png query에만 들어간다 (의도된 전달 경로).\n * - wssUrl은 dashboard HTML에 절대 들어가지 않는다. tunnel.up boolean만 사용.\n * - stdout/stderr/로그에 별도 출력하지 않는다.\n * - tmp 파일 만들지 않음 — 모든 응답을 메모리에서 생성.\n * - TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — SSE payload나 page 목록 등\n * 다른 필드에 TOTP 코드를 평문으로 싣지 않는다.\n */\n\nimport type { IncomingMessage, Server, ServerResponse } from 'node:http';\nimport { createServer } from 'node:http';\nimport { parseAcceptLanguage, resolveLocaleStrings } from '../i18n/index.js';\nimport {\n type AttachChromeFamily,\n attachChromeByLocale,\n dashboardChromeByLocale,\n} from './dashboard.generated.js';\nimport type { McpEnvironment } from './environment.js';\n\n/** dashboard에 노출되는 현재 상태 스냅샷. */\nexport interface DashboardState {\n /** 현재 터널 상태 — up/down + wssUrl. SECRET: wssUrl은 로그 출력 금지. */\n tunnel: { up: boolean; wssUrl: string | null };\n /**\n * 현재 연결된 page 목록 (id/url만).\n *\n * - `Array<…>` — env 3/4(MCP): relay에 attach된 페이지를 라이브 조회한 목록.\n * 빈 배열 `[]`은 \"attach된 페이지 없음\"으로 정직하게 표시한다.\n * - `null` — env 2(unplugin 터널): 플러그인 핸들이 connected target을 노출하지\n * 않아 라이브 page 목록을 알 수 없다. 거짓 빈 목록을 보여주느니 \"연결된 Pages\"\n * 섹션 자체를 숨긴다(#411). 정적 렌더와 SSE 갱신 양쪽에서 섹션이 사라진다.\n */\n pages: Array<{ id: string; url: string }> | null;\n /** 마지막으로 생성된 attachUrl (없으면 null). TOTP at= 코드는 이 안에 캡슐화. */\n attachUrl: string | null;\n /**\n * 현재 세션의 Chii 인스펙터 URL — 살아있는 세션 기준 DevTools 진입점 (#503).\n *\n * - `string` — relay up + 페이지 attached → `buildChiiInspectorUrl`로 조립된 URL.\n * TOTP at= 코드는 이 URL 안에 캡슐화. 대시보드 HTML 내 렌더는 의도된 transport.\n * - `null` — relay up이지만 페이지 미첨부, 또는 relay down, 또는 env 1(mock).\n *\n * SECRET-HANDLING: 이 URL은 relay host + TOTP at= 코드를 담을 수 있다.\n * 대시보드 HTML 본문에 렌더되는 건 의도된 transport(attachUrl과 동일 취급)이지만,\n * stdout/stderr/로그/에러 메시지에는 절대 출력하지 않는다.\n */\n inspectorUrl?: string | null;\n /**\n * 현재 세션 환경 — /attach 스캔 절차·체크리스트 카피 분기 + 상단 환경 라벨 (#468).\n *\n * - `'relay-mobile'` → sandbox family (환경 2: launcher PWA 절차, 토스 앱·_deploymentId 없음)\n * - `'relay-dev'` → intoss family (환경 3: 토스 앱 deep-link 절차)\n * - `'relay-live'` → intoss family + LIVE read-only 한 줄 (환경 4)\n * - `'mock'` / 미지정 → intoss family, 환경 라벨 없음 (환경 1은 /attach 표면이\n * 없어 사실상 도달 불가 — legacy 카피 유지 fallback)\n *\n * 호출처는 자기 mode를 명시적으로 전달한다: debug-server는 active connection에서\n * `deriveEnvironment(...)`로 파생, unplugin tunnel 대시보드는 `'relay-mobile'` 고정.\n */\n mode?: McpEnvironment;\n}\n\n/** mode → 어느 precompiled attach chrome family를 쓰는가 (#468). */\nfunction attachFamilyForMode(mode: McpEnvironment | undefined): AttachChromeFamily {\n return mode === 'relay-mobile' ? 'sandbox' : 'intoss';\n}\n\n/**\n * mode → 페이지 상단 환경 라벨 HTML (`__MODE_LABEL__` 토큰 채움, #468).\n * 사용자가 fidelity 사다리의 어느 겹에 있는지 즉시 알게 하는 환경 가시화 배지.\n * mode 미지정/'mock'은 빈 문자열 — 알 수 없는 환경을 거짓으로 라벨링하지 않는다.\n */\nfunction buildModeLabel(\n mode: McpEnvironment | undefined,\n s: ReturnType<typeof resolveLocaleStrings>,\n): string {\n let label: string;\n switch (mode) {\n case 'relay-mobile':\n label = s('attach.mode.sandbox');\n break;\n case 'relay-dev':\n label = s('attach.mode.intossDev');\n break;\n case 'relay-live':\n label = s('attach.mode.intossLive');\n break;\n case 'mock':\n case undefined:\n return '';\n }\n return `<p class=\"mode-label\">${escapeHtml(label)}</p>`;\n}\n\nexport interface QrHttpServer {\n port: number;\n /** `http://127.0.0.1:<port>/attach?u=<encoded>` URL 생성 헬퍼. */\n buildAttachPageUrl(attachUrl: string): string;\n /**\n * 상태 변경 시 호출 — SSE 구독자에게 최신 상태를 push한다.\n * `getDashboardState`가 주입돼 있지 않으면 no-op.\n */\n notifyStateChange(): void;\n close(): Promise<void>;\n}\n\n/** HTML 특수문자를 이스케이프한다. */\nfunction escapeHtml(s: string): string {\n return s.replace(/[<>&\"']/g, (c) => `&#${c.charCodeAt(0)};`);\n}\n\n/**\n * 현재 path+query에서 lang 파라미터만 교체한 ko/en 토글 링크를 생성한다.\n *\n * SECRET-HANDLING: u= (attachUrl, TOTP at= 캡슐 포함) 등 기존 query를 보존한다.\n * lang= 만 덮어쓴다. 링크 href에 at= 코드가 들어가는 건 의도된 전달 경로.\n */\nfunction buildLangSwitcher(\n path: string,\n existingParams: URLSearchParams,\n locale: 'ko' | 'en',\n s: ReturnType<typeof resolveLocaleStrings>,\n): string {\n function switcherHref(targetLang: 'ko' | 'en'): string {\n const p = new URLSearchParams(existingParams);\n p.set('lang', targetLang);\n return `${escapeHtml(path)}?${p.toString()}`;\n }\n const koLabel = escapeHtml(s('dashboard.lang.ko'));\n const enLabel = escapeHtml(s('dashboard.lang.en'));\n const koClass = locale === 'ko' ? 'active' : '';\n const enClass = locale === 'en' ? 'active' : '';\n return `<div class=\"lang-switcher\"><a href=\"${switcherHref('ko')}\" class=\"${koClass}\">${koLabel}</a><a href=\"${switcherHref('en')}\" class=\"${enClass}\">${enLabel}</a></div>`;\n}\n\n/**\n * Dashboard HTML — precompiled chrome에 per-request 동적 값을 채워 완성한다.\n *\n * 토큰 채우기 순서:\n * 1. chrome string(locale별 precompile)을 가져온다.\n * 2. 동적 부분을 단순 replaceAll로 채운다 (토큰이 HTML context 밖에 있으므로 안전).\n * 3. inline SSE <script>를 </body> 직전에 주입한다.\n *\n * 동적 파트 분류:\n * - \"token-fill\": 단일 값 교체 (__NOW__, __TUNNEL_CLASS__, __TUNNEL_STATUS__,\n * __ATTACH_SECTION__, __INSPECTOR_SECTION__)\n * - \"runtime builder\": 가변 길이 구조 (__PAGES_SECTION__ — 조건부 렌더 + 가변 rows)\n * - \"suffix\": inline SSE <script> (빌드 파이프라인 없는 클라이언트 스크립트, locale\n * aware 문자열 포함)\n *\n * SECRET-HANDLING:\n * - attachUrl은 url-box 안에서만 노출 (TOTP at= 코드 캡슐 그대로).\n * - inspectorUrl은 anchor href 안에서만 노출 (TOTP at= 코드 캡슐 그대로).\n * relay host + TOTP 코드가 담길 수 있으나 대시보드 HTML은 의도된 transport.\n * - tunnel wssUrl은 \"터널 연결됨\" 상태 표시에서 UP/DOWN만 노출.\n * wssUrl 값 자체는 dashboard HTML에 넣지 않는다.\n */\nfunction buildDashboardHtml(\n state: DashboardState,\n qrDataUrl: string | null,\n locale: 'ko' | 'en',\n path = '/',\n params = new URLSearchParams(),\n): string {\n const s = resolveLocaleStrings(locale);\n const now = new Date().toISOString();\n\n const tunnelStatus = state.tunnel.up ? s('dashboard.tunnel.up') : s('dashboard.tunnel.down');\n const tunnelClass = state.tunnel.up ? 'status-up' : 'status-down';\n\n // attachSection: QR img + url-row(url-box + 복사 버튼), or hint.\n // dashboard 표면에서 SSE 재렌더 시에도 동일 구조를 유지해 복사 버튼이 생존한다.\n let attachSection: string;\n if (qrDataUrl && state.attachUrl) {\n const safeAttachUrl = escapeHtml(state.attachUrl);\n const copyLabel = escapeHtml(s('dashboard.url.copy'));\n attachSection =\n `<img class=\"qr\" src=\"${qrDataUrl}\" alt=\"attach QR\" />` +\n `<div class=\"url-row\">` +\n `<p class=\"url-box\" id=\"url-box\">${safeAttachUrl}</p>` +\n `<button class=\"copy-btn\" id=\"copy-btn\" type=\"button\" aria-label=\"${copyLabel}\">${copyLabel}</button>` +\n `</div>`;\n } else {\n attachSection = `<p class=\"hint\">${escapeHtml(s('dashboard.attach.hint'))}</p>`;\n }\n\n // inspectorSection — 인스펙터 열기 링크 (#503).\n // relay up + 페이지 attached(inspectorUrl 있음) 시에만 살아있는 링크를 표시.\n // 미첨부(inspectorUrl null)이면 비활성 안내 힌트를 보여준다.\n // SECRET-HANDLING: inspectorUrl에 relay host + TOTP at= 코드가 담길 수 있으나\n // 대시보드 HTML 본문 렌더는 의도된 transport — 단 stdout/로그로 출력 금지.\n let inspectorSection: string;\n if (state.inspectorUrl) {\n const safeUrl = escapeHtml(state.inspectorUrl);\n const label = escapeHtml(s('dashboard.inspector.open'));\n inspectorSection = `<a class=\"inspector-link\" id=\"inspector-link\" href=\"${safeUrl}\" target=\"_blank\" rel=\"noopener noreferrer\">${label}</a>`;\n } else {\n const hint = escapeHtml(s('dashboard.inspector.waiting'));\n inspectorSection = `<span class=\"inspector-hint\" id=\"inspector-link\">${hint}</span>`;\n }\n\n // pagesSection — \"연결된 Pages\" 섹션: env 3/4(pages: Array)에서만 렌더한다.\n // env 2(pages: null)는 라이브 page 목록을 알 수 없어 섹션 자체를 숨긴다(#411).\n // runtime builder: 조건부 블록 + 가변 row 목록이라 token-fill로는 불충분.\n const pagesSection =\n state.pages === null\n ? ''\n : `<hr /><section id=\"pages-section\"><h2>${escapeHtml(s('dashboard.pages.section'))}</h2><ul id=\"pages-list\">${\n state.pages.length > 0\n ? state.pages\n .map((p) => {\n const safeId = escapeHtml(p.id);\n const safeUrl = escapeHtml(p.url.slice(0, 120));\n return `<li><span class=\"page-id\">${safeId}</span> <span class=\"page-url\">${safeUrl}</span></li>`;\n })\n .join('\\n')\n : `<li class=\"empty\">${escapeHtml(s('dashboard.pages.empty'))}</li>`\n }</ul></section>`;\n\n // locale-aware strings for the inline SSE client script\n const sseStrings: SseScriptStrings = {\n tunnelUp: JSON.stringify(s('dashboard.tunnel.up')),\n tunnelDown: JSON.stringify(s('dashboard.tunnel.down')),\n pagesEmpty: JSON.stringify(s('dashboard.pages.empty')),\n attachHint: JSON.stringify(s('dashboard.attach.hint')),\n copyLabel: JSON.stringify(s('dashboard.url.copy')),\n copiedLabel: JSON.stringify(s('dashboard.url.copied')),\n inspectorOpenLabel: JSON.stringify(s('dashboard.inspector.open')),\n inspectorWaitingLabel: JSON.stringify(s('dashboard.inspector.waiting')),\n dashboardSurface: true,\n };\n\n const langSwitcher = buildLangSwitcher(path, params, locale, s);\n\n // Fill token placeholders in the precompiled chrome.\n // replaceAll is safe because these __TOKEN__ strings cannot appear in\n // any legitimate user-facing value (they are sentinel strings).\n const chrome = dashboardChromeByLocale[locale];\n const filled = chrome\n .replaceAll('__LANG_SWITCHER__', langSwitcher)\n .replaceAll('__NOW__', escapeHtml(now))\n .replaceAll('__TUNNEL_CLASS__', tunnelClass)\n .replaceAll('__TUNNEL_STATUS__', escapeHtml(tunnelStatus))\n .replaceAll('__ATTACH_SECTION__', attachSection)\n .replaceAll('__INSPECTOR_SECTION__', inspectorSection)\n .replaceAll('__PAGES_SECTION__', pagesSection);\n\n // Append the inline SSE <script> suffix directly before </body>.\n // This keeps the client script out of the precompiled chrome (it references\n // locale-aware strings resolved per-request) while staying self-contained.\n const sseScript = buildSseScript(sseStrings);\n return filled.replace('</body>', `${sseScript}\\n</body>`);\n}\n\ninterface SseScriptStrings {\n tunnelUp: string;\n tunnelDown: string;\n pagesEmpty: string;\n attachHint: string;\n /** 복사 버튼 기본 라벨 (JSON.stringify로 이미 escape됨). */\n copyLabel: string;\n /** 복사 완료 피드백 라벨 (JSON.stringify로 이미 escape됨). */\n copiedLabel: string;\n /** \"인스펙터 열기\" 링크 라벨 (JSON.stringify로 이미 escape됨, #503). */\n inspectorOpenLabel: string;\n /** 인스펙터 URL 대기 힌트 (JSON.stringify로 이미 escape됨, #503). */\n inspectorWaitingLabel: string;\n /**\n * true: dashboard 표면 — `#attach-section` innerHTML 전체 교체 방식 유지.\n * url-box 텍스트도 innerHTML 교체로 갱신됨.\n * false: /attach 표면 — img src만 교체, url-box는 `#url-box` textContent만 갱신.\n * 이 분기가 url-box 이중 표시 결함을 방지한다.\n */\n dashboardSurface: boolean;\n}\n\n/**\n * Inline SSE client <script> — injected into the dashboard HTML at runtime.\n *\n * Subscribes to /events and updates the DOM without a build pipeline.\n * client side: attachUrl은 DOM에 렌더링, wssUrl은 절대 렌더링하지 않는다.\n * pages === null 이면 섹션을 건드리지 않는다 (#411).\n *\n * 두 표면(dashboard / attach) 분기:\n * - dashboard (dashboardSurface=true): #attach-section innerHTML 전체 교체 방식 유지.\n * url-box도 innerHTML 재렌더 안에 포함되어 갱신됨.\n * - /attach (dashboardSurface=false): #attach-section의 img src만 교체하고,\n * url-box는 #url-box textContent만 갱신한다. (#attach-section에 url-box가 없으므로\n * innerHTML 교체 시 url-box가 새로 생겨 이중 표시되는 결함을 방지 — #458 결함 수정.)\n *\n * 복사 기능: 이벤트 위임으로 document에 단일 핸들러. innerHTML 재렌더 후에도 생존.\n * - .url-box 클릭 또는 .copy-btn 클릭 → 현재 #url-box textContent 복사.\n * - clipboard: navigator.clipboard.writeText → 실패/부재 시 textarea execCommand fallback.\n * - 피드백: 버튼 라벨이 COPIED_LABEL로 ~1.5초 전환 후 COPY_LABEL로 복귀.\n *\n * 문자열 인자는 빌드타임에 ko/en 테이블에서 가져와 JSON.stringify로 이미 escape됨.\n *\n * SECRET-HANDLING: URL 값을 console.log 등으로 출력하지 않는다.\n */\nfunction buildSseScript(strings: SseScriptStrings): string {\n const isDashboard = strings.dashboardSurface;\n return `<script>\n // SSE — /events 구독해 상태 자동 갱신. 빌드 파이프라인 없는 인라인 스크립트.\n (function () {\n var TUNNEL_UP = ${strings.tunnelUp};\n var TUNNEL_DOWN = ${strings.tunnelDown};\n var PAGES_EMPTY = ${strings.pagesEmpty};\n var ATTACH_HINT = ${strings.attachHint};\n var COPY_LABEL = ${strings.copyLabel};\n var COPIED_LABEL = ${strings.copiedLabel};\n var INSPECTOR_OPEN_LABEL = ${strings.inspectorOpenLabel};\n var INSPECTOR_WAITING_LABEL = ${strings.inspectorWaitingLabel};\n\n // ── 클립보드 복사 헬퍼 ────────────────────────────────────────────────\n function copyText(text) {\n if (navigator.clipboard && navigator.clipboard.writeText) {\n return navigator.clipboard.writeText(text);\n }\n // fallback: textarea + execCommand\n return new Promise(function (resolve, reject) {\n var ta = document.createElement('textarea');\n ta.value = text;\n ta.style.position = 'fixed';\n ta.style.opacity = '0';\n document.body.appendChild(ta);\n ta.focus();\n ta.select();\n try {\n document.execCommand('copy') ? resolve() : reject(new Error('execCommand failed'));\n } catch (err) {\n reject(err);\n } finally {\n document.body.removeChild(ta);\n }\n });\n }\n\n // ── 복사 피드백 ───────────────────────────────────────────────────────\n var copyTimer = null;\n function triggerCopy() {\n var urlBox = document.getElementById('url-box');\n if (!urlBox) return;\n var text = urlBox.textContent || '';\n if (!text) return;\n copyText(text).then(function () {\n var btn = document.getElementById('copy-btn');\n if (btn) {\n btn.textContent = COPIED_LABEL;\n if (copyTimer) clearTimeout(copyTimer);\n copyTimer = setTimeout(function () {\n btn.textContent = COPY_LABEL;\n copyTimer = null;\n }, 1500);\n }\n }).catch(function () { /* 복사 실패 시 조용히 무시 */ });\n }\n\n // ── 이벤트 위임 — document 레벨에서 단일 핸들러 (innerHTML 재렌더 후에도 생존) ──\n document.addEventListener('click', function (e) {\n var target = e.target;\n if (!target) return;\n // .copy-btn 또는 .url-box 클릭 시 복사\n if (target.closest && (target.closest('.copy-btn') || target.closest('.url-box'))) {\n triggerCopy();\n }\n });\n\n // ── SSE 구독 ──────────────────────────────────────────────────────────\n var src = new EventSource('/events');\n src.onmessage = function (e) {\n try {\n var s = JSON.parse(e.data);\n // 터널 상태 갱신\n var el = document.getElementById('tunnel-status');\n if (el) {\n el.textContent = s.tunnel && s.tunnel.up ? TUNNEL_UP : TUNNEL_DOWN;\n el.className = 'status ' + (s.tunnel && s.tunnel.up ? 'status-up' : 'status-down');\n }\n // page 목록 갱신 — pages === null(env 2)이면 섹션 자체를 숨긴 채 둔다.\n // 정적 렌더가 #pages-section을 아예 안 그렸으므로 여기서도 손대지 않아\n // SSE push 때 섹션이 되살아나지 않는다(#411). 배열일 때만 목록을 채운다.\n if (s.pages !== null && s.pages !== undefined) {\n var ul = document.getElementById('pages-list');\n if (ul) {\n if (s.pages.length === 0) {\n ul.innerHTML = '<li class=\"empty\">' + PAGES_EMPTY + '</li>';\n } else {\n ul.innerHTML = s.pages.map(function (p) {\n var sid = String(p.id || '').slice(0, 36).replace(/[<>&\"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });\n var su = String(p.url || '').slice(0, 120).replace(/[<>&\"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });\n return '<li><span class=\"page-id\">' + sid + '</span> <span class=\"page-url\">' + su + '</span></li>';\n }).join('');\n }\n }\n }\n // attachUrl QR + url-box 갱신\n // SECRET-HANDLING: URL 값을 로그로 출력하지 않는다.\n var sec = document.getElementById('attach-section');\n if (sec) {\n if (s.attachUrl) {\n var encoded = encodeURIComponent(s.attachUrl);\n var safeUrl = String(s.attachUrl).slice(0, 2000).replace(/[<>&\"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });\n ${\n isDashboard\n ? `// dashboard: #attach-section innerHTML 전체 교체 (img + url-row).\n // url-box id=\"url-box\" 를 포함해 복사 핸들러가 계속 동작함.\n sec.innerHTML =\n '<img class=\"qr\" src=\"/qr.png?u=' + encoded + '\" alt=\"attach QR\" />' +\n '<div class=\\\\\"url-row\\\\\">' +\n '<p class=\\\\\"url-box\\\\\" id=\\\\\"url-box\\\\\">' + safeUrl + '</p>' +\n '<button class=\\\\\"copy-btn\\\\\" id=\\\\\"copy-btn\\\\\" type=\\\\\"button\\\\\" aria-label=\\\\\"' + COPY_LABEL + '\\\\\">' + COPY_LABEL + '</button>' +\n '</div>';`\n : `// /attach: img src만 교체 — url-box는 별도 #url-section에서 관리해 이중 표시 방지(#458).\n // QR img src 교체: img가 있으면 src만 갱신, 없으면 img 요소 생성.\n var img = sec.querySelector('img.qr');\n if (img) {\n img.src = '/qr.png?u=' + encoded;\n } else {\n sec.innerHTML = '<img class=\\\\\"qr\\\\\" src=\\\\\"/qr.png?u=' + encoded + '\\\\\" alt=\\\\\"attach QR\\\\\" />';\n }\n // url-box textContent만 갱신 (innerHTML 교체하지 않아 복사 버튼/핸들러 생존).\n var ub = document.getElementById('url-box');\n if (ub) ub.textContent = s.attachUrl;`\n }\n } else {\n ${\n isDashboard\n ? `sec.innerHTML = '<p class=\\\\\"hint\\\\\">' + ATTACH_HINT + '</p>';`\n : `// /attach에서 hint가 필요한 경우는 없으나 방어 처리.\n sec.innerHTML = '<p class=\\\\\"hint\\\\\">' + ATTACH_HINT + '</p>';`\n }\n }\n }\n // 인스펙터 링크 갱신 — #inspector-link (#503).\n // SECRET-HANDLING: inspectorUrl을 console.log 등으로 출력하지 않는다.\n var insp = document.getElementById('inspector-link');\n if (insp) {\n if (s.inspectorUrl) {\n var safeInspUrl = String(s.inspectorUrl).slice(0, 2000).replace(/[<>&\"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });\n insp.outerHTML = '<a class=\\\\\"inspector-link\\\\\" id=\\\\\"inspector-link\\\\\" href=\\\\\"' + safeInspUrl + '\\\\\" target=\\\\\"_blank\\\\\" rel=\\\\\"noopener noreferrer\\\\\">' + INSPECTOR_OPEN_LABEL + '</a>';\n } else {\n insp.outerHTML = '<span class=\\\\\"inspector-hint\\\\\" id=\\\\\"inspector-link\\\\\">' + INSPECTOR_WAITING_LABEL + '</span>';\n }\n }\n // 갱신 시각 (dashboard만 #updated 요소 있음)\n var upd = document.getElementById('updated');\n if (upd) upd.textContent = upd.textContent.replace(/[^ ]+$/, new Date().toISOString());\n } catch (_) { /* 파싱 오류 무시 */ }\n };\n src.onerror = function () {\n // 재연결은 EventSource가 자동 처리 (spec 기본 동작).\n };\n })();\n </script>`;\n}\n\n/**\n * Attach 페이지 HTML — precompiled chrome에 per-request 동적 값을 채워 완성한다.\n *\n * 동적 파트:\n * - __QR_DATA_URL__ : base64 data URL (QR 이미지)\n * - __SAFE_LABEL__ : HTML-escaped deploymentId label (intoss family에만 존재)\n * - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)\n * - __MODE_LABEL__ : 환경 배지 (`<p class=\"mode-label\">…</p>` 또는 빈 문자열, #468)\n * - __LIVE_FAQ__ : 환경 4 LIVE read-only `<li>` 또는 빈 문자열 (intoss family에만 존재)\n *\n * mode-aware 분기 (#468): mode가 `relay-mobile`이면 sandbox family chrome(launcher\n * PWA 절차), 그 외는 intoss family chrome(토스 앱 절차)을 선택한다. `relay-live`는\n * intoss chrome에 LIVE read-only 라인을 추가한다.\n *\n * SSE 스크립트도 주입 — `#attach-section` hook이 있으면 `/events` push 때 QR이\n * `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#tunnel-status`·`#pages-list` 등\n * 나머지 selector는 /attach 페이지에 없으므로 null-guard로 no-op.\n *\n * SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.\n */\nfunction buildAttachHtml(\n qrDataUrl: string,\n safeLabel: string,\n safeAttachUrl: string,\n locale: 'ko' | 'en',\n path = '/attach',\n params = new URLSearchParams(),\n mode?: McpEnvironment,\n): string {\n const s = resolveLocaleStrings(locale);\n const langSwitcher = buildLangSwitcher(path, params, locale, s);\n const family = attachFamilyForMode(mode);\n // 환경 4 전용 LIVE read-only 라인 — i18n 문자열은 신뢰된 빌드타임 카피(strong/code\n // 인라인 HTML 포함)라 verbatim 주입한다 (다른 FAQ 항목과 동일한 취급).\n const liveFaq = mode === 'relay-live' ? `<li>${s('attach.intoss.faq.liveReadOnly')}</li>` : '';\n const chrome = attachChromeByLocale[locale][family];\n const filled = chrome\n .replaceAll('__LANG_SWITCHER__', langSwitcher)\n .replaceAll('__MODE_LABEL__', buildModeLabel(mode, s))\n .replaceAll('__LIVE_FAQ__', liveFaq)\n .replaceAll('__QR_DATA_URL__', qrDataUrl)\n .replaceAll('__SAFE_LABEL__', safeLabel)\n .replaceAll('__SAFE_ATTACH_URL__', safeAttachUrl);\n\n // Inject SSE script so QR auto-refreshes on each /events push.\n // `#attach-section` is the only selector present on /attach — other selectors\n // (#tunnel-status, #pages-list, #updated) are null-guarded and are no-ops here.\n const sseStrings: SseScriptStrings = {\n tunnelUp: JSON.stringify(s('dashboard.tunnel.up')),\n tunnelDown: JSON.stringify(s('dashboard.tunnel.down')),\n pagesEmpty: JSON.stringify(s('dashboard.pages.empty')),\n attachHint: JSON.stringify(s('dashboard.attach.hint')),\n copyLabel: JSON.stringify(s('dashboard.url.copy')),\n copiedLabel: JSON.stringify(s('dashboard.url.copied')),\n // /attach 페이지에는 #inspector-link 가 없어 inspector 갱신은 no-op이지만\n // SseScriptStrings 타입 충족을 위해 필드를 제공한다 (#503).\n inspectorOpenLabel: JSON.stringify(s('dashboard.inspector.open')),\n inspectorWaitingLabel: JSON.stringify(s('dashboard.inspector.waiting')),\n // /attach 표면: img src만 교체, #url-box textContent만 갱신 → url-box 이중 표시 방지(#458).\n dashboardSurface: false,\n };\n const sseScript = buildSseScript(sseStrings);\n return filled.replace('</body>', `${sseScript}\\n</body>`);\n}\n\nexport interface QrHttpServerOptions {\n /**\n * SSE 주기 갱신 간격 (ms). 기본값 90_000 (90초).\n *\n * SSE 구독자가 있는 동안 이 간격마다 `notifyStateChange()`와 동일한 push를 수행한다.\n * `getDashboardState()`가 호출 시점에 `at=` TOTP 코드를 재발급하므로, push 자체가\n * 열린 탭의 인스펙터 링크를 신선하게 유지한다. 90s 주기 < relay gate 허용창 ~3분\n * (±6 TOTP steps)이므로 탭이 열려 있는 한 링크가 항상 유효하다 (issue #509).\n *\n * 테스트에서 짧은 값(예: 50ms)을 주입해 검증한다. `undefined`이면 기본값 90_000.\n */\n sseRefreshIntervalMs?: number;\n}\n\n/**\n * 로컬 HTTP 서버를 127.0.0.1 random port(또는 `AIT_DEBUG_HTTP_PORT` env)로 시작한다.\n * MCP debug server 생애주기에 묶어 사용 — `runDebugServer` shutdown 시 `close()`로 정리.\n *\n * @param getDashboardState - dashboard 상태를 반환하는 클로저. 주입 시 `GET /` dashboard와\n * `GET /events` SSE 스트림이 활성화된다. 미주입 시 두 라우트는 204/서비스 없음으로 응답.\n * @param options - 서버 옵션. `sseRefreshIntervalMs`로 idle 탭 TOTP 만료 방지 주기를 조정.\n */\nexport async function startQrHttpServer(\n getDashboardState?: () => DashboardState,\n options?: QrHttpServerOptions,\n): Promise<QrHttpServer> {\n const { default: QRCode } = await import('qrcode');\n\n /** SSE 활성 연결 목록 — `notifyStateChange()` 시 전체 push. */\n const sseClients: ServerResponse[] = [];\n\n /** SSE 연결 하나에 상태 이벤트를 flush한다. */\n function pushStateToClient(res: ServerResponse, state: DashboardState): void {\n const payload = JSON.stringify({\n tunnel: { up: state.tunnel.up, wssUrl: state.tunnel.wssUrl },\n pages: state.pages,\n // attachUrl은 캡슐 그대로 전달 — TOTP at= 코드 분리 없음 (의도된 설계).\n attachUrl: state.attachUrl,\n // inspectorUrl: relay + 페이지 attached 시 살아있는 인스펙터 URL (#503).\n // SECRET-HANDLING: URL(relay host + TOTP at=)은 SSE payload 전달이 의도된 transport.\n // 단 stdout/로그/에러에는 절대 출력하지 않는다.\n inspectorUrl: state.inspectorUrl ?? null,\n });\n // SSE frame: \"data: <json>\\n\\n\"\n res.write(`data: ${payload}\\n\\n`);\n }\n\n const server: Server = createServer(async (req: IncomingMessage, res: ServerResponse) => {\n const rawUrl = req.url ?? '/';\n const [path, query = ''] = rawUrl.split('?', 2) as [string, string | undefined];\n const params = new URLSearchParams(query ?? '');\n\n // per-request locale — ?lang= query param이 있으면 우선 적용, 없으면 Accept-Language header에서 결정.\n const langParam = params.get('lang');\n const locale =\n langParam === 'ko' || langParam === 'en'\n ? langParam\n : parseAcceptLanguage(req.headers['accept-language']);\n\n // ── GET / — dashboard 루트 ─────────────────────────────────────────────\n if (path === '/') {\n if (!getDashboardState) {\n res.writeHead(204, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end();\n return;\n }\n const state = getDashboardState();\n let qrDataUrl: string | null = null;\n if (state.attachUrl) {\n try {\n qrDataUrl = await QRCode.toDataURL(state.attachUrl, {\n type: 'image/png',\n errorCorrectionLevel: 'M',\n });\n } catch {\n // QR 생성 실패 시 null 유지 — dashboard는 텍스트 fallback 표시\n }\n }\n const html = buildDashboardHtml(state, qrDataUrl, locale, path, params);\n res.writeHead(200, {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Cache-Control': 'no-store',\n });\n res.end(html);\n return;\n }\n\n // ── GET /events — SSE 스트림 ──────────────────────────────────────────\n if (path === '/events') {\n if (!getDashboardState) {\n res.writeHead(204, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end();\n return;\n }\n res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n });\n // 즉시 현재 상태를 한 번 push — 페이지 로드 시 최신 상태 보장.\n const initialState = getDashboardState();\n pushStateToClient(res, initialState);\n\n sseClients.push(res);\n\n // 연결 끊기면 목록에서 제거.\n req.once('close', () => {\n const idx = sseClients.indexOf(res);\n if (idx !== -1) sseClients.splice(idx, 1);\n });\n return;\n }\n\n if (path === '/attach') {\n const encodedU = params.get('u') ?? '';\n let attachUrl: string;\n try {\n attachUrl = decodeURIComponent(encodedU);\n } catch {\n res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('잘못된 u 파라미터입니다.');\n return;\n }\n\n // deploymentId 라벨 — attachUrl에서 _deploymentId 파라미터만 추출 (at= 노출 방지).\n let deploymentIdLabel = 'attach';\n try {\n const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);\n if (dpMatch?.[1]) {\n deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);\n }\n } catch {\n // best-effort\n }\n\n // 현재 세션 mode — 카피 분기(#468). getDashboardState 미주입(legacy) 시 undefined\n // → intoss family + 환경 라벨 없음 fallback.\n const mode = getDashboardState?.().mode;\n\n // QR을 base64 data URL로 인라인 생성 — 외부 fetch 없이 self-contained HTML.\n QRCode.toDataURL(attachUrl, { type: 'image/png', errorCorrectionLevel: 'M' })\n .then((dataUrl: string) => {\n const safeLabel = escapeHtml(deploymentIdLabel);\n const safeAttachUrl = escapeHtml(attachUrl);\n const html = buildAttachHtml(\n dataUrl,\n safeLabel,\n safeAttachUrl,\n locale,\n path,\n params,\n mode,\n );\n res.writeHead(200, {\n 'Content-Type': 'text/html; charset=utf-8',\n 'Cache-Control': 'no-store',\n });\n res.end(html);\n })\n .catch(() => {\n res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('QR 생성에 실패했습니다.');\n });\n return;\n }\n\n if (path === '/qr.png') {\n const encodedU = params.get('u') ?? '';\n let attachUrl: string;\n try {\n attachUrl = decodeURIComponent(encodedU);\n } catch {\n res.writeHead(400, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('잘못된 u 파라미터입니다.');\n return;\n }\n\n QRCode.toBuffer(attachUrl, { type: 'png', errorCorrectionLevel: 'M' })\n .then((buf: Buffer) => {\n res.writeHead(200, {\n 'Content-Type': 'image/png',\n 'Cache-Control': 'no-store',\n 'Content-Length': String(buf.length),\n });\n res.end(buf);\n })\n .catch(() => {\n res.writeHead(500, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('QR PNG 생성에 실패했습니다.');\n });\n return;\n }\n\n res.writeHead(404, { 'Content-Type': 'text/plain; charset=utf-8' });\n res.end('Not Found');\n });\n\n const listenPort = Number(process.env.AIT_DEBUG_HTTP_PORT ?? 0);\n\n await new Promise<void>((resolve, reject) => {\n server.listen(listenPort, '127.0.0.1', () => resolve());\n server.once('error', reject);\n });\n\n const address = server.address();\n if (!address || typeof address === 'string') {\n throw new Error('qr-http-server: server.address()가 예상하지 못한 형태입니다.');\n }\n const port = address.port;\n\n /** idle 탭 TOTP 만료 방지용 주기 SSE 갱신 interval. */\n function notifyStateChangeInternal(): void {\n if (!getDashboardState) return;\n const state = getDashboardState();\n for (const client of sseClients) {\n try {\n pushStateToClient(client, state);\n } catch {\n // 연결이 이미 끊어진 경우 — 무시 (close 핸들러가 목록에서 제거함).\n }\n }\n }\n\n // 주기 SSE 갱신 — getDashboardState() 호출 시점에 TOTP at=가 재발급되므로\n // push 자체가 열린 탭의 인스펙터 링크를 신선하게 유지한다 (issue #509).\n // .unref()로 프로세스 종료를 막지 않는다.\n const refreshIntervalMs = options?.sseRefreshIntervalMs ?? 90_000;\n const refreshHandle = setInterval(() => {\n if (sseClients.length > 0 && getDashboardState) {\n notifyStateChangeInternal();\n }\n }, refreshIntervalMs).unref();\n\n return {\n port,\n buildAttachPageUrl(attachUrl: string): string {\n return `http://127.0.0.1:${port}/attach?u=${encodeURIComponent(attachUrl)}`;\n },\n notifyStateChange(): void {\n notifyStateChangeInternal();\n },\n close(): Promise<void> {\n clearInterval(refreshHandle);\n return new Promise((resolve, reject) => {\n server.close((err) => (err ? reject(err) : resolve()));\n });\n },\n };\n}\n"],"mappings":"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"}
|