@ait-co/devtools 0.1.66 → 0.1.68
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 +495 -68
- 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 +56 -20
- package/dist/panel/index.js.map +1 -1
- package/dist/{qr-http-server-BuyQnaS6.js → qr-http-server-CyVQphTM.js} +376 -45
- package/dist/qr-http-server-CyVQphTM.js.map +1 -0
- package/dist/{qr-http-server-CLtsKfPF.js → qr-http-server-DKEca8J3.js} +376 -45
- package/dist/qr-http-server-DKEca8J3.js.map +1 -0
- package/dist/{qr-http-server-CMJmKkb8.cjs → qr-http-server-DR__VNnX.cjs} +376 -45
- package/dist/qr-http-server-DR__VNnX.cjs.map +1 -0
- package/dist/{qr-http-server-CQwQumPJ.cjs → qr-http-server-DnQSQ3hC.cjs} +376 -45
- package/dist/qr-http-server-DnQSQ3hC.cjs.map +1 -0
- package/dist/{tunnel-BpllDsRw.cjs → tunnel-BMY7KgO5.cjs} +4 -3
- package/dist/{tunnel-BpllDsRw.cjs.map → tunnel-BMY7KgO5.cjs.map} +1 -1
- package/dist/{tunnel-fm4hDfV-.js → tunnel-DIN5Vvbo.js} +4 -3
- package/dist/{tunnel-fm4hDfV-.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-BuyQnaS6.js.map +0 -1
- package/dist/qr-http-server-CLtsKfPF.js.map +0 -1
- package/dist/qr-http-server-CMJmKkb8.cjs.map +0 -1
- package/dist/qr-http-server-CQwQumPJ.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}`,
|
|
@@ -1832,19 +1895,32 @@ const en = {
|
|
|
1832
1895
|
"dashboard.attach.hint": "Call the build_attach_url MCP tool to show the QR here.",
|
|
1833
1896
|
"dashboard.pages.section": "Connected Pages",
|
|
1834
1897
|
"dashboard.pages.empty": "No attached pages",
|
|
1898
|
+
"dashboard.url.copy": "Copy",
|
|
1899
|
+
"dashboard.url.copied": "Copied",
|
|
1835
1900
|
"attach.title": "AIT Debug Session — QR Scan",
|
|
1836
1901
|
"attach.deployment": "deployment: {label}",
|
|
1837
1902
|
"attach.steps.section": "How to scan",
|
|
1838
|
-
"attach.step1": "Open the Toss app.",
|
|
1839
|
-
"attach.step2": "Scan the QR code with your phone camera app.",
|
|
1840
|
-
"attach.step3": "Tap <strong>\"Open in Toss\"</strong> when the popup appears.",
|
|
1841
|
-
"attach.step4": "The mini-app opens and the debug session attaches automatically.",
|
|
1842
1903
|
"attach.faq.section": "Troubleshooting checklist",
|
|
1843
|
-
"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)",
|
|
1844
|
-
"attach.faq.prepare": "<strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter",
|
|
1845
|
-
"attach.faq.chii": "<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import",
|
|
1846
|
-
"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",
|
|
1847
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>",
|
|
1848
1924
|
"launcher.title": "AITC DevTools Launcher",
|
|
1849
1925
|
"launcher.description": "Scan the terminal QR code or paste the tunnel URL.",
|
|
1850
1926
|
"launcher.installCta": "Install launcher to your phone",
|
|
@@ -1858,7 +1934,12 @@ const en = {
|
|
|
1858
1934
|
"launcher.invalidUrl": "Enter a valid http(s):// URL.",
|
|
1859
1935
|
"launcher.debugAuthFailed": "Debug connection authentication failed",
|
|
1860
1936
|
"launcher.debugAuthFailedHint": "The QR code may have expired. Scan a fresh QR code.",
|
|
1861
|
-
"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."
|
|
1862
1943
|
};
|
|
1863
1944
|
//#endregion
|
|
1864
1945
|
//#region src/i18n/index.ts
|
|
@@ -2057,19 +2138,32 @@ const tables = {
|
|
|
2057
2138
|
"dashboard.attach.hint": "build_attach_url MCP tool을 호출하면 QR이 여기에 표시됩니다.",
|
|
2058
2139
|
"dashboard.pages.section": "연결된 Pages",
|
|
2059
2140
|
"dashboard.pages.empty": "attach된 페이지 없음",
|
|
2141
|
+
"dashboard.url.copy": "복사",
|
|
2142
|
+
"dashboard.url.copied": "복사됨",
|
|
2060
2143
|
"attach.title": "AIT 디버그 세션 — QR 스캔",
|
|
2061
2144
|
"attach.deployment": "deployment: {label}",
|
|
2062
2145
|
"attach.steps.section": "스캔 절차",
|
|
2063
|
-
"attach.step1": "토스 앱을 실행하세요.",
|
|
2064
|
-
"attach.step2": "폰 카메라 앱으로 QR 코드를 스캔하세요.",
|
|
2065
|
-
"attach.step3": "팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.",
|
|
2066
|
-
"attach.step4": "미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.",
|
|
2067
2146
|
"attach.faq.section": "진단 체크리스트",
|
|
2068
|
-
"attach.faq.appNotOpen": "<strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)",
|
|
2069
|
-
"attach.faq.prepare": "<strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인",
|
|
2070
|
-
"attach.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
|
|
2071
|
-
"attach.faq.totp": "<strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인",
|
|
2072
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>이 필요합니다",
|
|
2073
2167
|
"launcher.title": "AITC DevTools Launcher",
|
|
2074
2168
|
"launcher.description": "터미널 QR을 스캔하거나 URL을 입력하세요.",
|
|
2075
2169
|
"launcher.installCta": "폰에 런처 설치하기",
|
|
@@ -2083,7 +2177,12 @@ const tables = {
|
|
|
2083
2177
|
"launcher.invalidUrl": "올바른 http(s):// URL을 입력하세요.",
|
|
2084
2178
|
"launcher.debugAuthFailed": "디버그 연결 인증 실패",
|
|
2085
2179
|
"launcher.debugAuthFailedHint": "QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.",
|
|
2086
|
-
"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로 보입니다. 런처를 홈 화면에서 제거 후 다시 설치하면 해소될 수 있어요."
|
|
2087
2186
|
},
|
|
2088
2187
|
en
|
|
2089
2188
|
};
|
|
@@ -2152,12 +2251,24 @@ img.qr {
|
|
|
2152
2251
|
background: #fff; padding: 0.75rem; border-radius: 10px;
|
|
2153
2252
|
display: block; margin: 0.5rem auto;
|
|
2154
2253
|
}
|
|
2254
|
+
.url-row {
|
|
2255
|
+
display: flex; align-items: stretch; gap: 0; margin: 0.5rem 0 0;
|
|
2256
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
2257
|
+
}
|
|
2155
2258
|
.url-box {
|
|
2156
2259
|
font-family: monospace; font-size: 0.7rem;
|
|
2157
2260
|
word-break: break-all; opacity: 0.45;
|
|
2158
2261
|
background: #161b22; padding: 0.6rem 0.85rem;
|
|
2159
|
-
|
|
2262
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
2160
2263
|
}
|
|
2264
|
+
.url-box:hover { opacity: 0.65; }
|
|
2265
|
+
.copy-btn {
|
|
2266
|
+
flex-shrink: 0; padding: 0.4rem 0.7rem;
|
|
2267
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
2268
|
+
color: #58a6ff; font-size: 0.7rem; cursor: pointer; white-space: nowrap;
|
|
2269
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2270
|
+
}
|
|
2271
|
+
.copy-btn:hover { background: #30363d; }
|
|
2161
2272
|
.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }
|
|
2162
2273
|
ul { margin: 0; padding-left: 1.25rem; }
|
|
2163
2274
|
li { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }
|
|
@@ -2169,7 +2280,57 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
|
|
|
2169
2280
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2170
2281
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2171
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>`;
|
|
2172
|
-
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>
|
|
2173
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>
|
|
2174
2335
|
*, *::before, *::after { box-sizing: border-box; }
|
|
2175
2336
|
body {
|
|
@@ -2180,6 +2341,11 @@ body {
|
|
|
2180
2341
|
gap: 1.5rem;
|
|
2181
2342
|
}
|
|
2182
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
|
+
}
|
|
2183
2349
|
.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
|
|
2184
2350
|
img.qr {
|
|
2185
2351
|
width: min(90vw, 360px); height: auto;
|
|
@@ -2191,17 +2357,29 @@ section { width: 100%; max-width: 480px; }
|
|
|
2191
2357
|
h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
|
|
2192
2358
|
ol, ul { margin: 0; padding-left: 1.25rem; }
|
|
2193
2359
|
li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
|
|
2360
|
+
.url-row {
|
|
2361
|
+
display: flex; align-items: stretch; gap: 0;
|
|
2362
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
2363
|
+
}
|
|
2194
2364
|
.url-box {
|
|
2195
2365
|
font-family: monospace; font-size: 0.72rem;
|
|
2196
2366
|
word-break: break-all; opacity: 0.4;
|
|
2197
2367
|
background: #161b22; padding: 0.75rem 1rem;
|
|
2198
|
-
|
|
2368
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
2199
2369
|
}
|
|
2370
|
+
.url-box:hover { opacity: 0.6; }
|
|
2371
|
+
.copy-btn {
|
|
2372
|
+
flex-shrink: 0; padding: 0.5rem 0.8rem;
|
|
2373
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
2374
|
+
color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;
|
|
2375
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2376
|
+
}
|
|
2377
|
+
.copy-btn:hover { background: #30363d; }
|
|
2200
2378
|
hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
|
|
2201
2379
|
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2202
2380
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2203
2381
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2204
|
-
</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>`;
|
|
2205
2383
|
const dashboardChromeHtmlEn = `<!DOCTYPE html>
|
|
2206
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>
|
|
2207
2385
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -2225,12 +2403,24 @@ img.qr {
|
|
|
2225
2403
|
background: #fff; padding: 0.75rem; border-radius: 10px;
|
|
2226
2404
|
display: block; margin: 0.5rem auto;
|
|
2227
2405
|
}
|
|
2406
|
+
.url-row {
|
|
2407
|
+
display: flex; align-items: stretch; gap: 0; margin: 0.5rem 0 0;
|
|
2408
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
2409
|
+
}
|
|
2228
2410
|
.url-box {
|
|
2229
2411
|
font-family: monospace; font-size: 0.7rem;
|
|
2230
2412
|
word-break: break-all; opacity: 0.45;
|
|
2231
2413
|
background: #161b22; padding: 0.6rem 0.85rem;
|
|
2232
|
-
|
|
2414
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
2233
2415
|
}
|
|
2416
|
+
.url-box:hover { opacity: 0.65; }
|
|
2417
|
+
.copy-btn {
|
|
2418
|
+
flex-shrink: 0; padding: 0.4rem 0.7rem;
|
|
2419
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
2420
|
+
color: #58a6ff; font-size: 0.7rem; cursor: pointer; white-space: nowrap;
|
|
2421
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2422
|
+
}
|
|
2423
|
+
.copy-btn:hover { background: #30363d; }
|
|
2234
2424
|
.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }
|
|
2235
2425
|
ul { margin: 0; padding-left: 1.25rem; }
|
|
2236
2426
|
li { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }
|
|
@@ -2242,7 +2432,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
|
|
|
2242
2432
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2243
2433
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2244
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>`;
|
|
2245
|
-
const
|
|
2435
|
+
const attachChromeHtmlEnSandbox = `<!DOCTYPE html>
|
|
2246
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>
|
|
2247
2437
|
*, *::before, *::after { box-sizing: border-box; }
|
|
2248
2438
|
body {
|
|
@@ -2253,6 +2443,11 @@ body {
|
|
|
2253
2443
|
gap: 1.5rem;
|
|
2254
2444
|
}
|
|
2255
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
|
+
}
|
|
2256
2451
|
.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
|
|
2257
2452
|
img.qr {
|
|
2258
2453
|
width: min(90vw, 360px); height: auto;
|
|
@@ -2264,29 +2459,123 @@ section { width: 100%; max-width: 480px; }
|
|
|
2264
2459
|
h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
|
|
2265
2460
|
ol, ul { margin: 0; padding-left: 1.25rem; }
|
|
2266
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
|
+
}
|
|
2267
2466
|
.url-box {
|
|
2268
2467
|
font-family: monospace; font-size: 0.72rem;
|
|
2269
2468
|
word-break: break-all; opacity: 0.4;
|
|
2270
2469
|
background: #161b22; padding: 0.75rem 1rem;
|
|
2271
|
-
|
|
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;
|
|
2272
2478
|
}
|
|
2479
|
+
.copy-btn:hover { background: #30363d; }
|
|
2273
2480
|
hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
|
|
2274
2481
|
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2275
2482
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2276
2483
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2277
|
-
</style></head><body><h1>AIT Debug Session — QR Scan</h1>
|
|
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>
|
|
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>
|
|
2487
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
2488
|
+
body {
|
|
2489
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2490
|
+
background: #0d1117; color: #c9d1d9;
|
|
2491
|
+
display: flex; flex-direction: column; align-items: center;
|
|
2492
|
+
min-height: 100vh; margin: 0; padding: 2rem 1rem;
|
|
2493
|
+
gap: 1.5rem;
|
|
2494
|
+
}
|
|
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
|
+
}
|
|
2501
|
+
.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
|
|
2502
|
+
img.qr {
|
|
2503
|
+
width: min(90vw, 360px); height: auto;
|
|
2504
|
+
image-rendering: pixelated;
|
|
2505
|
+
background: #fff; padding: 1rem; border-radius: 12px;
|
|
2506
|
+
display: block; margin: 0 auto;
|
|
2507
|
+
}
|
|
2508
|
+
section { width: 100%; max-width: 480px; }
|
|
2509
|
+
h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
|
|
2510
|
+
ol, ul { margin: 0; padding-left: 1.25rem; }
|
|
2511
|
+
li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
|
|
2512
|
+
.url-row {
|
|
2513
|
+
display: flex; align-items: stretch; gap: 0;
|
|
2514
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
2515
|
+
}
|
|
2516
|
+
.url-box {
|
|
2517
|
+
font-family: monospace; font-size: 0.72rem;
|
|
2518
|
+
word-break: break-all; opacity: 0.4;
|
|
2519
|
+
background: #161b22; padding: 0.75rem 1rem;
|
|
2520
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
2521
|
+
}
|
|
2522
|
+
.url-box:hover { opacity: 0.6; }
|
|
2523
|
+
.copy-btn {
|
|
2524
|
+
flex-shrink: 0; padding: 0.5rem 0.8rem;
|
|
2525
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
2526
|
+
color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;
|
|
2527
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2528
|
+
}
|
|
2529
|
+
.copy-btn:hover { background: #30363d; }
|
|
2530
|
+
hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
|
|
2531
|
+
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2532
|
+
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2533
|
+
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
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>`;
|
|
2278
2535
|
/** Map from Locale to the precompiled dashboard chrome string. */
|
|
2279
2536
|
const dashboardChromeByLocale = {
|
|
2280
2537
|
ko: dashboardChromeHtmlKo,
|
|
2281
2538
|
en: dashboardChromeHtmlEn
|
|
2282
2539
|
};
|
|
2283
|
-
/** Map from Locale to the precompiled attach page chrome string. */
|
|
2540
|
+
/** Map from Locale × copy family to the precompiled attach page chrome string (#468). */
|
|
2284
2541
|
const attachChromeByLocale = {
|
|
2285
|
-
ko:
|
|
2286
|
-
|
|
2542
|
+
ko: {
|
|
2543
|
+
sandbox: attachChromeHtmlKoSandbox,
|
|
2544
|
+
intoss: attachChromeHtmlKoIntoss
|
|
2545
|
+
},
|
|
2546
|
+
en: {
|
|
2547
|
+
sandbox: attachChromeHtmlEnSandbox,
|
|
2548
|
+
intoss: attachChromeHtmlEnIntoss
|
|
2549
|
+
}
|
|
2287
2550
|
};
|
|
2288
2551
|
//#endregion
|
|
2289
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
|
+
}
|
|
2290
2579
|
/** HTML 특수문자를 이스케이프한다. */
|
|
2291
2580
|
function escapeHtml(s) {
|
|
2292
2581
|
return s.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
@@ -2335,8 +2624,11 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
|
|
|
2335
2624
|
const tunnelStatus = state.tunnel.up ? s("dashboard.tunnel.up") : s("dashboard.tunnel.down");
|
|
2336
2625
|
const tunnelClass = state.tunnel.up ? "status-up" : "status-down";
|
|
2337
2626
|
let attachSection;
|
|
2338
|
-
if (qrDataUrl && state.attachUrl)
|
|
2339
|
-
|
|
2627
|
+
if (qrDataUrl && state.attachUrl) {
|
|
2628
|
+
const safeAttachUrl = escapeHtml(state.attachUrl);
|
|
2629
|
+
const copyLabel = escapeHtml(s("dashboard.url.copy"));
|
|
2630
|
+
attachSection = `<img class="qr" src="${qrDataUrl}" alt="attach QR" /><div class="url-row"><p class="url-box" id="url-box">${safeAttachUrl}</p><button class="copy-btn" id="copy-btn" type="button" aria-label="${copyLabel}">${copyLabel}</button></div>`;
|
|
2631
|
+
} else attachSection = `<p class="hint">${escapeHtml(s("dashboard.attach.hint"))}</p>`;
|
|
2340
2632
|
const pagesSection = state.pages === null ? "" : `<hr /><section id="pages-section"><h2>${escapeHtml(s("dashboard.pages.section"))}</h2><ul id="pages-list">${state.pages.length > 0 ? state.pages.map((p) => {
|
|
2341
2633
|
return `<li><span class="page-id">${escapeHtml(p.id)}</span> <span class="page-url">${escapeHtml(p.url.slice(0, 120))}</span></li>`;
|
|
2342
2634
|
}).join("\n") : `<li class="empty">${escapeHtml(s("dashboard.pages.empty"))}</li>`}</ul></section>`;
|
|
@@ -2344,7 +2636,10 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
|
|
|
2344
2636
|
tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
|
|
2345
2637
|
tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
|
|
2346
2638
|
pagesEmpty: JSON.stringify(s("dashboard.pages.empty")),
|
|
2347
|
-
attachHint: JSON.stringify(s("dashboard.attach.hint"))
|
|
2639
|
+
attachHint: JSON.stringify(s("dashboard.attach.hint")),
|
|
2640
|
+
copyLabel: JSON.stringify(s("dashboard.url.copy")),
|
|
2641
|
+
copiedLabel: JSON.stringify(s("dashboard.url.copied")),
|
|
2642
|
+
dashboardSurface: true
|
|
2348
2643
|
};
|
|
2349
2644
|
const langSwitcher = buildLangSwitcher(path, params, locale, s);
|
|
2350
2645
|
const filled = dashboardChromeByLocale[locale].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__NOW__", escapeHtml(now)).replaceAll("__TUNNEL_CLASS__", tunnelClass).replaceAll("__TUNNEL_STATUS__", escapeHtml(tunnelStatus)).replaceAll("__ATTACH_SECTION__", attachSection).replaceAll("__PAGES_SECTION__", pagesSection);
|
|
@@ -2358,9 +2653,24 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
|
|
|
2358
2653
|
* client side: attachUrl은 DOM에 렌더링, wssUrl은 절대 렌더링하지 않는다.
|
|
2359
2654
|
* pages === null 이면 섹션을 건드리지 않는다 (#411).
|
|
2360
2655
|
*
|
|
2656
|
+
* 두 표면(dashboard / attach) 분기:
|
|
2657
|
+
* - dashboard (dashboardSurface=true): #attach-section innerHTML 전체 교체 방식 유지.
|
|
2658
|
+
* url-box도 innerHTML 재렌더 안에 포함되어 갱신됨.
|
|
2659
|
+
* - /attach (dashboardSurface=false): #attach-section의 img src만 교체하고,
|
|
2660
|
+
* url-box는 #url-box textContent만 갱신한다. (#attach-section에 url-box가 없으므로
|
|
2661
|
+
* innerHTML 교체 시 url-box가 새로 생겨 이중 표시되는 결함을 방지 — #458 결함 수정.)
|
|
2662
|
+
*
|
|
2663
|
+
* 복사 기능: 이벤트 위임으로 document에 단일 핸들러. innerHTML 재렌더 후에도 생존.
|
|
2664
|
+
* - .url-box 클릭 또는 .copy-btn 클릭 → 현재 #url-box textContent 복사.
|
|
2665
|
+
* - clipboard: navigator.clipboard.writeText → 실패/부재 시 textarea execCommand fallback.
|
|
2666
|
+
* - 피드백: 버튼 라벨이 COPIED_LABEL로 ~1.5초 전환 후 COPY_LABEL로 복귀.
|
|
2667
|
+
*
|
|
2361
2668
|
* 문자열 인자는 빌드타임에 ko/en 테이블에서 가져와 JSON.stringify로 이미 escape됨.
|
|
2669
|
+
*
|
|
2670
|
+
* SECRET-HANDLING: URL 값을 console.log 등으로 출력하지 않는다.
|
|
2362
2671
|
*/
|
|
2363
2672
|
function buildSseScript(strings) {
|
|
2673
|
+
const isDashboard = strings.dashboardSurface;
|
|
2364
2674
|
return `<script>
|
|
2365
2675
|
// SSE — /events 구독해 상태 자동 갱신. 빌드 파이프라인 없는 인라인 스크립트.
|
|
2366
2676
|
(function () {
|
|
@@ -2368,6 +2678,64 @@ function buildSseScript(strings) {
|
|
|
2368
2678
|
var TUNNEL_DOWN = ${strings.tunnelDown};
|
|
2369
2679
|
var PAGES_EMPTY = ${strings.pagesEmpty};
|
|
2370
2680
|
var ATTACH_HINT = ${strings.attachHint};
|
|
2681
|
+
var COPY_LABEL = ${strings.copyLabel};
|
|
2682
|
+
var COPIED_LABEL = ${strings.copiedLabel};
|
|
2683
|
+
|
|
2684
|
+
// ── 클립보드 복사 헬퍼 ────────────────────────────────────────────────
|
|
2685
|
+
function copyText(text) {
|
|
2686
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
2687
|
+
return navigator.clipboard.writeText(text);
|
|
2688
|
+
}
|
|
2689
|
+
// fallback: textarea + execCommand
|
|
2690
|
+
return new Promise(function (resolve, reject) {
|
|
2691
|
+
var ta = document.createElement('textarea');
|
|
2692
|
+
ta.value = text;
|
|
2693
|
+
ta.style.position = 'fixed';
|
|
2694
|
+
ta.style.opacity = '0';
|
|
2695
|
+
document.body.appendChild(ta);
|
|
2696
|
+
ta.focus();
|
|
2697
|
+
ta.select();
|
|
2698
|
+
try {
|
|
2699
|
+
document.execCommand('copy') ? resolve() : reject(new Error('execCommand failed'));
|
|
2700
|
+
} catch (err) {
|
|
2701
|
+
reject(err);
|
|
2702
|
+
} finally {
|
|
2703
|
+
document.body.removeChild(ta);
|
|
2704
|
+
}
|
|
2705
|
+
});
|
|
2706
|
+
}
|
|
2707
|
+
|
|
2708
|
+
// ── 복사 피드백 ───────────────────────────────────────────────────────
|
|
2709
|
+
var copyTimer = null;
|
|
2710
|
+
function triggerCopy() {
|
|
2711
|
+
var urlBox = document.getElementById('url-box');
|
|
2712
|
+
if (!urlBox) return;
|
|
2713
|
+
var text = urlBox.textContent || '';
|
|
2714
|
+
if (!text) return;
|
|
2715
|
+
copyText(text).then(function () {
|
|
2716
|
+
var btn = document.getElementById('copy-btn');
|
|
2717
|
+
if (btn) {
|
|
2718
|
+
btn.textContent = COPIED_LABEL;
|
|
2719
|
+
if (copyTimer) clearTimeout(copyTimer);
|
|
2720
|
+
copyTimer = setTimeout(function () {
|
|
2721
|
+
btn.textContent = COPY_LABEL;
|
|
2722
|
+
copyTimer = null;
|
|
2723
|
+
}, 1500);
|
|
2724
|
+
}
|
|
2725
|
+
}).catch(function () { /* 복사 실패 시 조용히 무시 */ });
|
|
2726
|
+
}
|
|
2727
|
+
|
|
2728
|
+
// ── 이벤트 위임 — document 레벨에서 단일 핸들러 (innerHTML 재렌더 후에도 생존) ──
|
|
2729
|
+
document.addEventListener('click', function (e) {
|
|
2730
|
+
var target = e.target;
|
|
2731
|
+
if (!target) return;
|
|
2732
|
+
// .copy-btn 또는 .url-box 클릭 시 복사
|
|
2733
|
+
if (target.closest && (target.closest('.copy-btn') || target.closest('.url-box'))) {
|
|
2734
|
+
triggerCopy();
|
|
2735
|
+
}
|
|
2736
|
+
});
|
|
2737
|
+
|
|
2738
|
+
// ── SSE 구독 ──────────────────────────────────────────────────────────
|
|
2371
2739
|
var src = new EventSource('/events');
|
|
2372
2740
|
src.onmessage = function (e) {
|
|
2373
2741
|
try {
|
|
@@ -2395,23 +2763,37 @@ function buildSseScript(strings) {
|
|
|
2395
2763
|
}
|
|
2396
2764
|
}
|
|
2397
2765
|
}
|
|
2398
|
-
// attachUrl QR
|
|
2766
|
+
// attachUrl QR + url-box 갱신
|
|
2767
|
+
// SECRET-HANDLING: URL 값을 로그로 출력하지 않는다.
|
|
2399
2768
|
var sec = document.getElementById('attach-section');
|
|
2400
2769
|
if (sec) {
|
|
2401
2770
|
if (s.attachUrl) {
|
|
2402
|
-
// QR은 서버에서 새로 렌더한 /qr.png?u= 로 img src 교체.
|
|
2403
|
-
// TOTP at= 코드는 attachUrl 안에 캡슐화 — 별도 노출 없음.
|
|
2404
|
-
// wssUrl은 절대 DOM에 렌더하지 않는다 (SECRET-HANDLING).
|
|
2405
2771
|
var encoded = encodeURIComponent(s.attachUrl);
|
|
2406
2772
|
var safeUrl = String(s.attachUrl).slice(0, 2000).replace(/[<>&"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });
|
|
2773
|
+
${isDashboard ? `// dashboard: #attach-section innerHTML 전체 교체 (img + url-row).
|
|
2774
|
+
// url-box id="url-box" 를 포함해 복사 핸들러가 계속 동작함.
|
|
2407
2775
|
sec.innerHTML =
|
|
2408
2776
|
'<img class="qr" src="/qr.png?u=' + encoded + '" alt="attach QR" />' +
|
|
2409
|
-
'<
|
|
2777
|
+
'<div class=\\"url-row\\">' +
|
|
2778
|
+
'<p class=\\"url-box\\" id=\\"url-box\\">' + safeUrl + '</p>' +
|
|
2779
|
+
'<button class=\\"copy-btn\\" id=\\"copy-btn\\" type=\\"button\\" aria-label=\\"' + COPY_LABEL + '\\">' + COPY_LABEL + '</button>' +
|
|
2780
|
+
'</div>';` : `// /attach: img src만 교체 — url-box는 별도 #url-section에서 관리해 이중 표시 방지(#458).
|
|
2781
|
+
// QR img src 교체: img가 있으면 src만 갱신, 없으면 img 요소 생성.
|
|
2782
|
+
var img = sec.querySelector('img.qr');
|
|
2783
|
+
if (img) {
|
|
2784
|
+
img.src = '/qr.png?u=' + encoded;
|
|
2785
|
+
} else {
|
|
2786
|
+
sec.innerHTML = '<img class=\\"qr\\" src=\\"/qr.png?u=' + encoded + '\\" alt=\\"attach QR\\" />';
|
|
2787
|
+
}
|
|
2788
|
+
// url-box textContent만 갱신 (innerHTML 교체하지 않아 복사 버튼/핸들러 생존).
|
|
2789
|
+
var ub = document.getElementById('url-box');
|
|
2790
|
+
if (ub) ub.textContent = s.attachUrl;`}
|
|
2410
2791
|
} else {
|
|
2411
|
-
sec.innerHTML = '<p class
|
|
2792
|
+
${isDashboard ? `sec.innerHTML = '<p class=\\"hint\\">' + ATTACH_HINT + '</p>';` : `// /attach에서 hint가 필요한 경우는 없으나 방어 처리.
|
|
2793
|
+
sec.innerHTML = '<p class=\\"hint\\">' + ATTACH_HINT + '</p>';`}
|
|
2412
2794
|
}
|
|
2413
2795
|
}
|
|
2414
|
-
// 갱신 시각
|
|
2796
|
+
// 갱신 시각 (dashboard만 #updated 요소 있음)
|
|
2415
2797
|
var upd = document.getElementById('updated');
|
|
2416
2798
|
if (upd) upd.textContent = upd.textContent.replace(/[^ ]+$/, new Date().toISOString());
|
|
2417
2799
|
} catch (_) { /* 파싱 오류 무시 */ }
|
|
@@ -2427,8 +2809,14 @@ function buildSseScript(strings) {
|
|
|
2427
2809
|
*
|
|
2428
2810
|
* 동적 파트:
|
|
2429
2811
|
* - __QR_DATA_URL__ : base64 data URL (QR 이미지)
|
|
2430
|
-
* - __SAFE_LABEL__ : HTML-escaped deploymentId label
|
|
2812
|
+
* - __SAFE_LABEL__ : HTML-escaped deploymentId label (intoss family에만 존재)
|
|
2431
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 라인을 추가한다.
|
|
2432
2820
|
*
|
|
2433
2821
|
* SSE 스크립트도 주입 — `#attach-section` hook이 있으면 `/events` push 때 QR이
|
|
2434
2822
|
* `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#tunnel-status`·`#pages-list` 등
|
|
@@ -2436,15 +2824,20 @@ function buildSseScript(strings) {
|
|
|
2436
2824
|
*
|
|
2437
2825
|
* SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.
|
|
2438
2826
|
*/
|
|
2439
|
-
function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams()) {
|
|
2827
|
+
function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams(), mode) {
|
|
2440
2828
|
const s = resolveLocaleStrings(locale);
|
|
2441
2829
|
const langSwitcher = buildLangSwitcher(path, params, locale, s);
|
|
2442
|
-
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);
|
|
2443
2833
|
const sseScript = buildSseScript({
|
|
2444
2834
|
tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
|
|
2445
2835
|
tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
|
|
2446
2836
|
pagesEmpty: JSON.stringify(s("dashboard.pages.empty")),
|
|
2447
|
-
attachHint: JSON.stringify(s("dashboard.attach.hint"))
|
|
2837
|
+
attachHint: JSON.stringify(s("dashboard.attach.hint")),
|
|
2838
|
+
copyLabel: JSON.stringify(s("dashboard.url.copy")),
|
|
2839
|
+
copiedLabel: JSON.stringify(s("dashboard.url.copied")),
|
|
2840
|
+
dashboardSurface: false
|
|
2448
2841
|
});
|
|
2449
2842
|
return filled.replace("</body>", `${sseScript}\n</body>`);
|
|
2450
2843
|
}
|
|
@@ -2533,11 +2926,12 @@ async function startQrHttpServer(getDashboardState) {
|
|
|
2533
2926
|
const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);
|
|
2534
2927
|
if (dpMatch?.[1]) deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);
|
|
2535
2928
|
} catch {}
|
|
2929
|
+
const mode = getDashboardState?.().mode;
|
|
2536
2930
|
QRCode.toDataURL(attachUrl, {
|
|
2537
2931
|
type: "image/png",
|
|
2538
2932
|
errorCorrectionLevel: "M"
|
|
2539
2933
|
}).then((dataUrl) => {
|
|
2540
|
-
const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params);
|
|
2934
|
+
const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params, mode);
|
|
2541
2935
|
res.writeHead(200, {
|
|
2542
2936
|
"Content-Type": "text/html; charset=utf-8",
|
|
2543
2937
|
"Cache-Control": "no-store"
|
|
@@ -3155,7 +3549,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
3155
3549
|
},
|
|
3156
3550
|
{
|
|
3157
3551
|
name: "get_debug_status",
|
|
3158
|
-
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).",
|
|
3159
3553
|
inputSchema: {
|
|
3160
3554
|
type: "object",
|
|
3161
3555
|
properties: { recent_errors_limit: {
|
|
@@ -3920,6 +4314,8 @@ var InMemoryDiagnosticsCollector = class {
|
|
|
3920
4314
|
maxSize;
|
|
3921
4315
|
lastAttachAt = null;
|
|
3922
4316
|
lastDetachAt = null;
|
|
4317
|
+
authRejectCount = 0;
|
|
4318
|
+
lastAuthRejectAt = null;
|
|
3923
4319
|
constructor(maxSize = DEFAULT_ERROR_BUFFER_SIZE) {
|
|
3924
4320
|
this.maxSize = maxSize;
|
|
3925
4321
|
}
|
|
@@ -3948,6 +4344,16 @@ var InMemoryDiagnosticsCollector = class {
|
|
|
3948
4344
|
getLastDetachAt() {
|
|
3949
4345
|
return this.lastDetachAt;
|
|
3950
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
|
+
}
|
|
3951
4357
|
};
|
|
3952
4358
|
/**
|
|
3953
4359
|
* Returns the `@modelcontextprotocol/sdk` version baked in at build time via
|
|
@@ -3975,7 +4381,7 @@ async function readMcpSdkVersion() {
|
|
|
3975
4381
|
* some test environments that skip the build step).
|
|
3976
4382
|
*/
|
|
3977
4383
|
function readDevtoolsVersion() {
|
|
3978
|
-
return "0.1.
|
|
4384
|
+
return "0.1.68";
|
|
3979
4385
|
}
|
|
3980
4386
|
/**
|
|
3981
4387
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -3984,13 +4390,18 @@ function readDevtoolsVersion() {
|
|
|
3984
4390
|
* 0. tunnel.droppedAt non-null → restart (permanent tunnel drop — highest priority)
|
|
3985
4391
|
* 1. tunnel.up === false AND env is relay → restart (relay needs a live tunnel)
|
|
3986
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)
|
|
3987
4395
|
* 2. tunnel.up, pages empty, env === relay → build_attach_url (start attach)
|
|
3988
4396
|
* 3. pages has entry + crashDetectedAt non-null → build_attach_url (re-attach after crash)
|
|
3989
4397
|
* 4. otherwise → null (session looks healthy)
|
|
3990
4398
|
*
|
|
3991
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.
|
|
3992
4403
|
*/
|
|
3993
|
-
function computeNextRecommendedAction(tunnel, pages, env) {
|
|
4404
|
+
function computeNextRecommendedAction(tunnel, pages, env, authRejects = null) {
|
|
3994
4405
|
if (tunnel.droppedAt != null) return {
|
|
3995
4406
|
tool: "restart",
|
|
3996
4407
|
reason: `tunnel permanently dropped at ${tunnel.droppedAt} after ${tunnel.reissueAttempts} reissue attempt(s) — restart the MCP server (npx @ait-co/devtools devtools-mcp)`
|
|
@@ -4004,6 +4415,10 @@ function computeNextRecommendedAction(tunnel, pages, env) {
|
|
|
4004
4415
|
tool: "restart",
|
|
4005
4416
|
reason: "tunnel not up — run `npx @ait-co/devtools devtools-mcp` to restart"
|
|
4006
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
|
+
};
|
|
4007
4422
|
if (isRelayEnv(env) && pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) return {
|
|
4008
4423
|
tool: "build_attach_url",
|
|
4009
4424
|
reason: "tunnel ready, no pages attached — call build_attach_url to generate the attach QR"
|
|
@@ -4047,7 +4462,13 @@ async function getDiagnostics(input) {
|
|
|
4047
4462
|
} catch {}
|
|
4048
4463
|
const limit = Math.min(Math.max(1, recentErrorsLimit), 50);
|
|
4049
4464
|
const recentErrors = collector.getRecentErrors(limit);
|
|
4050
|
-
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);
|
|
4051
4472
|
return {
|
|
4052
4473
|
mcpVersion,
|
|
4053
4474
|
devtoolsVersion,
|
|
@@ -4056,6 +4477,7 @@ async function getDiagnostics(input) {
|
|
|
4056
4477
|
lastAttachAt: collector.getLastAttachAt(),
|
|
4057
4478
|
lastDetachAt: collector.getLastDetachAt(),
|
|
4058
4479
|
recentErrors,
|
|
4480
|
+
authRejects,
|
|
4059
4481
|
environment: {
|
|
4060
4482
|
kind: env,
|
|
4061
4483
|
env: toLegacyEnv(env),
|
|
@@ -4463,7 +4885,7 @@ function createDebugServer(deps) {
|
|
|
4463
4885
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
4464
4886
|
const server = new Server({
|
|
4465
4887
|
name: "ait-debug",
|
|
4466
|
-
version: "0.1.
|
|
4888
|
+
version: "0.1.68"
|
|
4467
4889
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
4468
4890
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
4469
4891
|
const conn = router.active;
|
|
@@ -5172,7 +5594,8 @@ async function bootRelayFamily(options = {}) {
|
|
|
5172
5594
|
const totpEnabled = options.verifyAuth !== void 0;
|
|
5173
5595
|
const relay = await startChiiRelay({
|
|
5174
5596
|
port: relayPort,
|
|
5175
|
-
verifyAuth: options.verifyAuth
|
|
5597
|
+
verifyAuth: options.verifyAuth,
|
|
5598
|
+
onAuthReject: options.onAuthReject
|
|
5176
5599
|
});
|
|
5177
5600
|
logInfo("server.start", {
|
|
5178
5601
|
port: relay.port,
|
|
@@ -5484,7 +5907,8 @@ async function runDebugServer(options = {}) {
|
|
|
5484
5907
|
onWssUrl: (wssUrl) => {
|
|
5485
5908
|
lockHandle.updateWssUrl(wssUrl);
|
|
5486
5909
|
qrServer?.notifyStateChange();
|
|
5487
|
-
}
|
|
5910
|
+
},
|
|
5911
|
+
onAuthReject: () => diagnosticsCollector.recordAuthReject()
|
|
5488
5912
|
}),
|
|
5489
5913
|
diagnosticsCollector,
|
|
5490
5914
|
devtoolsOpener,
|
|
@@ -5503,7 +5927,8 @@ async function runDebugServer(options = {}) {
|
|
|
5503
5927
|
id: t.id,
|
|
5504
5928
|
url: t.url
|
|
5505
5929
|
})),
|
|
5506
|
-
attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null
|
|
5930
|
+
attachUrl: lastAttachParts ? rebuildAttachUrl(lastAttachParts) : null,
|
|
5931
|
+
mode: deriveEnvironment(router.active.kind, getLiveIntent(), router.activeRelayOrigin)
|
|
5507
5932
|
});
|
|
5508
5933
|
let qrServer;
|
|
5509
5934
|
try {
|
|
@@ -5648,7 +6073,8 @@ async function runLocalDebugServer(options = {}) {
|
|
|
5648
6073
|
onWssUrl: (wssUrl) => {
|
|
5649
6074
|
lockHandle.updateWssUrl(wssUrl);
|
|
5650
6075
|
qrServer?.notifyStateChange();
|
|
5651
|
-
}
|
|
6076
|
+
},
|
|
6077
|
+
onAuthReject: () => diagnosticsCollector.recordAuthReject()
|
|
5652
6078
|
}),
|
|
5653
6079
|
diagnosticsCollector,
|
|
5654
6080
|
devtoolsOpener,
|
|
@@ -5796,7 +6222,8 @@ async function runMobileDebugServer(options = {}) {
|
|
|
5796
6222
|
onWssUrl: (wssUrl) => {
|
|
5797
6223
|
lockHandle.updateWssUrl(wssUrl);
|
|
5798
6224
|
qrServer?.notifyStateChange();
|
|
5799
|
-
}
|
|
6225
|
+
},
|
|
6226
|
+
onAuthReject: () => diagnosticsCollector.recordAuthReject()
|
|
5800
6227
|
}),
|
|
5801
6228
|
diagnosticsCollector,
|
|
5802
6229
|
devtoolsOpener,
|
|
@@ -6334,7 +6761,7 @@ function createDevServer(deps = {}) {
|
|
|
6334
6761
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
6335
6762
|
const server = new Server({
|
|
6336
6763
|
name: "ait-devtools",
|
|
6337
|
-
version: "0.1.
|
|
6764
|
+
version: "0.1.68"
|
|
6338
6765
|
}, { capabilities: { tools: {} } });
|
|
6339
6766
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
6340
6767
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|