@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
|
@@ -179,19 +179,32 @@ const en = {
|
|
|
179
179
|
"dashboard.attach.hint": "Call the build_attach_url MCP tool to show the QR here.",
|
|
180
180
|
"dashboard.pages.section": "Connected Pages",
|
|
181
181
|
"dashboard.pages.empty": "No attached pages",
|
|
182
|
+
"dashboard.url.copy": "Copy",
|
|
183
|
+
"dashboard.url.copied": "Copied",
|
|
182
184
|
"attach.title": "AIT Debug Session — QR Scan",
|
|
183
185
|
"attach.deployment": "deployment: {label}",
|
|
184
186
|
"attach.steps.section": "How to scan",
|
|
185
|
-
"attach.step1": "Open the Toss app.",
|
|
186
|
-
"attach.step2": "Scan the QR code with your phone camera app.",
|
|
187
|
-
"attach.step3": "Tap <strong>\"Open in Toss\"</strong> when the popup appears.",
|
|
188
|
-
"attach.step4": "The mini-app opens and the debug session attaches automatically.",
|
|
189
187
|
"attach.faq.section": "Troubleshooting checklist",
|
|
190
|
-
"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)",
|
|
191
|
-
"attach.faq.prepare": "<strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter",
|
|
192
|
-
"attach.faq.chii": "<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import",
|
|
193
|
-
"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",
|
|
194
188
|
"attach.url.section": "URL (fallback)",
|
|
189
|
+
"attach.mode.sandbox": "Env 2 — AITC Sandbox PWA",
|
|
190
|
+
"attach.mode.intossDev": "Env 3 — intoss-private relay dev",
|
|
191
|
+
"attach.mode.intossLive": "Env 4 — intoss live relay debug",
|
|
192
|
+
"attach.sandbox.step1": "Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).",
|
|
193
|
+
"attach.sandbox.step2": "Scan this QR code with <strong>\"Scan QR with camera\"</strong> inside the launcher.",
|
|
194
|
+
"attach.sandbox.step3": "The mini-app opens fullscreen and the debug session attaches automatically.",
|
|
195
|
+
"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",
|
|
196
|
+
"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",
|
|
197
|
+
"attach.sandbox.faq.totp": "<strong>QR expired (TOTP 30 s)</strong> — scan a fresh QR code",
|
|
198
|
+
"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",
|
|
199
|
+
"attach.intoss.step1": "Open the Toss app.",
|
|
200
|
+
"attach.intoss.step2": "Scan the QR code with your phone camera app.",
|
|
201
|
+
"attach.intoss.step3": "Tap <strong>\"Open in Toss\"</strong> when the popup appears.",
|
|
202
|
+
"attach.intoss.step4": "The mini-app opens and the debug session attaches automatically.",
|
|
203
|
+
"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)",
|
|
204
|
+
"attach.intoss.faq.prepare": "<strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter",
|
|
205
|
+
"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",
|
|
206
|
+
"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",
|
|
207
|
+
"attach.intoss.faq.liveReadOnly": "<strong>LIVE session is read-only</strong> — <code>call_sdk</code>/<code>evaluate</code> require an explicit <code>confirm</code>",
|
|
195
208
|
"launcher.title": "AITC DevTools Launcher",
|
|
196
209
|
"launcher.description": "Scan the terminal QR code or paste the tunnel URL.",
|
|
197
210
|
"launcher.installCta": "Install launcher to your phone",
|
|
@@ -205,7 +218,12 @@ const en = {
|
|
|
205
218
|
"launcher.invalidUrl": "Enter a valid http(s):// URL.",
|
|
206
219
|
"launcher.debugAuthFailed": "Debug connection authentication failed",
|
|
207
220
|
"launcher.debugAuthFailedHint": "The QR code may have expired. Scan a fresh QR code.",
|
|
208
|
-
"launcher.debugAuthRescanCta": "Scan a new QR"
|
|
221
|
+
"launcher.debugAuthRescanCta": "Scan a new QR",
|
|
222
|
+
"launcher.diagFab": "Diag",
|
|
223
|
+
"launcher.diagTitle": "Viewport diagnostics",
|
|
224
|
+
"launcher.diagYes": "yes",
|
|
225
|
+
"launcher.diagNo": "no",
|
|
226
|
+
"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."
|
|
209
227
|
};
|
|
210
228
|
//#endregion
|
|
211
229
|
//#region src/i18n/index.ts
|
|
@@ -404,19 +422,32 @@ const tables = {
|
|
|
404
422
|
"dashboard.attach.hint": "build_attach_url MCP tool을 호출하면 QR이 여기에 표시됩니다.",
|
|
405
423
|
"dashboard.pages.section": "연결된 Pages",
|
|
406
424
|
"dashboard.pages.empty": "attach된 페이지 없음",
|
|
425
|
+
"dashboard.url.copy": "복사",
|
|
426
|
+
"dashboard.url.copied": "복사됨",
|
|
407
427
|
"attach.title": "AIT 디버그 세션 — QR 스캔",
|
|
408
428
|
"attach.deployment": "deployment: {label}",
|
|
409
429
|
"attach.steps.section": "스캔 절차",
|
|
410
|
-
"attach.step1": "토스 앱을 실행하세요.",
|
|
411
|
-
"attach.step2": "폰 카메라 앱으로 QR 코드를 스캔하세요.",
|
|
412
|
-
"attach.step3": "팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.",
|
|
413
|
-
"attach.step4": "미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.",
|
|
414
430
|
"attach.faq.section": "진단 체크리스트",
|
|
415
|
-
"attach.faq.appNotOpen": "<strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)",
|
|
416
|
-
"attach.faq.prepare": "<strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인",
|
|
417
|
-
"attach.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
|
|
418
|
-
"attach.faq.totp": "<strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인",
|
|
419
431
|
"attach.url.section": "URL (fallback)",
|
|
432
|
+
"attach.mode.sandbox": "환경 2 — AITC Sandbox PWA",
|
|
433
|
+
"attach.mode.intossDev": "환경 3 — intoss-private relay dev",
|
|
434
|
+
"attach.mode.intossLive": "환경 4 — intoss live relay debug",
|
|
435
|
+
"attach.sandbox.step1": "홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).",
|
|
436
|
+
"attach.sandbox.step2": "launcher 안의 <strong>\"QR 카메라로 스캔\"</strong>으로 이 QR 코드를 스캔하세요.",
|
|
437
|
+
"attach.sandbox.step3": "미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.",
|
|
438
|
+
"attach.sandbox.faq.notInstalled": "<strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요",
|
|
439
|
+
"attach.sandbox.faq.cameraApp": "<strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요",
|
|
440
|
+
"attach.sandbox.faq.totp": "<strong>QR이 만료된 경우 (TOTP 30초)</strong> — 새 QR을 다시 스캔하세요",
|
|
441
|
+
"attach.sandbox.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
|
|
442
|
+
"attach.intoss.step1": "토스 앱을 실행하세요.",
|
|
443
|
+
"attach.intoss.step2": "폰 카메라 앱으로 QR 코드를 스캔하세요.",
|
|
444
|
+
"attach.intoss.step3": "팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.",
|
|
445
|
+
"attach.intoss.step4": "미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.",
|
|
446
|
+
"attach.intoss.faq.appNotOpen": "<strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)",
|
|
447
|
+
"attach.intoss.faq.prepare": "<strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인",
|
|
448
|
+
"attach.intoss.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
|
|
449
|
+
"attach.intoss.faq.totp": "<strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인",
|
|
450
|
+
"attach.intoss.faq.liveReadOnly": "<strong>LIVE 세션은 read-only입니다</strong> — <code>call_sdk</code>/<code>evaluate</code> 실행에는 명시적 <code>confirm</code>이 필요합니다",
|
|
420
451
|
"launcher.title": "AITC DevTools Launcher",
|
|
421
452
|
"launcher.description": "터미널 QR을 스캔하거나 URL을 입력하세요.",
|
|
422
453
|
"launcher.installCta": "폰에 런처 설치하기",
|
|
@@ -430,7 +461,12 @@ const tables = {
|
|
|
430
461
|
"launcher.invalidUrl": "올바른 http(s):// URL을 입력하세요.",
|
|
431
462
|
"launcher.debugAuthFailed": "디버그 연결 인증 실패",
|
|
432
463
|
"launcher.debugAuthFailedHint": "QR 코드가 만료되었을 수 있어요. 새 QR을 다시 스캔하세요.",
|
|
433
|
-
"launcher.debugAuthRescanCta": "새 QR 스캔하기"
|
|
464
|
+
"launcher.debugAuthRescanCta": "새 QR 스캔하기",
|
|
465
|
+
"launcher.diagFab": "진단",
|
|
466
|
+
"launcher.diagTitle": "뷰포트 진단",
|
|
467
|
+
"launcher.diagYes": "예",
|
|
468
|
+
"launcher.diagNo": "아니요",
|
|
469
|
+
"launcher.letterboxDetected": "표시 영역이 {pt}pt 부족합니다 — iOS standalone letterbox로 보입니다. 런처를 홈 화면에서 제거 후 다시 설치하면 해소될 수 있어요."
|
|
434
470
|
},
|
|
435
471
|
en
|
|
436
472
|
};
|
|
@@ -499,12 +535,24 @@ img.qr {
|
|
|
499
535
|
background: #fff; padding: 0.75rem; border-radius: 10px;
|
|
500
536
|
display: block; margin: 0.5rem auto;
|
|
501
537
|
}
|
|
538
|
+
.url-row {
|
|
539
|
+
display: flex; align-items: stretch; gap: 0; margin: 0.5rem 0 0;
|
|
540
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
541
|
+
}
|
|
502
542
|
.url-box {
|
|
503
543
|
font-family: monospace; font-size: 0.7rem;
|
|
504
544
|
word-break: break-all; opacity: 0.45;
|
|
505
545
|
background: #161b22; padding: 0.6rem 0.85rem;
|
|
506
|
-
|
|
546
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
547
|
+
}
|
|
548
|
+
.url-box:hover { opacity: 0.65; }
|
|
549
|
+
.copy-btn {
|
|
550
|
+
flex-shrink: 0; padding: 0.4rem 0.7rem;
|
|
551
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
552
|
+
color: #58a6ff; font-size: 0.7rem; cursor: pointer; white-space: nowrap;
|
|
553
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
507
554
|
}
|
|
555
|
+
.copy-btn:hover { background: #30363d; }
|
|
508
556
|
.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }
|
|
509
557
|
ul { margin: 0; padding-left: 1.25rem; }
|
|
510
558
|
li { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }
|
|
@@ -516,7 +564,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
|
|
|
516
564
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
517
565
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
518
566
|
</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>`;
|
|
519
|
-
const
|
|
567
|
+
const attachChromeHtmlKoSandbox = `<!DOCTYPE html>
|
|
520
568
|
<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>
|
|
521
569
|
*, *::before, *::after { box-sizing: border-box; }
|
|
522
570
|
body {
|
|
@@ -527,6 +575,11 @@ body {
|
|
|
527
575
|
gap: 1.5rem;
|
|
528
576
|
}
|
|
529
577
|
h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
|
|
578
|
+
.mode-label {
|
|
579
|
+
font-size: 0.78rem; font-weight: 600; color: #79c0ff;
|
|
580
|
+
background: #161b22; border: 1px solid #30363d; border-radius: 999px;
|
|
581
|
+
padding: 0.25rem 0.75rem; margin: 0;
|
|
582
|
+
}
|
|
530
583
|
.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
|
|
531
584
|
img.qr {
|
|
532
585
|
width: min(90vw, 360px); height: auto;
|
|
@@ -538,17 +591,79 @@ section { width: 100%; max-width: 480px; }
|
|
|
538
591
|
h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
|
|
539
592
|
ol, ul { margin: 0; padding-left: 1.25rem; }
|
|
540
593
|
li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
|
|
594
|
+
.url-row {
|
|
595
|
+
display: flex; align-items: stretch; gap: 0;
|
|
596
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
597
|
+
}
|
|
541
598
|
.url-box {
|
|
542
599
|
font-family: monospace; font-size: 0.72rem;
|
|
543
600
|
word-break: break-all; opacity: 0.4;
|
|
544
601
|
background: #161b22; padding: 0.75rem 1rem;
|
|
545
|
-
|
|
602
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
603
|
+
}
|
|
604
|
+
.url-box:hover { opacity: 0.6; }
|
|
605
|
+
.copy-btn {
|
|
606
|
+
flex-shrink: 0; padding: 0.5rem 0.8rem;
|
|
607
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
608
|
+
color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;
|
|
609
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
546
610
|
}
|
|
611
|
+
.copy-btn:hover { background: #30363d; }
|
|
547
612
|
hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
|
|
548
613
|
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
549
614
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
550
615
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
551
|
-
</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>
|
|
616
|
+
</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>`;
|
|
617
|
+
const attachChromeHtmlKoIntoss = `<!DOCTYPE html>
|
|
618
|
+
<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>
|
|
619
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
620
|
+
body {
|
|
621
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
622
|
+
background: #0d1117; color: #c9d1d9;
|
|
623
|
+
display: flex; flex-direction: column; align-items: center;
|
|
624
|
+
min-height: 100vh; margin: 0; padding: 2rem 1rem;
|
|
625
|
+
gap: 1.5rem;
|
|
626
|
+
}
|
|
627
|
+
h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
|
|
628
|
+
.mode-label {
|
|
629
|
+
font-size: 0.78rem; font-weight: 600; color: #79c0ff;
|
|
630
|
+
background: #161b22; border: 1px solid #30363d; border-radius: 999px;
|
|
631
|
+
padding: 0.25rem 0.75rem; margin: 0;
|
|
632
|
+
}
|
|
633
|
+
.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
|
|
634
|
+
img.qr {
|
|
635
|
+
width: min(90vw, 360px); height: auto;
|
|
636
|
+
image-rendering: pixelated;
|
|
637
|
+
background: #fff; padding: 1rem; border-radius: 12px;
|
|
638
|
+
display: block; margin: 0 auto;
|
|
639
|
+
}
|
|
640
|
+
section { width: 100%; max-width: 480px; }
|
|
641
|
+
h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
|
|
642
|
+
ol, ul { margin: 0; padding-left: 1.25rem; }
|
|
643
|
+
li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
|
|
644
|
+
.url-row {
|
|
645
|
+
display: flex; align-items: stretch; gap: 0;
|
|
646
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
647
|
+
}
|
|
648
|
+
.url-box {
|
|
649
|
+
font-family: monospace; font-size: 0.72rem;
|
|
650
|
+
word-break: break-all; opacity: 0.4;
|
|
651
|
+
background: #161b22; padding: 0.75rem 1rem;
|
|
652
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
653
|
+
}
|
|
654
|
+
.url-box:hover { opacity: 0.6; }
|
|
655
|
+
.copy-btn {
|
|
656
|
+
flex-shrink: 0; padding: 0.5rem 0.8rem;
|
|
657
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
658
|
+
color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;
|
|
659
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
660
|
+
}
|
|
661
|
+
.copy-btn:hover { background: #30363d; }
|
|
662
|
+
hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
|
|
663
|
+
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
664
|
+
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
665
|
+
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
666
|
+
</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>`;
|
|
552
667
|
const dashboardChromeHtmlEn = `<!DOCTYPE html>
|
|
553
668
|
<html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>AIT Debug Dashboard</title><style>
|
|
554
669
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -572,12 +687,24 @@ img.qr {
|
|
|
572
687
|
background: #fff; padding: 0.75rem; border-radius: 10px;
|
|
573
688
|
display: block; margin: 0.5rem auto;
|
|
574
689
|
}
|
|
690
|
+
.url-row {
|
|
691
|
+
display: flex; align-items: stretch; gap: 0; margin: 0.5rem 0 0;
|
|
692
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
693
|
+
}
|
|
575
694
|
.url-box {
|
|
576
695
|
font-family: monospace; font-size: 0.7rem;
|
|
577
696
|
word-break: break-all; opacity: 0.45;
|
|
578
697
|
background: #161b22; padding: 0.6rem 0.85rem;
|
|
579
|
-
|
|
698
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
580
699
|
}
|
|
700
|
+
.url-box:hover { opacity: 0.65; }
|
|
701
|
+
.copy-btn {
|
|
702
|
+
flex-shrink: 0; padding: 0.4rem 0.7rem;
|
|
703
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
704
|
+
color: #58a6ff; font-size: 0.7rem; cursor: pointer; white-space: nowrap;
|
|
705
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
706
|
+
}
|
|
707
|
+
.copy-btn:hover { background: #30363d; }
|
|
581
708
|
.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }
|
|
582
709
|
ul { margin: 0; padding-left: 1.25rem; }
|
|
583
710
|
li { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }
|
|
@@ -589,7 +716,57 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
|
|
|
589
716
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
590
717
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
591
718
|
</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>`;
|
|
592
|
-
const
|
|
719
|
+
const attachChromeHtmlEnSandbox = `<!DOCTYPE html>
|
|
720
|
+
<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>
|
|
721
|
+
*, *::before, *::after { box-sizing: border-box; }
|
|
722
|
+
body {
|
|
723
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
724
|
+
background: #0d1117; color: #c9d1d9;
|
|
725
|
+
display: flex; flex-direction: column; align-items: center;
|
|
726
|
+
min-height: 100vh; margin: 0; padding: 2rem 1rem;
|
|
727
|
+
gap: 1.5rem;
|
|
728
|
+
}
|
|
729
|
+
h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
|
|
730
|
+
.mode-label {
|
|
731
|
+
font-size: 0.78rem; font-weight: 600; color: #79c0ff;
|
|
732
|
+
background: #161b22; border: 1px solid #30363d; border-radius: 999px;
|
|
733
|
+
padding: 0.25rem 0.75rem; margin: 0;
|
|
734
|
+
}
|
|
735
|
+
.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
|
|
736
|
+
img.qr {
|
|
737
|
+
width: min(90vw, 360px); height: auto;
|
|
738
|
+
image-rendering: pixelated;
|
|
739
|
+
background: #fff; padding: 1rem; border-radius: 12px;
|
|
740
|
+
display: block; margin: 0 auto;
|
|
741
|
+
}
|
|
742
|
+
section { width: 100%; max-width: 480px; }
|
|
743
|
+
h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
|
|
744
|
+
ol, ul { margin: 0; padding-left: 1.25rem; }
|
|
745
|
+
li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
|
|
746
|
+
.url-row {
|
|
747
|
+
display: flex; align-items: stretch; gap: 0;
|
|
748
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
749
|
+
}
|
|
750
|
+
.url-box {
|
|
751
|
+
font-family: monospace; font-size: 0.72rem;
|
|
752
|
+
word-break: break-all; opacity: 0.4;
|
|
753
|
+
background: #161b22; padding: 0.75rem 1rem;
|
|
754
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
755
|
+
}
|
|
756
|
+
.url-box:hover { opacity: 0.6; }
|
|
757
|
+
.copy-btn {
|
|
758
|
+
flex-shrink: 0; padding: 0.5rem 0.8rem;
|
|
759
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
760
|
+
color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;
|
|
761
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
762
|
+
}
|
|
763
|
+
.copy-btn:hover { background: #30363d; }
|
|
764
|
+
hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
|
|
765
|
+
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
766
|
+
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
767
|
+
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
768
|
+
</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>`;
|
|
769
|
+
const attachChromeHtmlEnIntoss = `<!DOCTYPE html>
|
|
593
770
|
<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>
|
|
594
771
|
*, *::before, *::after { box-sizing: border-box; }
|
|
595
772
|
body {
|
|
@@ -600,6 +777,11 @@ body {
|
|
|
600
777
|
gap: 1.5rem;
|
|
601
778
|
}
|
|
602
779
|
h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
|
|
780
|
+
.mode-label {
|
|
781
|
+
font-size: 0.78rem; font-weight: 600; color: #79c0ff;
|
|
782
|
+
background: #161b22; border: 1px solid #30363d; border-radius: 999px;
|
|
783
|
+
padding: 0.25rem 0.75rem; margin: 0;
|
|
784
|
+
}
|
|
603
785
|
.label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
|
|
604
786
|
img.qr {
|
|
605
787
|
width: min(90vw, 360px); height: auto;
|
|
@@ -611,29 +793,73 @@ section { width: 100%; max-width: 480px; }
|
|
|
611
793
|
h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
|
|
612
794
|
ol, ul { margin: 0; padding-left: 1.25rem; }
|
|
613
795
|
li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
|
|
796
|
+
.url-row {
|
|
797
|
+
display: flex; align-items: stretch; gap: 0;
|
|
798
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
799
|
+
}
|
|
614
800
|
.url-box {
|
|
615
801
|
font-family: monospace; font-size: 0.72rem;
|
|
616
802
|
word-break: break-all; opacity: 0.4;
|
|
617
803
|
background: #161b22; padding: 0.75rem 1rem;
|
|
618
|
-
|
|
804
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
619
805
|
}
|
|
806
|
+
.url-box:hover { opacity: 0.6; }
|
|
807
|
+
.copy-btn {
|
|
808
|
+
flex-shrink: 0; padding: 0.5rem 0.8rem;
|
|
809
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
810
|
+
color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;
|
|
811
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
812
|
+
}
|
|
813
|
+
.copy-btn:hover { background: #30363d; }
|
|
620
814
|
hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
|
|
621
815
|
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
622
816
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
623
817
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
624
|
-
</style></head><body><h1>AIT Debug Session — QR Scan</h1>
|
|
818
|
+
</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>`;
|
|
625
819
|
/** Map from Locale to the precompiled dashboard chrome string. */
|
|
626
820
|
const dashboardChromeByLocale = {
|
|
627
821
|
ko: dashboardChromeHtmlKo,
|
|
628
822
|
en: dashboardChromeHtmlEn
|
|
629
823
|
};
|
|
630
|
-
/** Map from Locale to the precompiled attach page chrome string. */
|
|
824
|
+
/** Map from Locale × copy family to the precompiled attach page chrome string (#468). */
|
|
631
825
|
const attachChromeByLocale = {
|
|
632
|
-
ko:
|
|
633
|
-
|
|
826
|
+
ko: {
|
|
827
|
+
sandbox: attachChromeHtmlKoSandbox,
|
|
828
|
+
intoss: attachChromeHtmlKoIntoss
|
|
829
|
+
},
|
|
830
|
+
en: {
|
|
831
|
+
sandbox: attachChromeHtmlEnSandbox,
|
|
832
|
+
intoss: attachChromeHtmlEnIntoss
|
|
833
|
+
}
|
|
634
834
|
};
|
|
635
835
|
//#endregion
|
|
636
836
|
//#region src/mcp/qr-http-server.ts
|
|
837
|
+
/** mode → 어느 precompiled attach chrome family를 쓰는가 (#468). */
|
|
838
|
+
function attachFamilyForMode(mode) {
|
|
839
|
+
return mode === "relay-mobile" ? "sandbox" : "intoss";
|
|
840
|
+
}
|
|
841
|
+
/**
|
|
842
|
+
* mode → 페이지 상단 환경 라벨 HTML (`__MODE_LABEL__` 토큰 채움, #468).
|
|
843
|
+
* 사용자가 fidelity 사다리의 어느 겹에 있는지 즉시 알게 하는 환경 가시화 배지.
|
|
844
|
+
* mode 미지정/'mock'은 빈 문자열 — 알 수 없는 환경을 거짓으로 라벨링하지 않는다.
|
|
845
|
+
*/
|
|
846
|
+
function buildModeLabel(mode, s) {
|
|
847
|
+
let label;
|
|
848
|
+
switch (mode) {
|
|
849
|
+
case "relay-mobile":
|
|
850
|
+
label = s("attach.mode.sandbox");
|
|
851
|
+
break;
|
|
852
|
+
case "relay-dev":
|
|
853
|
+
label = s("attach.mode.intossDev");
|
|
854
|
+
break;
|
|
855
|
+
case "relay-live":
|
|
856
|
+
label = s("attach.mode.intossLive");
|
|
857
|
+
break;
|
|
858
|
+
case "mock":
|
|
859
|
+
case void 0: return "";
|
|
860
|
+
}
|
|
861
|
+
return `<p class="mode-label">${escapeHtml(label)}</p>`;
|
|
862
|
+
}
|
|
637
863
|
/** HTML 특수문자를 이스케이프한다. */
|
|
638
864
|
function escapeHtml(s) {
|
|
639
865
|
return s.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
@@ -682,8 +908,11 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
|
|
|
682
908
|
const tunnelStatus = state.tunnel.up ? s("dashboard.tunnel.up") : s("dashboard.tunnel.down");
|
|
683
909
|
const tunnelClass = state.tunnel.up ? "status-up" : "status-down";
|
|
684
910
|
let attachSection;
|
|
685
|
-
if (qrDataUrl && state.attachUrl)
|
|
686
|
-
|
|
911
|
+
if (qrDataUrl && state.attachUrl) {
|
|
912
|
+
const safeAttachUrl = escapeHtml(state.attachUrl);
|
|
913
|
+
const copyLabel = escapeHtml(s("dashboard.url.copy"));
|
|
914
|
+
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>`;
|
|
915
|
+
} else attachSection = `<p class="hint">${escapeHtml(s("dashboard.attach.hint"))}</p>`;
|
|
687
916
|
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) => {
|
|
688
917
|
return `<li><span class="page-id">${escapeHtml(p.id)}</span> <span class="page-url">${escapeHtml(p.url.slice(0, 120))}</span></li>`;
|
|
689
918
|
}).join("\n") : `<li class="empty">${escapeHtml(s("dashboard.pages.empty"))}</li>`}</ul></section>`;
|
|
@@ -691,7 +920,10 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
|
|
|
691
920
|
tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
|
|
692
921
|
tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
|
|
693
922
|
pagesEmpty: JSON.stringify(s("dashboard.pages.empty")),
|
|
694
|
-
attachHint: JSON.stringify(s("dashboard.attach.hint"))
|
|
923
|
+
attachHint: JSON.stringify(s("dashboard.attach.hint")),
|
|
924
|
+
copyLabel: JSON.stringify(s("dashboard.url.copy")),
|
|
925
|
+
copiedLabel: JSON.stringify(s("dashboard.url.copied")),
|
|
926
|
+
dashboardSurface: true
|
|
695
927
|
};
|
|
696
928
|
const langSwitcher = buildLangSwitcher(path, params, locale, s);
|
|
697
929
|
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);
|
|
@@ -705,9 +937,24 @@ function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new U
|
|
|
705
937
|
* client side: attachUrl은 DOM에 렌더링, wssUrl은 절대 렌더링하지 않는다.
|
|
706
938
|
* pages === null 이면 섹션을 건드리지 않는다 (#411).
|
|
707
939
|
*
|
|
940
|
+
* 두 표면(dashboard / attach) 분기:
|
|
941
|
+
* - dashboard (dashboardSurface=true): #attach-section innerHTML 전체 교체 방식 유지.
|
|
942
|
+
* url-box도 innerHTML 재렌더 안에 포함되어 갱신됨.
|
|
943
|
+
* - /attach (dashboardSurface=false): #attach-section의 img src만 교체하고,
|
|
944
|
+
* url-box는 #url-box textContent만 갱신한다. (#attach-section에 url-box가 없으므로
|
|
945
|
+
* innerHTML 교체 시 url-box가 새로 생겨 이중 표시되는 결함을 방지 — #458 결함 수정.)
|
|
946
|
+
*
|
|
947
|
+
* 복사 기능: 이벤트 위임으로 document에 단일 핸들러. innerHTML 재렌더 후에도 생존.
|
|
948
|
+
* - .url-box 클릭 또는 .copy-btn 클릭 → 현재 #url-box textContent 복사.
|
|
949
|
+
* - clipboard: navigator.clipboard.writeText → 실패/부재 시 textarea execCommand fallback.
|
|
950
|
+
* - 피드백: 버튼 라벨이 COPIED_LABEL로 ~1.5초 전환 후 COPY_LABEL로 복귀.
|
|
951
|
+
*
|
|
708
952
|
* 문자열 인자는 빌드타임에 ko/en 테이블에서 가져와 JSON.stringify로 이미 escape됨.
|
|
953
|
+
*
|
|
954
|
+
* SECRET-HANDLING: URL 값을 console.log 등으로 출력하지 않는다.
|
|
709
955
|
*/
|
|
710
956
|
function buildSseScript(strings) {
|
|
957
|
+
const isDashboard = strings.dashboardSurface;
|
|
711
958
|
return `<script>
|
|
712
959
|
// SSE — /events 구독해 상태 자동 갱신. 빌드 파이프라인 없는 인라인 스크립트.
|
|
713
960
|
(function () {
|
|
@@ -715,6 +962,64 @@ function buildSseScript(strings) {
|
|
|
715
962
|
var TUNNEL_DOWN = ${strings.tunnelDown};
|
|
716
963
|
var PAGES_EMPTY = ${strings.pagesEmpty};
|
|
717
964
|
var ATTACH_HINT = ${strings.attachHint};
|
|
965
|
+
var COPY_LABEL = ${strings.copyLabel};
|
|
966
|
+
var COPIED_LABEL = ${strings.copiedLabel};
|
|
967
|
+
|
|
968
|
+
// ── 클립보드 복사 헬퍼 ────────────────────────────────────────────────
|
|
969
|
+
function copyText(text) {
|
|
970
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
971
|
+
return navigator.clipboard.writeText(text);
|
|
972
|
+
}
|
|
973
|
+
// fallback: textarea + execCommand
|
|
974
|
+
return new Promise(function (resolve, reject) {
|
|
975
|
+
var ta = document.createElement('textarea');
|
|
976
|
+
ta.value = text;
|
|
977
|
+
ta.style.position = 'fixed';
|
|
978
|
+
ta.style.opacity = '0';
|
|
979
|
+
document.body.appendChild(ta);
|
|
980
|
+
ta.focus();
|
|
981
|
+
ta.select();
|
|
982
|
+
try {
|
|
983
|
+
document.execCommand('copy') ? resolve() : reject(new Error('execCommand failed'));
|
|
984
|
+
} catch (err) {
|
|
985
|
+
reject(err);
|
|
986
|
+
} finally {
|
|
987
|
+
document.body.removeChild(ta);
|
|
988
|
+
}
|
|
989
|
+
});
|
|
990
|
+
}
|
|
991
|
+
|
|
992
|
+
// ── 복사 피드백 ───────────────────────────────────────────────────────
|
|
993
|
+
var copyTimer = null;
|
|
994
|
+
function triggerCopy() {
|
|
995
|
+
var urlBox = document.getElementById('url-box');
|
|
996
|
+
if (!urlBox) return;
|
|
997
|
+
var text = urlBox.textContent || '';
|
|
998
|
+
if (!text) return;
|
|
999
|
+
copyText(text).then(function () {
|
|
1000
|
+
var btn = document.getElementById('copy-btn');
|
|
1001
|
+
if (btn) {
|
|
1002
|
+
btn.textContent = COPIED_LABEL;
|
|
1003
|
+
if (copyTimer) clearTimeout(copyTimer);
|
|
1004
|
+
copyTimer = setTimeout(function () {
|
|
1005
|
+
btn.textContent = COPY_LABEL;
|
|
1006
|
+
copyTimer = null;
|
|
1007
|
+
}, 1500);
|
|
1008
|
+
}
|
|
1009
|
+
}).catch(function () { /* 복사 실패 시 조용히 무시 */ });
|
|
1010
|
+
}
|
|
1011
|
+
|
|
1012
|
+
// ── 이벤트 위임 — document 레벨에서 단일 핸들러 (innerHTML 재렌더 후에도 생존) ──
|
|
1013
|
+
document.addEventListener('click', function (e) {
|
|
1014
|
+
var target = e.target;
|
|
1015
|
+
if (!target) return;
|
|
1016
|
+
// .copy-btn 또는 .url-box 클릭 시 복사
|
|
1017
|
+
if (target.closest && (target.closest('.copy-btn') || target.closest('.url-box'))) {
|
|
1018
|
+
triggerCopy();
|
|
1019
|
+
}
|
|
1020
|
+
});
|
|
1021
|
+
|
|
1022
|
+
// ── SSE 구독 ──────────────────────────────────────────────────────────
|
|
718
1023
|
var src = new EventSource('/events');
|
|
719
1024
|
src.onmessage = function (e) {
|
|
720
1025
|
try {
|
|
@@ -742,23 +1047,37 @@ function buildSseScript(strings) {
|
|
|
742
1047
|
}
|
|
743
1048
|
}
|
|
744
1049
|
}
|
|
745
|
-
// attachUrl QR
|
|
1050
|
+
// attachUrl QR + url-box 갱신
|
|
1051
|
+
// SECRET-HANDLING: URL 값을 로그로 출력하지 않는다.
|
|
746
1052
|
var sec = document.getElementById('attach-section');
|
|
747
1053
|
if (sec) {
|
|
748
1054
|
if (s.attachUrl) {
|
|
749
|
-
// QR은 서버에서 새로 렌더한 /qr.png?u= 로 img src 교체.
|
|
750
|
-
// TOTP at= 코드는 attachUrl 안에 캡슐화 — 별도 노출 없음.
|
|
751
|
-
// wssUrl은 절대 DOM에 렌더하지 않는다 (SECRET-HANDLING).
|
|
752
1055
|
var encoded = encodeURIComponent(s.attachUrl);
|
|
753
1056
|
var safeUrl = String(s.attachUrl).slice(0, 2000).replace(/[<>&"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });
|
|
1057
|
+
${isDashboard ? `// dashboard: #attach-section innerHTML 전체 교체 (img + url-row).
|
|
1058
|
+
// url-box id="url-box" 를 포함해 복사 핸들러가 계속 동작함.
|
|
754
1059
|
sec.innerHTML =
|
|
755
1060
|
'<img class="qr" src="/qr.png?u=' + encoded + '" alt="attach QR" />' +
|
|
756
|
-
'<
|
|
1061
|
+
'<div class=\\"url-row\\">' +
|
|
1062
|
+
'<p class=\\"url-box\\" id=\\"url-box\\">' + safeUrl + '</p>' +
|
|
1063
|
+
'<button class=\\"copy-btn\\" id=\\"copy-btn\\" type=\\"button\\" aria-label=\\"' + COPY_LABEL + '\\">' + COPY_LABEL + '</button>' +
|
|
1064
|
+
'</div>';` : `// /attach: img src만 교체 — url-box는 별도 #url-section에서 관리해 이중 표시 방지(#458).
|
|
1065
|
+
// QR img src 교체: img가 있으면 src만 갱신, 없으면 img 요소 생성.
|
|
1066
|
+
var img = sec.querySelector('img.qr');
|
|
1067
|
+
if (img) {
|
|
1068
|
+
img.src = '/qr.png?u=' + encoded;
|
|
1069
|
+
} else {
|
|
1070
|
+
sec.innerHTML = '<img class=\\"qr\\" src=\\"/qr.png?u=' + encoded + '\\" alt=\\"attach QR\\" />';
|
|
1071
|
+
}
|
|
1072
|
+
// url-box textContent만 갱신 (innerHTML 교체하지 않아 복사 버튼/핸들러 생존).
|
|
1073
|
+
var ub = document.getElementById('url-box');
|
|
1074
|
+
if (ub) ub.textContent = s.attachUrl;`}
|
|
757
1075
|
} else {
|
|
758
|
-
sec.innerHTML = '<p class
|
|
1076
|
+
${isDashboard ? `sec.innerHTML = '<p class=\\"hint\\">' + ATTACH_HINT + '</p>';` : `// /attach에서 hint가 필요한 경우는 없으나 방어 처리.
|
|
1077
|
+
sec.innerHTML = '<p class=\\"hint\\">' + ATTACH_HINT + '</p>';`}
|
|
759
1078
|
}
|
|
760
1079
|
}
|
|
761
|
-
// 갱신 시각
|
|
1080
|
+
// 갱신 시각 (dashboard만 #updated 요소 있음)
|
|
762
1081
|
var upd = document.getElementById('updated');
|
|
763
1082
|
if (upd) upd.textContent = upd.textContent.replace(/[^ ]+$/, new Date().toISOString());
|
|
764
1083
|
} catch (_) { /* 파싱 오류 무시 */ }
|
|
@@ -774,8 +1093,14 @@ function buildSseScript(strings) {
|
|
|
774
1093
|
*
|
|
775
1094
|
* 동적 파트:
|
|
776
1095
|
* - __QR_DATA_URL__ : base64 data URL (QR 이미지)
|
|
777
|
-
* - __SAFE_LABEL__ : HTML-escaped deploymentId label
|
|
1096
|
+
* - __SAFE_LABEL__ : HTML-escaped deploymentId label (intoss family에만 존재)
|
|
778
1097
|
* - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)
|
|
1098
|
+
* - __MODE_LABEL__ : 환경 배지 (`<p class="mode-label">…</p>` 또는 빈 문자열, #468)
|
|
1099
|
+
* - __LIVE_FAQ__ : 환경 4 LIVE read-only `<li>` 또는 빈 문자열 (intoss family에만 존재)
|
|
1100
|
+
*
|
|
1101
|
+
* mode-aware 분기 (#468): mode가 `relay-mobile`이면 sandbox family chrome(launcher
|
|
1102
|
+
* PWA 절차), 그 외는 intoss family chrome(토스 앱 절차)을 선택한다. `relay-live`는
|
|
1103
|
+
* intoss chrome에 LIVE read-only 라인을 추가한다.
|
|
779
1104
|
*
|
|
780
1105
|
* SSE 스크립트도 주입 — `#attach-section` hook이 있으면 `/events` push 때 QR이
|
|
781
1106
|
* `/qr.png?u=<fresh attachUrl>`로 자동 갱신된다. `#tunnel-status`·`#pages-list` 등
|
|
@@ -783,15 +1108,20 @@ function buildSseScript(strings) {
|
|
|
783
1108
|
*
|
|
784
1109
|
* SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.
|
|
785
1110
|
*/
|
|
786
|
-
function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams()) {
|
|
1111
|
+
function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams(), mode) {
|
|
787
1112
|
const s = resolveLocaleStrings(locale);
|
|
788
1113
|
const langSwitcher = buildLangSwitcher(path, params, locale, s);
|
|
789
|
-
const
|
|
1114
|
+
const family = attachFamilyForMode(mode);
|
|
1115
|
+
const liveFaq = mode === "relay-live" ? `<li>${s("attach.intoss.faq.liveReadOnly")}</li>` : "";
|
|
1116
|
+
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);
|
|
790
1117
|
const sseScript = buildSseScript({
|
|
791
1118
|
tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
|
|
792
1119
|
tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
|
|
793
1120
|
pagesEmpty: JSON.stringify(s("dashboard.pages.empty")),
|
|
794
|
-
attachHint: JSON.stringify(s("dashboard.attach.hint"))
|
|
1121
|
+
attachHint: JSON.stringify(s("dashboard.attach.hint")),
|
|
1122
|
+
copyLabel: JSON.stringify(s("dashboard.url.copy")),
|
|
1123
|
+
copiedLabel: JSON.stringify(s("dashboard.url.copied")),
|
|
1124
|
+
dashboardSurface: false
|
|
795
1125
|
});
|
|
796
1126
|
return filled.replace("</body>", `${sseScript}\n</body>`);
|
|
797
1127
|
}
|
|
@@ -880,11 +1210,12 @@ async function startQrHttpServer(getDashboardState) {
|
|
|
880
1210
|
const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);
|
|
881
1211
|
if (dpMatch?.[1]) deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);
|
|
882
1212
|
} catch {}
|
|
1213
|
+
const mode = getDashboardState?.().mode;
|
|
883
1214
|
QRCode.toDataURL(attachUrl, {
|
|
884
1215
|
type: "image/png",
|
|
885
1216
|
errorCorrectionLevel: "M"
|
|
886
1217
|
}).then((dataUrl) => {
|
|
887
|
-
const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params);
|
|
1218
|
+
const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params, mode);
|
|
888
1219
|
res.writeHead(200, {
|
|
889
1220
|
"Content-Type": "text/html; charset=utf-8",
|
|
890
1221
|
"Cache-Control": "no-store"
|
|
@@ -955,4 +1286,4 @@ async function startQrHttpServer(getDashboardState) {
|
|
|
955
1286
|
//#endregion
|
|
956
1287
|
export { startQrHttpServer };
|
|
957
1288
|
|
|
958
|
-
//# sourceMappingURL=qr-http-server-
|
|
1289
|
+
//# sourceMappingURL=qr-http-server-DKEca8J3.js.map
|