@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.
- package/dist/chii-relay-BNd3G3UG.js +152 -0
- package/dist/chii-relay-BNd3G3UG.js.map +1 -0
- package/dist/chii-relay-DngjQ2_A.cjs +151 -0
- package/dist/chii-relay-DngjQ2_A.cjs.map +1 -0
- package/dist/in-app/index.d.ts +24 -6
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +27 -8
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.js +332 -53
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +52 -20
- package/dist/panel/index.js.map +1 -1
- package/dist/{qr-http-server-ChC7P6-H.js → qr-http-server-CyVQphTM.js} +213 -30
- package/dist/qr-http-server-CyVQphTM.js.map +1 -0
- package/dist/{qr-http-server-BUfbLGm1.js → qr-http-server-DKEca8J3.js} +213 -30
- package/dist/qr-http-server-DKEca8J3.js.map +1 -0
- package/dist/{qr-http-server-B5YndXcS.cjs → qr-http-server-DR__VNnX.cjs} +213 -30
- package/dist/qr-http-server-DR__VNnX.cjs.map +1 -0
- package/dist/{qr-http-server-DRlwR54D.cjs → qr-http-server-DnQSQ3hC.cjs} +213 -30
- package/dist/qr-http-server-DnQSQ3hC.cjs.map +1 -0
- package/dist/{tunnel-CrlCX5sZ.cjs → tunnel-BMY7KgO5.cjs} +4 -3
- package/dist/{tunnel-CrlCX5sZ.cjs.map → tunnel-BMY7KgO5.cjs.map} +1 -1
- package/dist/{tunnel-BNzbSCfB.js → tunnel-DIN5Vvbo.js} +4 -3
- package/dist/{tunnel-BNzbSCfB.js.map → tunnel-DIN5Vvbo.js.map} +1 -1
- package/dist/unplugin/index.cjs +10 -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 +10 -3
- package/dist/unplugin/index.js.map +1 -1
- package/dist/unplugin/tunnel.cjs +3 -2
- package/dist/unplugin/tunnel.cjs.map +1 -1
- package/dist/unplugin/tunnel.d.cts.map +1 -1
- package/dist/unplugin/tunnel.d.ts.map +1 -1
- package/dist/unplugin/tunnel.js +3 -2
- package/dist/unplugin/tunnel.js.map +1 -1
- package/package.json +1 -1
- package/dist/chii-relay-57BfqF_5.cjs +0 -88
- package/dist/chii-relay-57BfqF_5.cjs.map +0 -1
- package/dist/chii-relay-itXOz7kS.js +0 -89
- package/dist/chii-relay-itXOz7kS.js.map +0 -1
- package/dist/qr-http-server-B5YndXcS.cjs.map +0 -1
- package/dist/qr-http-server-BUfbLGm1.js.map +0 -1
- package/dist/qr-http-server-ChC7P6-H.js.map +0 -1
- 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
|
|
725
|
-
*
|
|
726
|
-
* `http.Server`
|
|
727
|
-
* `socket.destroy()`
|
|
728
|
-
*
|
|
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
|
-
|
|
768
|
-
if (
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
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
|
|
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>
|
|
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
|
|
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>
|
|
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:
|
|
2338
|
-
|
|
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
|
|
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.
|
|
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
|
|
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.
|
|
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.
|
|
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) => {
|