@ait-co/devtools 0.1.67 → 0.1.69

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (47) hide show
  1. package/dist/chii-relay-BNd3G3UG.js +152 -0
  2. package/dist/chii-relay-BNd3G3UG.js.map +1 -0
  3. package/dist/chii-relay-DngjQ2_A.cjs +151 -0
  4. package/dist/chii-relay-DngjQ2_A.cjs.map +1 -0
  5. package/dist/in-app/index.d.ts +24 -6
  6. package/dist/in-app/index.d.ts.map +1 -1
  7. package/dist/in-app/index.js +27 -8
  8. package/dist/in-app/index.js.map +1 -1
  9. package/dist/mcp/cli.js +332 -53
  10. package/dist/mcp/cli.js.map +1 -1
  11. package/dist/mcp/server.js +2 -2
  12. package/dist/mcp/server.js.map +1 -1
  13. package/dist/panel/index.js +52 -20
  14. package/dist/panel/index.js.map +1 -1
  15. package/dist/{qr-http-server-ChC7P6-H.js → qr-http-server-CyVQphTM.js} +213 -30
  16. package/dist/qr-http-server-CyVQphTM.js.map +1 -0
  17. package/dist/{qr-http-server-BUfbLGm1.js → qr-http-server-DKEca8J3.js} +213 -30
  18. package/dist/qr-http-server-DKEca8J3.js.map +1 -0
  19. package/dist/{qr-http-server-B5YndXcS.cjs → qr-http-server-DR__VNnX.cjs} +213 -30
  20. package/dist/qr-http-server-DR__VNnX.cjs.map +1 -0
  21. package/dist/{qr-http-server-DRlwR54D.cjs → qr-http-server-DnQSQ3hC.cjs} +213 -30
  22. package/dist/qr-http-server-DnQSQ3hC.cjs.map +1 -0
  23. package/dist/{tunnel-CrlCX5sZ.cjs → tunnel-BMY7KgO5.cjs} +4 -3
  24. package/dist/{tunnel-CrlCX5sZ.cjs.map → tunnel-BMY7KgO5.cjs.map} +1 -1
  25. package/dist/{tunnel-BNzbSCfB.js → tunnel-DIN5Vvbo.js} +4 -3
  26. package/dist/{tunnel-BNzbSCfB.js.map → tunnel-DIN5Vvbo.js.map} +1 -1
  27. package/dist/unplugin/index.cjs +10 -3
  28. package/dist/unplugin/index.cjs.map +1 -1
  29. package/dist/unplugin/index.d.cts.map +1 -1
  30. package/dist/unplugin/index.d.ts.map +1 -1
  31. package/dist/unplugin/index.js +10 -3
  32. package/dist/unplugin/index.js.map +1 -1
  33. package/dist/unplugin/tunnel.cjs +3 -2
  34. package/dist/unplugin/tunnel.cjs.map +1 -1
  35. package/dist/unplugin/tunnel.d.cts.map +1 -1
  36. package/dist/unplugin/tunnel.d.ts.map +1 -1
  37. package/dist/unplugin/tunnel.js +3 -2
  38. package/dist/unplugin/tunnel.js.map +1 -1
  39. package/package.json +1 -1
  40. package/dist/chii-relay-57BfqF_5.cjs +0 -88
  41. package/dist/chii-relay-57BfqF_5.cjs.map +0 -1
  42. package/dist/chii-relay-itXOz7kS.js +0 -89
  43. package/dist/chii-relay-itXOz7kS.js.map +0 -1
  44. package/dist/qr-http-server-B5YndXcS.cjs.map +0 -1
  45. package/dist/qr-http-server-BUfbLGm1.js.map +0 -1
  46. package/dist/qr-http-server-ChC7P6-H.js.map +0 -1
  47. package/dist/qr-http-server-DRlwR54D.cjs.map +0 -1
