@ait-co/devtools 0.1.65 → 0.1.67
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/mcp/cli.js +217 -32
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/panel/index.js +10 -2
- package/dist/panel/index.js.map +1 -1
- package/dist/{qr-http-server-C34O140J.cjs → qr-http-server-B5YndXcS.cjs} +215 -30
- package/dist/qr-http-server-B5YndXcS.cjs.map +1 -0
- package/dist/{qr-http-server-DBgh4rxe.js → qr-http-server-BUfbLGm1.js} +215 -30
- package/dist/qr-http-server-BUfbLGm1.js.map +1 -0
- package/dist/{qr-http-server-Dp3a1AMl.js → qr-http-server-ChC7P6-H.js} +215 -30
- package/dist/qr-http-server-ChC7P6-H.js.map +1 -0
- package/dist/{qr-http-server-CR4p9Y2d.cjs → qr-http-server-DRlwR54D.cjs} +215 -30
- package/dist/qr-http-server-DRlwR54D.cjs.map +1 -0
- package/dist/{tunnel-CIc0oSit.js → tunnel-BNzbSCfB.js} +2 -2
- package/dist/{tunnel-CIc0oSit.js.map → tunnel-BNzbSCfB.js.map} +1 -1
- package/dist/{tunnel-H7VujZz5.cjs → tunnel-CrlCX5sZ.cjs} +2 -2
- package/dist/{tunnel-H7VujZz5.cjs.map → tunnel-CrlCX5sZ.cjs.map} +1 -1
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +1 -1
- package/dist/unplugin/tunnel.js +1 -1
- package/package.json +1 -1
- package/dist/qr-http-server-C34O140J.cjs.map +0 -1
- package/dist/qr-http-server-CR4p9Y2d.cjs.map +0 -1
- package/dist/qr-http-server-DBgh4rxe.js.map +0 -1
- package/dist/qr-http-server-Dp3a1AMl.js.map +0 -1
package/dist/mcp/cli.js
CHANGED
|
@@ -1821,6 +1821,8 @@ const en = {
|
|
|
1821
1821
|
"notifications.option.newAgreement": "newAgreement (first-time agree)",
|
|
1822
1822
|
"notifications.option.alreadyAgreed": "alreadyAgreed (already opted-in)",
|
|
1823
1823
|
"notifications.option.agreementRejected": "agreementRejected (user declined)",
|
|
1824
|
+
"dashboard.lang.ko": "한국어",
|
|
1825
|
+
"dashboard.lang.en": "English",
|
|
1824
1826
|
"dashboard.title": "AIT Debug Dashboard",
|
|
1825
1827
|
"dashboard.updated": "Last updated: {ts}",
|
|
1826
1828
|
"dashboard.tunnel.section": "Tunnel status",
|
|
@@ -1830,6 +1832,8 @@ const en = {
|
|
|
1830
1832
|
"dashboard.attach.hint": "Call the build_attach_url MCP tool to show the QR here.",
|
|
1831
1833
|
"dashboard.pages.section": "Connected Pages",
|
|
1832
1834
|
"dashboard.pages.empty": "No attached pages",
|
|
1835
|
+
"dashboard.url.copy": "Copy",
|
|
1836
|
+
"dashboard.url.copied": "Copied",
|
|
1833
1837
|
"attach.title": "AIT Debug Session — QR Scan",
|
|
1834
1838
|
"attach.deployment": "deployment: {label}",
|
|
1835
1839
|
"attach.steps.section": "How to scan",
|
|
@@ -2044,6 +2048,8 @@ const tables = {
|
|
|
2044
2048
|
"notifications.option.newAgreement": "newAgreement (최초 동의)",
|
|
2045
2049
|
"notifications.option.alreadyAgreed": "alreadyAgreed (이미 동의됨)",
|
|
2046
2050
|
"notifications.option.agreementRejected": "agreementRejected (사용자 거절)",
|
|
2051
|
+
"dashboard.lang.ko": "한국어",
|
|
2052
|
+
"dashboard.lang.en": "English",
|
|
2047
2053
|
"dashboard.title": "AIT 디버그 Dashboard",
|
|
2048
2054
|
"dashboard.updated": "마지막 갱신: {ts}",
|
|
2049
2055
|
"dashboard.tunnel.section": "터널 상태",
|
|
@@ -2053,6 +2059,8 @@ const tables = {
|
|
|
2053
2059
|
"dashboard.attach.hint": "build_attach_url MCP tool을 호출하면 QR이 여기에 표시됩니다.",
|
|
2054
2060
|
"dashboard.pages.section": "연결된 Pages",
|
|
2055
2061
|
"dashboard.pages.empty": "attach된 페이지 없음",
|
|
2062
|
+
"dashboard.url.copy": "복사",
|
|
2063
|
+
"dashboard.url.copied": "복사됨",
|
|
2056
2064
|
"attach.title": "AIT 디버그 세션 — QR 스캔",
|
|
2057
2065
|
"attach.deployment": "deployment: {label}",
|
|
2058
2066
|
"attach.steps.section": "스캔 절차",
|
|
@@ -2096,11 +2104,11 @@ function localeFromLanguageTag(lang) {
|
|
|
2096
2104
|
* surfaces (e.g. the qr-http-server dashboard) have no `navigator`, so the
|
|
2097
2105
|
* request header is the only language signal. Reads the FIRST language tag
|
|
2098
2106
|
* (highest priority, ignoring `q=` weights — good enough for ko/en) and feeds
|
|
2099
|
-
* it through the same `ko`-vs-`en` heuristic `detectLocale` uses. Returns `'
|
|
2100
|
-
* for an empty/missing header.
|
|
2107
|
+
* it through the same `ko`-vs-`en` heuristic `detectLocale` uses. Returns `'ko'`
|
|
2108
|
+
* for an empty/missing header (ko is the primary locale).
|
|
2101
2109
|
*/
|
|
2102
2110
|
function parseAcceptLanguage(header) {
|
|
2103
|
-
if (!header) return "
|
|
2111
|
+
if (!header) return "ko";
|
|
2104
2112
|
return localeFromLanguageTag(header.split(",")[0]?.trim().split(";")[0]?.trim() ?? "");
|
|
2105
2113
|
}
|
|
2106
2114
|
/**
|
|
@@ -2148,12 +2156,24 @@ img.qr {
|
|
|
2148
2156
|
background: #fff; padding: 0.75rem; border-radius: 10px;
|
|
2149
2157
|
display: block; margin: 0.5rem auto;
|
|
2150
2158
|
}
|
|
2159
|
+
.url-row {
|
|
2160
|
+
display: flex; align-items: stretch; gap: 0; margin: 0.5rem 0 0;
|
|
2161
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
2162
|
+
}
|
|
2151
2163
|
.url-box {
|
|
2152
2164
|
font-family: monospace; font-size: 0.7rem;
|
|
2153
2165
|
word-break: break-all; opacity: 0.45;
|
|
2154
2166
|
background: #161b22; padding: 0.6rem 0.85rem;
|
|
2155
|
-
|
|
2167
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
2168
|
+
}
|
|
2169
|
+
.url-box:hover { opacity: 0.65; }
|
|
2170
|
+
.copy-btn {
|
|
2171
|
+
flex-shrink: 0; padding: 0.4rem 0.7rem;
|
|
2172
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
2173
|
+
color: #58a6ff; font-size: 0.7rem; cursor: pointer; white-space: nowrap;
|
|
2174
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2156
2175
|
}
|
|
2176
|
+
.copy-btn:hover { background: #30363d; }
|
|
2157
2177
|
.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }
|
|
2158
2178
|
ul { margin: 0; padding-left: 1.25rem; }
|
|
2159
2179
|
li { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }
|
|
@@ -2161,7 +2181,10 @@ li.empty { opacity: 0.4; list-style: none; padding-left: 0; }
|
|
|
2161
2181
|
.page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }
|
|
2162
2182
|
.page-url { word-break: break-all; }
|
|
2163
2183
|
hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
|
|
2164
|
-
|
|
2184
|
+
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2185
|
+
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2186
|
+
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2187
|
+
</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>`;
|
|
2165
2188
|
const attachChromeHtmlKo = `<!DOCTYPE html>
|
|
2166
2189
|
<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>
|
|
2167
2190
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -2184,14 +2207,29 @@ section { width: 100%; max-width: 480px; }
|
|
|
2184
2207
|
h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
|
|
2185
2208
|
ol, ul { margin: 0; padding-left: 1.25rem; }
|
|
2186
2209
|
li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
|
|
2210
|
+
.url-row {
|
|
2211
|
+
display: flex; align-items: stretch; gap: 0;
|
|
2212
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
2213
|
+
}
|
|
2187
2214
|
.url-box {
|
|
2188
2215
|
font-family: monospace; font-size: 0.72rem;
|
|
2189
2216
|
word-break: break-all; opacity: 0.4;
|
|
2190
2217
|
background: #161b22; padding: 0.75rem 1rem;
|
|
2191
|
-
|
|
2218
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
2192
2219
|
}
|
|
2220
|
+
.url-box:hover { opacity: 0.6; }
|
|
2221
|
+
.copy-btn {
|
|
2222
|
+
flex-shrink: 0; padding: 0.5rem 0.8rem;
|
|
2223
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
2224
|
+
color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;
|
|
2225
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2226
|
+
}
|
|
2227
|
+
.copy-btn:hover { background: #30363d; }
|
|
2193
2228
|
hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
|
|
2194
|
-
|
|
2229
|
+
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2230
|
+
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2231
|
+
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2232
|
+
</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__LANG_SWITCHER__<p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>토스 앱을 실행하세요.</li><li>폰 카메라 앱으로 QR 코드를 스캔하세요.</li><li>팝업이 뜨면 <strong>"토스로 열기"</strong>를 탭하세요.</li><li>미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)</li><li><strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li><li><strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="복사">복사</button></div></section></body></html>`;
|
|
2195
2233
|
const dashboardChromeHtmlEn = `<!DOCTYPE html>
|
|
2196
2234
|
<html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>AIT Debug Dashboard</title><style>
|
|
2197
2235
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -2215,12 +2253,24 @@ img.qr {
|
|
|
2215
2253
|
background: #fff; padding: 0.75rem; border-radius: 10px;
|
|
2216
2254
|
display: block; margin: 0.5rem auto;
|
|
2217
2255
|
}
|
|
2256
|
+
.url-row {
|
|
2257
|
+
display: flex; align-items: stretch; gap: 0; margin: 0.5rem 0 0;
|
|
2258
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
2259
|
+
}
|
|
2218
2260
|
.url-box {
|
|
2219
2261
|
font-family: monospace; font-size: 0.7rem;
|
|
2220
2262
|
word-break: break-all; opacity: 0.45;
|
|
2221
2263
|
background: #161b22; padding: 0.6rem 0.85rem;
|
|
2222
|
-
|
|
2264
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
2223
2265
|
}
|
|
2266
|
+
.url-box:hover { opacity: 0.65; }
|
|
2267
|
+
.copy-btn {
|
|
2268
|
+
flex-shrink: 0; padding: 0.4rem 0.7rem;
|
|
2269
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
2270
|
+
color: #58a6ff; font-size: 0.7rem; cursor: pointer; white-space: nowrap;
|
|
2271
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2272
|
+
}
|
|
2273
|
+
.copy-btn:hover { background: #30363d; }
|
|
2224
2274
|
.hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }
|
|
2225
2275
|
ul { margin: 0; padding-left: 1.25rem; }
|
|
2226
2276
|
li { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }
|
|
@@ -2228,7 +2278,10 @@ li.empty { opacity: 0.4; list-style: none; padding-left: 0; }
|
|
|
2228
2278
|
.page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }
|
|
2229
2279
|
.page-url { word-break: break-all; }
|
|
2230
2280
|
hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
|
|
2231
|
-
|
|
2281
|
+
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2282
|
+
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2283
|
+
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2284
|
+
</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>`;
|
|
2232
2285
|
const attachChromeHtmlEn = `<!DOCTYPE html>
|
|
2233
2286
|
<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>
|
|
2234
2287
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -2251,14 +2304,29 @@ section { width: 100%; max-width: 480px; }
|
|
|
2251
2304
|
h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
|
|
2252
2305
|
ol, ul { margin: 0; padding-left: 1.25rem; }
|
|
2253
2306
|
li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
|
|
2307
|
+
.url-row {
|
|
2308
|
+
display: flex; align-items: stretch; gap: 0;
|
|
2309
|
+
border-radius: 6px; border: 1px solid #30363d; overflow: hidden;
|
|
2310
|
+
}
|
|
2254
2311
|
.url-box {
|
|
2255
2312
|
font-family: monospace; font-size: 0.72rem;
|
|
2256
2313
|
word-break: break-all; opacity: 0.4;
|
|
2257
2314
|
background: #161b22; padding: 0.75rem 1rem;
|
|
2258
|
-
|
|
2315
|
+
flex: 1; cursor: pointer; border: none; border-radius: 0;
|
|
2259
2316
|
}
|
|
2317
|
+
.url-box:hover { opacity: 0.6; }
|
|
2318
|
+
.copy-btn {
|
|
2319
|
+
flex-shrink: 0; padding: 0.5rem 0.8rem;
|
|
2320
|
+
background: #21262d; border: none; border-left: 1px solid #30363d;
|
|
2321
|
+
color: #58a6ff; font-size: 0.75rem; cursor: pointer; white-space: nowrap;
|
|
2322
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
2323
|
+
}
|
|
2324
|
+
.copy-btn:hover { background: #30363d; }
|
|
2260
2325
|
hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
|
|
2261
|
-
|
|
2326
|
+
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2327
|
+
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2328
|
+
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2329
|
+
</style></head><body><h1>AIT Debug Session — QR Scan</h1>__LANG_SWITCHER__<p class="label">deployment: __SAFE_LABEL__</p><div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Open the Toss app.</li><li>Scan the QR code with your phone camera app.</li><li>Tap <strong>"Open in Toss"</strong> when the popup appears.</li><li>The mini-app opens and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Toss app does not open</strong> — check app version; scan with the system camera app (not the Toss in-app QR reader)</li><li><strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li><li><strong>TOTP gate Layer C is inactive</strong> — check that <code>AIT_DEBUG_TOTP_SECRET</code> is set on the relay server</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="Copy">Copy</button></div></section></body></html>`;
|
|
2262
2330
|
/** Map from Locale to the precompiled dashboard chrome string. */
|
|
2263
2331
|
const dashboardChromeByLocale = {
|
|
2264
2332
|
ko: dashboardChromeHtmlKo,
|
|
@@ -2276,6 +2344,24 @@ function escapeHtml(s) {
|
|
|
2276
2344
|
return s.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
2277
2345
|
}
|
|
2278
2346
|
/**
|
|
2347
|
+
* 현재 path+query에서 lang 파라미터만 교체한 ko/en 토글 링크를 생성한다.
|
|
2348
|
+
*
|
|
2349
|
+
* SECRET-HANDLING: u= (attachUrl, TOTP at= 캡슐 포함) 등 기존 query를 보존한다.
|
|
2350
|
+
* lang= 만 덮어쓴다. 링크 href에 at= 코드가 들어가는 건 의도된 전달 경로.
|
|
2351
|
+
*/
|
|
2352
|
+
function buildLangSwitcher(path, existingParams, locale, s) {
|
|
2353
|
+
function switcherHref(targetLang) {
|
|
2354
|
+
const p = new URLSearchParams(existingParams);
|
|
2355
|
+
p.set("lang", targetLang);
|
|
2356
|
+
return `${escapeHtml(path)}?${p.toString()}`;
|
|
2357
|
+
}
|
|
2358
|
+
const koLabel = escapeHtml(s("dashboard.lang.ko"));
|
|
2359
|
+
const enLabel = escapeHtml(s("dashboard.lang.en"));
|
|
2360
|
+
const koClass = locale === "ko" ? "active" : "";
|
|
2361
|
+
const enClass = locale === "en" ? "active" : "";
|
|
2362
|
+
return `<div class="lang-switcher"><a href="${switcherHref("ko")}" class="${koClass}">${koLabel}</a><a href="${switcherHref("en")}" class="${enClass}">${enLabel}</a></div>`;
|
|
2363
|
+
}
|
|
2364
|
+
/**
|
|
2279
2365
|
* Dashboard HTML — precompiled chrome에 per-request 동적 값을 채워 완성한다.
|
|
2280
2366
|
*
|
|
2281
2367
|
* 토큰 채우기 순서:
|
|
@@ -2295,14 +2381,17 @@ function escapeHtml(s) {
|
|
|
2295
2381
|
* - tunnel wssUrl은 "터널 연결됨" 상태 표시에서 UP/DOWN만 노출.
|
|
2296
2382
|
* wssUrl 값 자체는 dashboard HTML에 넣지 않는다.
|
|
2297
2383
|
*/
|
|
2298
|
-
function buildDashboardHtml(state, qrDataUrl, locale) {
|
|
2384
|
+
function buildDashboardHtml(state, qrDataUrl, locale, path = "/", params = new URLSearchParams()) {
|
|
2299
2385
|
const s = resolveLocaleStrings(locale);
|
|
2300
2386
|
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
2301
2387
|
const tunnelStatus = state.tunnel.up ? s("dashboard.tunnel.up") : s("dashboard.tunnel.down");
|
|
2302
2388
|
const tunnelClass = state.tunnel.up ? "status-up" : "status-down";
|
|
2303
2389
|
let attachSection;
|
|
2304
|
-
if (qrDataUrl && state.attachUrl)
|
|
2305
|
-
|
|
2390
|
+
if (qrDataUrl && state.attachUrl) {
|
|
2391
|
+
const safeAttachUrl = escapeHtml(state.attachUrl);
|
|
2392
|
+
const copyLabel = escapeHtml(s("dashboard.url.copy"));
|
|
2393
|
+
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>`;
|
|
2394
|
+
} else attachSection = `<p class="hint">${escapeHtml(s("dashboard.attach.hint"))}</p>`;
|
|
2306
2395
|
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) => {
|
|
2307
2396
|
return `<li><span class="page-id">${escapeHtml(p.id)}</span> <span class="page-url">${escapeHtml(p.url.slice(0, 120))}</span></li>`;
|
|
2308
2397
|
}).join("\n") : `<li class="empty">${escapeHtml(s("dashboard.pages.empty"))}</li>`}</ul></section>`;
|
|
@@ -2310,9 +2399,13 @@ function buildDashboardHtml(state, qrDataUrl, locale) {
|
|
|
2310
2399
|
tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
|
|
2311
2400
|
tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
|
|
2312
2401
|
pagesEmpty: JSON.stringify(s("dashboard.pages.empty")),
|
|
2313
|
-
attachHint: JSON.stringify(s("dashboard.attach.hint"))
|
|
2402
|
+
attachHint: JSON.stringify(s("dashboard.attach.hint")),
|
|
2403
|
+
copyLabel: JSON.stringify(s("dashboard.url.copy")),
|
|
2404
|
+
copiedLabel: JSON.stringify(s("dashboard.url.copied")),
|
|
2405
|
+
dashboardSurface: true
|
|
2314
2406
|
};
|
|
2315
|
-
const
|
|
2407
|
+
const langSwitcher = buildLangSwitcher(path, params, locale, s);
|
|
2408
|
+
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);
|
|
2316
2409
|
const sseScript = buildSseScript(sseStrings);
|
|
2317
2410
|
return filled.replace("</body>", `${sseScript}\n</body>`);
|
|
2318
2411
|
}
|
|
@@ -2323,9 +2416,24 @@ function buildDashboardHtml(state, qrDataUrl, locale) {
|
|
|
2323
2416
|
* client side: attachUrl은 DOM에 렌더링, wssUrl은 절대 렌더링하지 않는다.
|
|
2324
2417
|
* pages === null 이면 섹션을 건드리지 않는다 (#411).
|
|
2325
2418
|
*
|
|
2419
|
+
* 두 표면(dashboard / attach) 분기:
|
|
2420
|
+
* - dashboard (dashboardSurface=true): #attach-section innerHTML 전체 교체 방식 유지.
|
|
2421
|
+
* url-box도 innerHTML 재렌더 안에 포함되어 갱신됨.
|
|
2422
|
+
* - /attach (dashboardSurface=false): #attach-section의 img src만 교체하고,
|
|
2423
|
+
* url-box는 #url-box textContent만 갱신한다. (#attach-section에 url-box가 없으므로
|
|
2424
|
+
* innerHTML 교체 시 url-box가 새로 생겨 이중 표시되는 결함을 방지 — #458 결함 수정.)
|
|
2425
|
+
*
|
|
2426
|
+
* 복사 기능: 이벤트 위임으로 document에 단일 핸들러. innerHTML 재렌더 후에도 생존.
|
|
2427
|
+
* - .url-box 클릭 또는 .copy-btn 클릭 → 현재 #url-box textContent 복사.
|
|
2428
|
+
* - clipboard: navigator.clipboard.writeText → 실패/부재 시 textarea execCommand fallback.
|
|
2429
|
+
* - 피드백: 버튼 라벨이 COPIED_LABEL로 ~1.5초 전환 후 COPY_LABEL로 복귀.
|
|
2430
|
+
*
|
|
2326
2431
|
* 문자열 인자는 빌드타임에 ko/en 테이블에서 가져와 JSON.stringify로 이미 escape됨.
|
|
2432
|
+
*
|
|
2433
|
+
* SECRET-HANDLING: URL 값을 console.log 등으로 출력하지 않는다.
|
|
2327
2434
|
*/
|
|
2328
2435
|
function buildSseScript(strings) {
|
|
2436
|
+
const isDashboard = strings.dashboardSurface;
|
|
2329
2437
|
return `<script>
|
|
2330
2438
|
// SSE — /events 구독해 상태 자동 갱신. 빌드 파이프라인 없는 인라인 스크립트.
|
|
2331
2439
|
(function () {
|
|
@@ -2333,6 +2441,64 @@ function buildSseScript(strings) {
|
|
|
2333
2441
|
var TUNNEL_DOWN = ${strings.tunnelDown};
|
|
2334
2442
|
var PAGES_EMPTY = ${strings.pagesEmpty};
|
|
2335
2443
|
var ATTACH_HINT = ${strings.attachHint};
|
|
2444
|
+
var COPY_LABEL = ${strings.copyLabel};
|
|
2445
|
+
var COPIED_LABEL = ${strings.copiedLabel};
|
|
2446
|
+
|
|
2447
|
+
// ── 클립보드 복사 헬퍼 ────────────────────────────────────────────────
|
|
2448
|
+
function copyText(text) {
|
|
2449
|
+
if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
2450
|
+
return navigator.clipboard.writeText(text);
|
|
2451
|
+
}
|
|
2452
|
+
// fallback: textarea + execCommand
|
|
2453
|
+
return new Promise(function (resolve, reject) {
|
|
2454
|
+
var ta = document.createElement('textarea');
|
|
2455
|
+
ta.value = text;
|
|
2456
|
+
ta.style.position = 'fixed';
|
|
2457
|
+
ta.style.opacity = '0';
|
|
2458
|
+
document.body.appendChild(ta);
|
|
2459
|
+
ta.focus();
|
|
2460
|
+
ta.select();
|
|
2461
|
+
try {
|
|
2462
|
+
document.execCommand('copy') ? resolve() : reject(new Error('execCommand failed'));
|
|
2463
|
+
} catch (err) {
|
|
2464
|
+
reject(err);
|
|
2465
|
+
} finally {
|
|
2466
|
+
document.body.removeChild(ta);
|
|
2467
|
+
}
|
|
2468
|
+
});
|
|
2469
|
+
}
|
|
2470
|
+
|
|
2471
|
+
// ── 복사 피드백 ───────────────────────────────────────────────────────
|
|
2472
|
+
var copyTimer = null;
|
|
2473
|
+
function triggerCopy() {
|
|
2474
|
+
var urlBox = document.getElementById('url-box');
|
|
2475
|
+
if (!urlBox) return;
|
|
2476
|
+
var text = urlBox.textContent || '';
|
|
2477
|
+
if (!text) return;
|
|
2478
|
+
copyText(text).then(function () {
|
|
2479
|
+
var btn = document.getElementById('copy-btn');
|
|
2480
|
+
if (btn) {
|
|
2481
|
+
btn.textContent = COPIED_LABEL;
|
|
2482
|
+
if (copyTimer) clearTimeout(copyTimer);
|
|
2483
|
+
copyTimer = setTimeout(function () {
|
|
2484
|
+
btn.textContent = COPY_LABEL;
|
|
2485
|
+
copyTimer = null;
|
|
2486
|
+
}, 1500);
|
|
2487
|
+
}
|
|
2488
|
+
}).catch(function () { /* 복사 실패 시 조용히 무시 */ });
|
|
2489
|
+
}
|
|
2490
|
+
|
|
2491
|
+
// ── 이벤트 위임 — document 레벨에서 단일 핸들러 (innerHTML 재렌더 후에도 생존) ──
|
|
2492
|
+
document.addEventListener('click', function (e) {
|
|
2493
|
+
var target = e.target;
|
|
2494
|
+
if (!target) return;
|
|
2495
|
+
// .copy-btn 또는 .url-box 클릭 시 복사
|
|
2496
|
+
if (target.closest && (target.closest('.copy-btn') || target.closest('.url-box'))) {
|
|
2497
|
+
triggerCopy();
|
|
2498
|
+
}
|
|
2499
|
+
});
|
|
2500
|
+
|
|
2501
|
+
// ── SSE 구독 ──────────────────────────────────────────────────────────
|
|
2336
2502
|
var src = new EventSource('/events');
|
|
2337
2503
|
src.onmessage = function (e) {
|
|
2338
2504
|
try {
|
|
@@ -2360,23 +2526,37 @@ function buildSseScript(strings) {
|
|
|
2360
2526
|
}
|
|
2361
2527
|
}
|
|
2362
2528
|
}
|
|
2363
|
-
// attachUrl QR
|
|
2529
|
+
// attachUrl QR + url-box 갱신
|
|
2530
|
+
// SECRET-HANDLING: URL 값을 로그로 출력하지 않는다.
|
|
2364
2531
|
var sec = document.getElementById('attach-section');
|
|
2365
2532
|
if (sec) {
|
|
2366
2533
|
if (s.attachUrl) {
|
|
2367
|
-
// QR은 서버에서 새로 렌더한 /qr.png?u= 로 img src 교체.
|
|
2368
|
-
// TOTP at= 코드는 attachUrl 안에 캡슐화 — 별도 노출 없음.
|
|
2369
|
-
// wssUrl은 절대 DOM에 렌더하지 않는다 (SECRET-HANDLING).
|
|
2370
2534
|
var encoded = encodeURIComponent(s.attachUrl);
|
|
2371
2535
|
var safeUrl = String(s.attachUrl).slice(0, 2000).replace(/[<>&"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });
|
|
2536
|
+
${isDashboard ? `// dashboard: #attach-section innerHTML 전체 교체 (img + url-row).
|
|
2537
|
+
// url-box id="url-box" 를 포함해 복사 핸들러가 계속 동작함.
|
|
2372
2538
|
sec.innerHTML =
|
|
2373
2539
|
'<img class="qr" src="/qr.png?u=' + encoded + '" alt="attach QR" />' +
|
|
2374
|
-
'<
|
|
2540
|
+
'<div class=\\"url-row\\">' +
|
|
2541
|
+
'<p class=\\"url-box\\" id=\\"url-box\\">' + safeUrl + '</p>' +
|
|
2542
|
+
'<button class=\\"copy-btn\\" id=\\"copy-btn\\" type=\\"button\\" aria-label=\\"' + COPY_LABEL + '\\">' + COPY_LABEL + '</button>' +
|
|
2543
|
+
'</div>';` : `// /attach: img src만 교체 — url-box는 별도 #url-section에서 관리해 이중 표시 방지(#458).
|
|
2544
|
+
// QR img src 교체: img가 있으면 src만 갱신, 없으면 img 요소 생성.
|
|
2545
|
+
var img = sec.querySelector('img.qr');
|
|
2546
|
+
if (img) {
|
|
2547
|
+
img.src = '/qr.png?u=' + encoded;
|
|
2548
|
+
} else {
|
|
2549
|
+
sec.innerHTML = '<img class=\\"qr\\" src=\\"/qr.png?u=' + encoded + '\\" alt=\\"attach QR\\" />';
|
|
2550
|
+
}
|
|
2551
|
+
// url-box textContent만 갱신 (innerHTML 교체하지 않아 복사 버튼/핸들러 생존).
|
|
2552
|
+
var ub = document.getElementById('url-box');
|
|
2553
|
+
if (ub) ub.textContent = s.attachUrl;`}
|
|
2375
2554
|
} else {
|
|
2376
|
-
sec.innerHTML = '<p class
|
|
2555
|
+
${isDashboard ? `sec.innerHTML = '<p class=\\"hint\\">' + ATTACH_HINT + '</p>';` : `// /attach에서 hint가 필요한 경우는 없으나 방어 처리.
|
|
2556
|
+
sec.innerHTML = '<p class=\\"hint\\">' + ATTACH_HINT + '</p>';`}
|
|
2377
2557
|
}
|
|
2378
2558
|
}
|
|
2379
|
-
// 갱신 시각
|
|
2559
|
+
// 갱신 시각 (dashboard만 #updated 요소 있음)
|
|
2380
2560
|
var upd = document.getElementById('updated');
|
|
2381
2561
|
if (upd) upd.textContent = upd.textContent.replace(/[^ ]+$/, new Date().toISOString());
|
|
2382
2562
|
} catch (_) { /* 파싱 오류 무시 */ }
|
|
@@ -2401,14 +2581,18 @@ function buildSseScript(strings) {
|
|
|
2401
2581
|
*
|
|
2402
2582
|
* SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.
|
|
2403
2583
|
*/
|
|
2404
|
-
function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale) {
|
|
2584
|
+
function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale, path = "/attach", params = new URLSearchParams()) {
|
|
2405
2585
|
const s = resolveLocaleStrings(locale);
|
|
2406
|
-
const
|
|
2586
|
+
const langSwitcher = buildLangSwitcher(path, params, locale, s);
|
|
2587
|
+
const filled = attachChromeByLocale[locale].replaceAll("__LANG_SWITCHER__", langSwitcher).replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl);
|
|
2407
2588
|
const sseScript = buildSseScript({
|
|
2408
2589
|
tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
|
|
2409
2590
|
tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
|
|
2410
2591
|
pagesEmpty: JSON.stringify(s("dashboard.pages.empty")),
|
|
2411
|
-
attachHint: JSON.stringify(s("dashboard.attach.hint"))
|
|
2592
|
+
attachHint: JSON.stringify(s("dashboard.attach.hint")),
|
|
2593
|
+
copyLabel: JSON.stringify(s("dashboard.url.copy")),
|
|
2594
|
+
copiedLabel: JSON.stringify(s("dashboard.url.copied")),
|
|
2595
|
+
dashboardSurface: false
|
|
2412
2596
|
});
|
|
2413
2597
|
return filled.replace("</body>", `${sseScript}\n</body>`);
|
|
2414
2598
|
}
|
|
@@ -2438,7 +2622,8 @@ async function startQrHttpServer(getDashboardState) {
|
|
|
2438
2622
|
const server = createServer(async (req, res) => {
|
|
2439
2623
|
const [path, query = ""] = (req.url ?? "/").split("?", 2);
|
|
2440
2624
|
const params = new URLSearchParams(query ?? "");
|
|
2441
|
-
const
|
|
2625
|
+
const langParam = params.get("lang");
|
|
2626
|
+
const locale = langParam === "ko" || langParam === "en" ? langParam : parseAcceptLanguage(req.headers["accept-language"]);
|
|
2442
2627
|
if (path === "/") {
|
|
2443
2628
|
if (!getDashboardState) {
|
|
2444
2629
|
res.writeHead(204, { "Content-Type": "text/plain; charset=utf-8" });
|
|
@@ -2453,7 +2638,7 @@ async function startQrHttpServer(getDashboardState) {
|
|
|
2453
2638
|
errorCorrectionLevel: "M"
|
|
2454
2639
|
});
|
|
2455
2640
|
} catch {}
|
|
2456
|
-
const html = buildDashboardHtml(state, qrDataUrl, locale);
|
|
2641
|
+
const html = buildDashboardHtml(state, qrDataUrl, locale, path, params);
|
|
2457
2642
|
res.writeHead(200, {
|
|
2458
2643
|
"Content-Type": "text/html; charset=utf-8",
|
|
2459
2644
|
"Cache-Control": "no-store"
|
|
@@ -2500,7 +2685,7 @@ async function startQrHttpServer(getDashboardState) {
|
|
|
2500
2685
|
type: "image/png",
|
|
2501
2686
|
errorCorrectionLevel: "M"
|
|
2502
2687
|
}).then((dataUrl) => {
|
|
2503
|
-
const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale);
|
|
2688
|
+
const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale, path, params);
|
|
2504
2689
|
res.writeHead(200, {
|
|
2505
2690
|
"Content-Type": "text/html; charset=utf-8",
|
|
2506
2691
|
"Cache-Control": "no-store"
|
|
@@ -3938,7 +4123,7 @@ async function readMcpSdkVersion() {
|
|
|
3938
4123
|
* some test environments that skip the build step).
|
|
3939
4124
|
*/
|
|
3940
4125
|
function readDevtoolsVersion() {
|
|
3941
|
-
return "0.1.
|
|
4126
|
+
return "0.1.67";
|
|
3942
4127
|
}
|
|
3943
4128
|
/**
|
|
3944
4129
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -4426,7 +4611,7 @@ function createDebugServer(deps) {
|
|
|
4426
4611
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
4427
4612
|
const server = new Server({
|
|
4428
4613
|
name: "ait-debug",
|
|
4429
|
-
version: "0.1.
|
|
4614
|
+
version: "0.1.67"
|
|
4430
4615
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
4431
4616
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
4432
4617
|
const conn = router.active;
|
|
@@ -6297,7 +6482,7 @@ function createDevServer(deps = {}) {
|
|
|
6297
6482
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
6298
6483
|
const server = new Server({
|
|
6299
6484
|
name: "ait-devtools",
|
|
6300
|
-
version: "0.1.
|
|
6485
|
+
version: "0.1.67"
|
|
6301
6486
|
}, { capabilities: { tools: {} } });
|
|
6302
6487
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
6303
6488
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|