package/dist/mcp/cli.js CHANGED
@@ -721,11 +721,26 @@ var ChiiCdpConnection = class {
721
721
  * entries.
722
722
  *
723
723
  * TOTP auth (relay-side, authoritative gate):
724
- * When `verifyAuth` is provided, this module registers an HTTP upgrade
725
- * listener on the server BEFORE calling `chii.start({server})`. Node's
726
- * `http.Server` allows multiple 'upgrade' listeners; the first to call
727
- * `socket.destroy()` wins. Invalid auth → 401 + destroy (chii never sees
728
- * the connection). Valid auth → return without side-effect (chii handles it).
724
+ * When `verifyAuth` is provided, this module registers HTTP 'upgrade' and
725
+ * 'request' listeners on the server BEFORE calling `chii.start({server})`.
726
+ * Node's `http.Server` calls listeners in registration order; the first to
727
+ * call `socket.destroy()` (upgrade) or `res.end()` (request) wins. Invalid
728
+ * auth → 401 + destroy (chii never sees the connection). Valid auth →
729
+ * return without side-effect (chii handles it).
730
+ *
731
+ * TOTP code transports (issue #466) — two equivalent ways to carry the code:
732
+ * 1. Query param `at=<code>` — used by the daemon-side `/client` connection
733
+ * (`chii-connection.ts` appends it; it holds the secret).
734
+ * 2. Path prefix `/at/<code>/…` — used by the phone-side target. Chii's
735
+ * stock `target.js` derives its WS endpoint from the script `src`
736
+ * (`scriptEl.src.replace('target.js','')`), so the only way for the
737
+ * phone to carry a code is to embed it in the script URL path. The
738
+ * in-app attach injects `https://<host>/at/<code>/target.js`; both the
739
+ * script fetch and the derived `wss://<host>/at/<code>/target/<id>` WS
740
+ * dial then carry the prefix. The listeners below rewrite the prefix
741
+ * into the query form (`rewriteAtPathPrefix`) and MUTATE `req.url`
742
+ * before chii's own handlers (registered later) parse it — chii only
743
+ * ever sees the stripped URL.
729
744
  *
730
745
  * Threat model: "URL leak" — someone obtains the tunnel URL (Slack paste, QR
731
746
  * screenshot, shoulder-surfing) but does not have the shared TOTP secret.
@@ -744,6 +759,33 @@ function loadChiiServer() {
744
759
  throw new Error("chii server module did not expose start()");
745
760
  }
746
761
  /**
762
+ * Rewrites a `/at/<code>/…` path-prefixed request URL into the equivalent
763
+ * query-based form, e.g.:
764
+ *
765
+ * `/at/123456/target.js` → `/target.js?at=123456`
766
+ * `/at/123456/target/x?url=u` → `/target/x?url=u&at=123456`
767
+ * `/at/123456/` → `/?at=123456`
768
+ *
769
+ * Returns `null` when the URL does not carry the prefix (including an empty
770
+ * code segment) — callers fall back to the unmodified URL and the existing
771
+ * query-based auth path.
772
+ *
773
+ * Pure string surgery — this function knows nothing about secrets or code
774
+ * validity; verification stays inside the caller-provided `verifyAuth`
775
+ * predicate (which parses the query). The raw path segment is appended
776
+ * verbatim to the query: both path segments and query values are
777
+ * percent-decoded exactly once by their consumers, so no re-encoding is
778
+ * needed (TOTP codes are 6 digits and never percent-encoded in practice).
779
+ */
780
+ function rewriteAtPathPrefix(rawUrl) {
781
+ const match = /^\/at\/([^/?]+)(\/[^?]*)?(\?.*)?$/.exec(rawUrl);
782
+ if (match === null) return null;
783
+ const code = match[1];
784
+ const path = match[2] === void 0 || match[2] === "" ? "/" : match[2];
785
+ const query = match[3] ?? "";
786
+ return `${path}${query}${query === "" ? "?" : "&"}at=${code}`;
787
+ }
788
+ /**
747
789
  * Starts the Chii relay and resolves once listening.
748
790
  *
749
791
  * Default port is 0 (OS-assigned). With port 0 the OS picks a free ephemeral
@@ -762,15 +804,36 @@ function loadChiiServer() {
762
804
  async function startChiiRelay(options = {}) {
763
805
  const requestedPort = options.port ?? 0;
764
806
  const host = options.host ?? "127.0.0.1";
765
- const { verifyAuth } = options;
807
+ const { verifyAuth, onAuthReject } = options;
766
808
  const httpServer = createServer();
767
- if (verifyAuth) httpServer.on("upgrade", (req, socket) => {
768
- if (!verifyAuth(req)) {
769
- socket.write("HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n");
770
- socket.destroy();
771
- return;
772
- }
773
- });
809
+ const notifyAuthReject = (kind) => {
810
+ if (onAuthReject === void 0) return;
811
+ try {
812
+ onAuthReject({ kind });
813
+ } catch {}
814
+ };
815
+ if (verifyAuth) {
816
+ httpServer.on("upgrade", (req, socket) => {
817
+ const rewritten = rewriteAtPathPrefix(req.url ?? "");
818
+ if (rewritten !== null) req.url = rewritten;
819
+ if (!verifyAuth(req)) {
820
+ socket.write("HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n");
821
+ socket.destroy();
822
+ notifyAuthReject("ws-upgrade");
823
+ return;
824
+ }
825
+ });
826
+ httpServer.on("request", (req, res) => {
827
+ const rewritten = rewriteAtPathPrefix(req.url ?? "");
828
+ if (rewritten === null) return;
829
+ req.url = rewritten;
830
+ if (!verifyAuth(req)) {
831
+ res.statusCode = 401;
832
+ res.end();
833
+ notifyAuthReject("http-request");
834
+ }
835
+ });
836
+ }
774
837
  await loadChiiServer().start({
775
838
  server: httpServer,
776
839
  domain: `${host}:${requestedPort}`,
@@ -1837,16 +1900,27 @@ const en = {
1837
1900
  "attach.title": "AIT Debug Session — QR Scan",
1838
1901
  "attach.deployment": "deployment: {label}",
1839
1902
  "attach.steps.section": "How to scan",
1840
- "attach.step1": "Open the Toss app.",
1841
- "attach.step2": "Scan the QR code with your phone camera app.",
1842
- "attach.step3": "Tap <strong>\"Open in Toss\"</strong> when the popup appears.",
1843
- "attach.step4": "The mini-app opens and the debug session attaches automatically.",
1844
1903
  "attach.faq.section": "Troubleshooting checklist",
1845
- "attach.faq.appNotOpen": "<strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)",
1846
- "attach.faq.prepare": "<strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter",
1847
- "attach.faq.chii": "<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import",
1848
- "attach.faq.totp": "<strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server",
1849
1904
  "attach.url.section": "URL (fallback)",
1905
+ "attach.mode.sandbox": "Env 2 — AITC Sandbox PWA",
1906
+ "attach.mode.intossDev": "Env 3 — intoss-private relay dev",
1907
+ "attach.mode.intossLive": "Env 4 — intoss live relay debug",
1908
+ "attach.sandbox.step1": "Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).",
1909
+ "attach.sandbox.step2": "Scan this QR code with <strong>\"Scan QR with camera\"</strong> inside the launcher.",
1910
+ "attach.sandbox.step3": "The mini-app opens fullscreen and the debug session attaches automatically.",
1911
+ "attach.sandbox.faq.notInstalled": "<strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen",
1912
+ "attach.sandbox.faq.cameraApp": "<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",
1913
+ "attach.sandbox.faq.totp": "<strong>QR expired (TOTP 30 s)</strong> — scan a fresh QR code",
1914
+ "attach.sandbox.faq.chii": "<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import",
1915
+ "attach.intoss.step1": "Open the Toss app.",
1916
+ "attach.intoss.step2": "Scan the QR code with your phone camera app.",
1917
+ "attach.intoss.step3": "Tap <strong>\"Open in Toss\"</strong> when the popup appears.",
1918
+ "attach.intoss.step4": "The mini-app opens and the debug session attaches automatically.",
1919
+ "attach.intoss.faq.appNotOpen": "<strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)",
1920
+ "attach.intoss.faq.prepare": "<strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter",
1921
+ "attach.intoss.faq.chii": "<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import",
1922
+ "attach.intoss.faq.totp": "<strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server",
1923
+ "attach.intoss.faq.liveReadOnly": "<strong>LIVE session is read-only</strong> — <code>call_sdk</code>/<code>evaluate</code> require an explicit <code>confirm</code>",
1850
1924
  "launcher.title": "AITC DevTools Launcher",
1851
1925
  "launcher.description": "Scan the terminal QR code or paste the tunnel URL.",
1852
1926
  "launcher.installCta": "Install launcher to your phone",
@@ -1860,7 +1934,12 @@ const en = {
1860
1934
  "launcher.invalidUrl": "Enter a valid http(s):// URL.",
1861
1935
  "launcher.debugAuthFailed": "Debug connection authentication failed",
1862
1936
  "launcher.debugAuthFailedHint": "The QR code may have expired. Scan a fresh QR code.",
1863
- "launcher.debugAuthRescanCta": "Scan a new QR"
1937
+ "launcher.debugAuthRescanCta": "Scan a new QR",
1938
+ "launcher.diagFab": "Diag",
1939
+ "launcher.diagTitle": "Viewport diagnostics",
1940
+ "launcher.diagYes": "yes",
1941
+ "launcher.diagNo": "no",
1942
+ "launcher.letterboxDetected": "Display area is {pt}pt short — likely an iOS standalone letterbox. Removing and re-adding the launcher to the home screen may fix it."
1864
1943
  };
1865
1944
  //#endregion
1866
1945
  //#region src/i18n/index.ts
@@ -2064,16 +2143,27 @@ const tables = {
2064
2143
  "attach.title": "AIT 디버그 세션 — QR 스캔",
2065
2144
  "attach.deployment": "deployment: {label}",
2066
2145
  "attach.steps.section": "스캔 절차",
2067
- "attach.step1": "토스 앱을 실행하세요.",
2068
- "attach.step2": "폰 카메라 앱으로 QR 코드를 스캔하세요.",
2069
- "attach.step3": "팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.",
2070
- "attach.step4": "미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.",
2071
2146
  "attach.faq.section": "진단 체크리스트",
2072
- "attach.faq.appNotOpen": "<strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)",
2073
- "attach.faq.prepare": "<strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인",
2074
- "attach.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
2075
- "attach.faq.totp": "<strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인",
2076
2147
  "attach.url.section": "URL (fallback)",
2148
+ "attach.mode.sandbox": "환경 2 — AITC Sandbox PWA",
2149
+ "attach.mode.intossDev": "환경 3 — intoss-private relay dev",
2150
+ "attach.mode.intossLive": "환경 4 — intoss live relay debug",
2151
+ "attach.sandbox.step1": "홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).",
2152
+ "attach.sandbox.step2": "launcher 안의 <strong>\"QR 카메라로 스캔\"</strong>으로 이 QR 코드를 스캔하세요.",
2153
+ "attach.sandbox.step3": "미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.",
2154
+ "attach.sandbox.faq.notInstalled": "<strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요",
2155
+ "attach.sandbox.faq.cameraApp": "<strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요",
2156
+ "attach.sandbox.faq.totp": "<strong>QR이 만료된 경우 (TOTP 30초)</strong> — 새 QR을 다시 스캔하세요",
2157
+ "attach.sandbox.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
2158
+ "attach.intoss.step1": "토스 앱을 실행하세요.",
2159
+ "attach.intoss.step2": "폰 카메라 앱으로 QR 코드를 스캔하세요.",
2160
+ "attach.intoss.step3": "팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.",
2161
+ "attach.intoss.step4": "미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.",
2162
+ "attach.intoss.faq.appNotOpen": "<strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)",
2163
+ "attach.intoss.faq.prepare": "<strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인",
2164
+ "attach.intoss.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
2165
+ "attach.intoss.faq.totp": "<strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인",
2166
+ "attach.intoss.faq.liveReadOnly": "<strong>LIVE 세션은 read-only입니다</strong> — <code>call_sdk</code>/<code>evaluate</code> 실행에는 명시적 <code>confirm</code>이 필요합니다",
2077
2167
  "launcher.title": "AITC DevTools Launcher",
2078
2168
  "launcher.description": "터미널 QR을 스캔하거나 URL을 입력하세요.",
2079
2169
  "launcher.installCta": "폰에 런처 설치하기",
@@ -2087,7 +2177,12 @@ const tables = {
2087
2177
  "launcher.invalidUrl": "올바른 http(s):// URL을 입력하세요.",
2088
2178
  "launcher.debugAuthFailed": "디버그 연결 인증 실패",
2089
2179
  "launcher.debugAuthFailedHint": "QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.",
2090
- "launcher.debugAuthRescanCta": "새 QR 스캔하기"
2180
+ "launcher.debugAuthRescanCta": "새 QR 스캔하기",
2181
+ "launcher.diagFab": "진단",
2182
+ "launcher.diagTitle": "뷰포트 진단",
2183
+ "launcher.diagYes": "예",
2184
+ "launcher.diagNo": "아니요",
2185
+ "launcher.letterboxDetected": "표시 영역이 {pt}pt 부족합니다 — iOS standalone letterbox로 보입니다. 런처를 홈 화면에서 제거 후 다시 설치하면 해소될 수 있어요."
2091
2186
  },
2092
2187
  en
2093
2188
  };
@@ -2185,7 +2280,57 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
2185
2280
  .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2186
2281
  .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2187
2282
  </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>__PAGES_SECTION__</body></html>`;
2188
- const attachChromeHtmlKo = `<!DOCTYPE html>
2283
+ const attachChromeHtmlKoSandbox = `<!DOCTYPE html>
2284
+ <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>
2285
+ *, *::before, *::after { box-sizing: border-box; }
2286
+ body {
2287
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2288
+ background: #0d1117; color: #c9d1d9;
2289
+ display: flex; flex-direction: column; align-items: center;
2290
+ min-height: 100vh; margin: 0; padding: 2rem 1rem;
2291
+ gap: 1.5rem;
2292
+ }
2293
+ h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
2294
+ .mode-label {
2295
+ font-size: 0.78rem; font-weight: 600; color: #79c0ff;
2296
+ background: #161b22; border: 1px solid #30363d; border-radius: 999px;
2297
+ padding: 0.25rem 0.75rem; margin: 0;
2298
+ }
2299
+ .label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
2300
+ img.qr {
2301
+ width: min(90vw, 360px); height: auto;
2302
+ image-rendering: pixelated;
2303
+ background: #fff; padding: 1rem; border-radius: 12px;
2304
+ display: block; margin: 0 auto;
2305
+ }
2306
+ section { width: 100%; max-width: 480px; }
2307
+ h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
2308
+ ol, ul { margin: 0; padding-left: 1.25rem; }
2309
+ li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
2310
+ .url-row {
2311
+ display: flex; align-items: stretch; gap: 0;
2312
+ border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
2313
+ }
2314
+ .url-box {
2315
+ font-family: monospace; font-size: 0.72rem;
2316
+ word-break: break-all; opacity: 0.4;
2317
+ background: #161b22; padding: 0.75rem 1rem;
2318
+ flex: 1; cursor: pointer; border: none; border-radius: 0;
2319
+ }
2320
+ .url-box:hover { opacity: 0.6; }
2321
+ .copy-btn {
2322
+ flex-shrink: 0; padding: 0.5rem 0.8rem;
2323
+ background: #21262d; border: none; border-left: 1px solid #30363d;
2324
+ color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;
2325
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2326
+ }
2327
+ .copy-btn:hover { background: #30363d; }
2328
+ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
2329
+ .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2330
+ .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2331
+ .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2332
+ </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 30초)</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>`;
2333
+ const attachChromeHtmlKoIntoss = `<!DOCTYPE html>
2189
2334
  <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>
2190
2335
  *, *::before, *::after { box-sizing: border-box; }
2191
2336
  body {
@@ -2196,6 +2341,11 @@ body {
2196
2341
  gap: 1.5rem;
2197
2342
  }
2198
2343
  h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
2344
+ .mode-label {
2345
+ font-size: 0.78rem; font-weight: 600; color: #79c0ff;
2346
+ background: #161b22; border: 1px solid #30363d; border-radius: 999px;
2347
+ padding: 0.25rem 0.75rem; margin: 0;
2348
+ }
2199
2349
  .label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
2200
2350
  img.qr {
2201
2351
  width: min(90vw, 360px); height: auto;
@@ -2229,7 +2379,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
2229
2379
  .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2230
2380
  .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2231
2381
  .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2232
- </style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__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></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>`;
2382
+ </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>`;
2233
2383
  const dashboardChromeHtmlEn = `<!DOCTYPE html>
2234
2384
  <html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>AIT Debug Dashboard</title><style>
2235
2385
  *, *::before, *::after { box-sizing: border-box; }
@@ -2282,7 +2432,57 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
2282
2432
  .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2283
2433
  .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2284
2434
  </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>__PAGES_SECTION__</body></html>`;
2285
- const attachChromeHtmlEn = `<!DOCTYPE html>
2435
+ const attachChromeHtmlEnSandbox = `<!DOCTYPE html>
2436
+ <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>
2437
+ *, *::before, *::after { box-sizing: border-box; }
2438
+ body {
2439
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2440
+ background: #0d1117; color: #c9d1d9;
2441
+ display: flex; flex-direction: column; align-items: center;
2442
+ min-height: 100vh; margin: 0; padding: 2rem 1rem;
2443
+ gap: 1.5rem;
2444
+ }
2445
+ h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
2446
+ .mode-label {
2447
+ font-size: 0.78rem; font-weight: 600; color: #79c0ff;
2448
+ background: #161b22; border: 1px solid #30363d; border-radius: 999px;
2449
+ padding: 0.25rem 0.75rem; margin: 0;
2450
+ }
2451
+ .label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
2452
+ img.qr {
2453
+ width: min(90vw, 360px); height: auto;
2454
+ image-rendering: pixelated;
2455
+ background: #fff; padding: 1rem; border-radius: 12px;
2456
+ display: block; margin: 0 auto;
2457
+ }
2458
+ section { width: 100%; max-width: 480px; }
2459
+ h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
2460
+ ol, ul { margin: 0; padding-left: 1.25rem; }
2461
+ li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
2462
+ .url-row {
2463
+ display: flex; align-items: stretch; gap: 0;
2464
+ border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
2465
+ }
2466
+ .url-box {
2467
+ font-family: monospace; font-size: 0.72rem;
2468
+ word-break: break-all; opacity: 0.4;
2469
+ background: #161b22; padding: 0.75rem 1rem;
2470
+ flex: 1; cursor: pointer; border: none; border-radius: 0;
2471
+ }
2472
+ .url-box:hover { opacity: 0.6; }
2473
+ .copy-btn {
2474
+ flex-shrink: 0; padding: 0.5rem 0.8rem;
2475
+ background: #21262d; border: none; border-left: 1px solid #30363d;
2476
+ color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;
2477
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
2478
+ }
2479
+ .copy-btn:hover { background: #30363d; }
2480
+ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
2481
+ .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2482
+ .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2483
+ .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2484
+ </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 s)</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>`;
2485
+ const attachChromeHtmlEnIntoss = `<!DOCTYPE html>
2286
2486
  <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>
2287
2487
  *, *::before, *::after { box-sizing: border-box; }
2288
2488
  body {
@@ -2293,6 +2493,11 @@ body {
2293
2493
  gap: 1.5rem;
2294
2494
  }
2295
2495
  h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
2496
+ .mode-label {
2497
+ font-size: 0.78rem; font-weight: 600; color: #79c0ff;
2498
+ background: #161b22; border: 1px solid #30363d; border-radius: 999px;
2499
+ padding: 0.25rem 0.75rem; margin: 0;
2500
+ }
2296
2501
  .label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
2297
2502
  img.qr {
2298
2503
  width: min(90vw, 360px); height: auto;
@@ -2326,19 +2531,51 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
2326
2531
  .lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
2327
2532
  .lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
2328
2533
  .lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
2329
- </style></head><body><h1>AIT Debug Session — QR Scan</h1>__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></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>`;
2534
+ </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>`;
2330
2535
  /** Map from Locale to the precompiled dashboard chrome string. */
2331
2536
  const dashboardChromeByLocale = {
2332
2537
  ko: dashboardChromeHtmlKo,
2333
2538
  en: dashboardChromeHtmlEn
2334
2539
  };
2335
- /** Map from Locale to the precompiled attach page chrome string. */
2540
+ /** Map from Locale × copy family to the precompiled attach page chrome string (#468). */
2336
2541
  const attachChromeByLocale = {
2337
- ko: attachChromeHtmlKo,
2338
- en: attachChromeHtmlEn
2542
+ ko: {
2543
+ sandbox: attachChromeHtmlKoSandbox,
2544
+ intoss: attachChromeHtmlKoIntoss
2545
+ },
2546
+ en: {
2547
+ sandbox: attachChromeHtmlEnSandbox,
2548
+ intoss: attachChromeHtmlEnIntoss
2549
+ }
2339
2550
  };
2340
2551
  //#endregion
2341
2552
  //#region src/mcp/qr-http-server.ts
2553
+ /** mode → 어느 precompiled attach chrome family를 쓰는가 (#468). */
2554
+ function attachFamilyForMode(mode) {
2555
+ return mode === "relay-mobile" ? "sandbox" : "intoss";
2556
+ }
2557
+ /**
2558
+ * mode → 페이지 상단 환경 라벨 HTML (`__MODE_LABEL__` 토큰 채움, #468).
2559
+ * 사용자가 fidelity 사다리의 어느 겹에 있는지 즉시 알게 하는 환경 가시화 배지.
2560
+ * mode 미지정/'mock'은 빈 문자열 — 알 수 없는 환경을 거짓으로 라벨링하지 않는다.
2561
+ */
2562
+ function buildModeLabel(mode, s) {
2563
+ let label;
2564
+ switch (mode) {
2565
+ case "relay-mobile":
2566
+ label = s("attach.mode.sandbox");
2567
+ break;
2568
+ case "relay-dev":
2569
+ label = s("attach.mode.intossDev");
2570
+ break;
2571
+ case "relay-live":
2572
+ label = s("attach.mode.intossLive");
2573
+ break;
2574
+ case "mock":
2575
+ case void 0: return "";
2576
+ }
2577
+ return `<p class="mode-label">${escapeHtml(label)}</p>`;
2578
+ }
2342
2579
  /** HTML 특수문자를 이스케이프한다. */
2343
2580
  function escapeHtml(s) {
2344
2581
  return s.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
@@ -2572,8 +2809,14 @@ function buildSseScript(strings) {
2572
2809
  *
2573
2810
  * 동적 파트:
2574
2811
  * - __QR_DATA_URL__ : base64 data URL (QR 이미지)
2575
- * - __SAFE_LABEL__ : HTML-escaped deploymentId label
2812
+ * - __SAFE_LABEL__ : HTML-escaped deploymentId label (intoss family에만 존재)
2576
2813
  * - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)
2814
+ * - __MODE_LABEL__ : 환경 배지 (`<p class="mode-label">…</p>` 또는 빈 문자열, #468)
2815
+ * - __LIVE_FAQ__ : 환경 4 LIVE read-only `<li>` 또는 빈 문자열 (intoss family에만 존재)
2816
+ *
2817
+ * mode-aware 분기 (#468): mode가 `relay-mobile`이면 sandbox family chrome(launcher
2818
+ * PWA 절차), 그 외는 intoss family chrome(토스 앱 절차)을 선택한다. `relay-live`는
2819
+ * intoss chrome에 LIVE read-only 라인을 추가한다.
2577
2820
  *
2578
2821
  * SSE 스크립트도 주입 — `#attach-section` hook이 있으면 `/events` push 때 QR이
2579
2822
  * `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#tunnel-status`·`#pages-list` 등
@@ -2581,10 +2824,12 @@ function buildSseScript(strings) {
2581
2824
  *
2582
2825
  * SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.
2583
2826
  */
2584
- function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams()) {
2827
+ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams(), mode) {
2585
2828
  const s = resolveLocaleStrings(locale);
2586
2829
  const langSwitcher = buildLangSwitcher(path, params, locale, s);
2587
- const filled = attachChromeByLocale[locale].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl);
2830
+ const family = attachFamilyForMode(mode);
2831
+ const liveFaq = mode === "relay-live" ? `<li>${s("attach.intoss.faq.liveReadOnly")}</li>` : "";
2832
+ const filled = attachChromeByLocale[locale][family].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__MODE_LABEL__", buildModeLabel(mode, s)).replaceAll("__LIVE_FAQ__", liveFaq).replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl);
2588
2833
  const sseScript = buildSseScript({
2589
2834
  tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
2590
2835
  tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
@@ -2681,11 +2926,12 @@ async function startQrHttpServer(getDashboardState) {
2681
2926
  const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);
2682
2927
  if (dpMatch?.[1]) deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);
2683
2928
  } catch {}
2929
+ const mode = getDashboardState?.().mode;
2684
2930
  QRCode.toDataURL(attachUrl, {
2685
2931
  type: "image/png",
2686
2932
  errorCorrectionLevel: "M"
2687
2933
  }).then((dataUrl) => {
2688
- const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params);
2934
+ const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params, mode);
2689
2935
  res.writeHead(200, {
2690
2936
  "Content-Type": "text/html; charset=utf-8",
2691
2937
  "Cache-Control": "no-store"
@@ -3303,7 +3549,7 @@ const DEBUG_TOOL_DEFINITIONS = [
3303
3549
  },
3304
3550
  {
3305
3551
  name: "get_debug_status",
3306
- description: "Reports the current debug session state — which environment/mode is active, whether a page is attached, and a full diagnostic snapshot — in one call. Use this any time to answer \"what mode am I in right now?\" or \"why is this not working?\" without chaining tools. Fields: mcpVersion (MCP SDK version), devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, recentErrors (last N server-side errors, PII/secret redacted), environment (kind: mock|relay-dev|relay-live|relay-mobile, env: mock|relay backward-compat, reason, liveGuardActive: true when relay-live LIVE guard is active), serverLockHolder (pid + startedAt from the lock file, or null), nextRecommendedAction ({tool, reason} or null — the single next tool to call; in local-target mode tunnel.up=false is normal so \"restart\" is never recommended). All fields are nullable — missing data is null, not an error. debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. Tier C (both mock and relay).",
3552
+ description: "Reports the current debug session state — which environment/mode is active, whether a page is attached, and a full diagnostic snapshot — in one call. Use this any time to answer \"what mode am I in right now?\" or \"why is this not working?\" without chaining tools. Fields: mcpVersion (MCP SDK version), devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, recentErrors (last N server-side errors, PII/secret redacted), authRejects ({count, lastAt} — relay TOTP 401 rejections, secret-free; count > 0 with empty pages means the phone reached the relay but its code was rejected), environment (kind: mock|relay-dev|relay-live|relay-mobile, env: mock|relay backward-compat, reason, liveGuardActive: true when relay-live LIVE guard is active), serverLockHolder (pid + startedAt from the lock file, or null), nextRecommendedAction ({tool, reason} or null — the single next tool to call; in local-target mode tunnel.up=false is normal so \"restart\" is never recommended). All fields are nullable — missing data is null, not an error. debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. Tier C (both mock and relay).",
3307
3553
  inputSchema: {
3308
3554
  type: "object",
3309
3555
  properties: { recent_errors_limit: {
@@ -4068,6 +4314,8 @@ var InMemoryDiagnosticsCollector = class {
4068
4314
  maxSize;
4069
4315
  lastAttachAt = null;
4070
4316
  lastDetachAt = null;
4317
+ authRejectCount = 0;
4318
+ lastAuthRejectAt = null;
4071
4319
  constructor(maxSize = DEFAULT_ERROR_BUFFER_SIZE) {
4072
4320
  this.maxSize = maxSize;
4073
4321
  }
@@ -4096,6 +4344,16 @@ var InMemoryDiagnosticsCollector = class {
4096
4344
  getLastDetachAt() {
4097
4345
  return this.lastDetachAt;
4098
4346
  }
4347
+ recordAuthReject() {
4348
+ this.authRejectCount += 1;
4349
+ this.lastAuthRejectAt = (/* @__PURE__ */ new Date()).toISOString();
4350
+ }
4351
+ getAuthRejects() {
4352
+ return {
4353
+ count: this.authRejectCount,
4354
+ lastAt: this.lastAuthRejectAt
4355
+ };
4356
+ }
4099
4357
  };
4100
4358
  /**
4101
4359
  * Returns the `@modelcontextprotocol/sdk` version baked in at build time via
@@ -4123,7 +4381,7 @@ async function readMcpSdkVersion() {
4123
4381
  * some test environments that skip the build step).
4124
4382
  */
4125
4383
  function readDevtoolsVersion() {
4126
- return "0.1.67";
4384
+ return "0.1.69";
4127
4385
  }
4128
4386
  /**
4129
4387
  * Derives the next recommended action from a completed diagnostics snapshot.
@@ -4132,13 +4390,18 @@ function readDevtoolsVersion() {
4132
4390
  * 0. tunnel.droppedAt non-null → restart (permanent tunnel drop — highest priority)
4133
4391
  * 1. tunnel.up === false AND env is relay → restart (relay needs a live tunnel)
4134
4392
  * 1b. tunnel.up === false AND env is mock → wait_for_page (local target: tunnel-less is normal)
4393
+ * 2a. authRejects.count > 0 AND pages empty → build_attach_url (relay TOTP 거부 관측 — QR 재스캔
4394
+ * 또는 target-side `at` 전달 확인. 일반 rule 2보다 구체적이므로 먼저 평가 — issue #467)
4135
4395
  * 2. tunnel.up, pages empty, env === relay → build_attach_url (start attach)
4136
4396
  * 3. pages has entry + crashDetectedAt non-null → build_attach_url (re-attach after crash)
4137
4397
  * 4. otherwise → null (session looks healthy)
4138
4398
  *
4139
4399
  * Pure — does not throw; receives the final assembled snapshot fields.
4400
+ *
4401
+ * SECRET-HANDLING: the auth-reject reason string carries only the count and
4402
+ * timestamp from {@link AuthRejectsSnapshot} — never a URL, code, or secret.
4140
4403
  */
4141
- function computeNextRecommendedAction(tunnel, pages, env) {
4404
+ function computeNextRecommendedAction(tunnel, pages, env, authRejects = null) {
4142
4405
  if (tunnel.droppedAt != null) return {
4143
4406
  tool: "restart",
4144
4407
  reason: `tunnel permanently dropped at ${tunnel.droppedAt} after ${tunnel.reissueAttempts} reissue attempt(s) — restart the MCP server (npx @ait-co/devtools devtools-mcp)`
@@ -4152,6 +4415,10 @@ function computeNextRecommendedAction(tunnel, pages, env) {
4152
4415
  tool: "restart",
4153
4416
  reason: "tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart"
4154
4417
  };
4418
+ if (authRejects !== null && authRejects.count > 0 && pages !== null && pages.pages.length === 0) return {
4419
+ tool: "build_attach_url",
4420
+ reason: `relay 인증(TOTP) 거부 ${authRejects.count}건 발생 (last ${authRejects.lastAt ?? "unknown"}) — QR을 다시 스캔해 새 코드로 attach하세요(코드는 30초 주기로 만료). 반복되면 폰 페이지 URL에 at 파라미터가 전달되는지(target-side TOTP 전달 경로)를 확인하세요`
4421
+ };
4155
4422
  if (isRelayEnv(env) && pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) return {
4156
4423
  tool: "build_attach_url",
4157
4424
  reason: "tunnel ready, no pages attached — call build_attach_url to generate the attach QR"
@@ -4195,7 +4462,13 @@ async function getDiagnostics(input) {
4195
4462
  } catch {}
4196
4463
  const limit = Math.min(Math.max(1, recentErrorsLimit), 50);
4197
4464
  const recentErrors = collector.getRecentErrors(limit);
4198
- const nextRecommendedAction = computeNextRecommendedAction(tunnelInfo, pages, env);
4465
+ const authRejects = collector.getAuthRejects();
4466
+ if (authRejects.count > 0) recentErrors.push({
4467
+ timestamp: authRejects.lastAt ?? (/* @__PURE__ */ new Date()).toISOString(),
4468
+ message: `WS upgrade auth-rejected (${authRejects.count} times, last ${authRejects.lastAt ?? "unknown"})`,
4469
+ category: "auth"
4470
+ });
4471
+ const nextRecommendedAction = computeNextRecommendedAction(tunnelInfo, pages, env, authRejects);
4199
4472
  return {
4200
4473
  mcpVersion,
4201
4474
  devtoolsVersion,
@@ -4204,6 +4477,7 @@ async function getDiagnostics(input) {
4204
4477
  lastAttachAt: collector.getLastAttachAt(),
4205
4478
  lastDetachAt: collector.getLastDetachAt(),
4206
4479
  recentErrors,
4480
+ authRejects,
4207
4481
  environment: {
4208
4482
  kind: env,
4209
4483
  env: toLegacyEnv(env),
@@ -4611,7 +4885,7 @@ function createDebugServer(deps) {
4611
4885
  const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
4612
4886
  const server = new Server({
4613
4887
  name: "ait-debug",
4614
- version: "0.1.67"
4888
+ version: "0.1.69"
4615
4889
  }, { capabilities: { tools: { listChanged: true } } });
4616
4890
  server.setRequestHandler(ListToolsRequestSchema, () => {
4617
4891
  const conn = router.active;
@@ -5320,7 +5594,8 @@ async function bootRelayFamily(options = {}) {
5320
5594
  const totpEnabled = options.verifyAuth !== void 0;
5321
5595
  const relay = await startChiiRelay({
5322
5596
  port: relayPort,
5323
- verifyAuth: options.verifyAuth
5597
+ verifyAuth: options.verifyAuth,
5598
+ onAuthReject: options.onAuthReject
5324
5599
  });
5325
5600
  logInfo("server.start", {
5326
5601
  port: relay.port,
@@ -5632,7 +5907,8 @@ async function runDebugServer(options = {}) {
5632
5907
  onWssUrl: (wssUrl) => {
5633
5908
  lockHandle.updateWssUrl(wssUrl);
5634
5909
  qrServer?.notifyStateChange();
5635
- }
5910
+ },
5911
+ onAuthReject: () => diagnosticsCollector.recordAuthReject()
5636
5912
  }),
5637
5913
  diagnosticsCollector,
5638
5914
  devtoolsOpener,
@@ -5651,7 +5927,8 @@ async function runDebugServer(options = {}) {
5651
5927
  id: t.id,
5652
5928
  url: t.url
5653
5929
  })),
5654
- attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null
5930
+ attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
5931
+ mode: deriveEnvironment(router.active.kind, getLiveIntent(), router.activeRelayOrigin)
5655
5932
  });
5656
5933
  let qrServer;
5657
5934
  try {
@@ -5796,7 +6073,8 @@ async function runLocalDebugServer(options = {}) {
5796
6073
  onWssUrl: (wssUrl) => {
5797
6074
  lockHandle.updateWssUrl(wssUrl);
5798
6075
  qrServer?.notifyStateChange();
5799
- }
6076
+ },
6077
+ onAuthReject: () => diagnosticsCollector.recordAuthReject()
5800
6078
  }),
5801
6079
  diagnosticsCollector,
5802
6080
  devtoolsOpener,
@@ -5944,7 +6222,8 @@ async function runMobileDebugServer(options = {}) {
5944
6222
  onWssUrl: (wssUrl) => {
5945
6223
  lockHandle.updateWssUrl(wssUrl);
5946
6224
  qrServer?.notifyStateChange();
5947
- }
6225
+ },
6226
+ onAuthReject: () => diagnosticsCollector.recordAuthReject()
5948
6227
  }),
5949
6228
  diagnosticsCollector,
5950
6229
  devtoolsOpener,
@@ -6482,7 +6761,7 @@ function createDevServer(deps = {}) {
6482
6761
  const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
6483
6762
  const server = new Server({
6484
6763
  name: "ait-devtools",
6485
- version: "0.1.67"
6764
+ version: "0.1.69"
6486
6765
  }, { capabilities: { tools: {} } });
6487
6766
  server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
6488
6767
  server.setRequestHandler(CallToolRequestSchema, async (request) => {