@ait-co/devtools 0.1.58 → 0.1.60

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/dist/deeplink-BONXxWEO.cjs +44 -0
  2. package/dist/deeplink-BONXxWEO.cjs.map +1 -0
  3. package/dist/deeplink-CCGiyoHq.cjs +44 -0
  4. package/dist/deeplink-CCGiyoHq.cjs.map +1 -0
  5. package/dist/deeplink-CaO6hZVG.js +44 -0
  6. package/dist/deeplink-CaO6hZVG.js.map +1 -0
  7. package/dist/deeplink-Cqli4qzm.js +44 -0
  8. package/dist/deeplink-Cqli4qzm.js.map +1 -0
  9. package/dist/devtools-opener-BbUXBzgA.js +65 -0
  10. package/dist/devtools-opener-BbUXBzgA.js.map +1 -0
  11. package/dist/devtools-opener-Bp671YXu.cjs +62 -0
  12. package/dist/devtools-opener-Bp671YXu.cjs.map +1 -0
  13. package/dist/devtools-opener-D84kZFtR.js +65 -0
  14. package/dist/devtools-opener-D84kZFtR.js.map +1 -0
  15. package/dist/devtools-opener-h6A-UjzC.cjs +62 -0
  16. package/dist/devtools-opener-h6A-UjzC.cjs.map +1 -0
  17. package/dist/mcp/cli.js +862 -403
  18. package/dist/mcp/cli.js.map +1 -1
  19. package/dist/mcp/server.js +5 -1
  20. package/dist/mcp/server.js.map +1 -1
  21. package/dist/panel/index.d.ts +15 -9
  22. package/dist/panel/index.d.ts.map +1 -1
  23. package/dist/panel/index.js +27705 -1965
  24. package/dist/panel/index.js.map +1 -1
  25. package/dist/qr-http-server-Byk0Yjk_.cjs +901 -0
  26. package/dist/qr-http-server-Byk0Yjk_.cjs.map +1 -0
  27. package/dist/qr-http-server-D_Aj5Vq6.cjs +901 -0
  28. package/dist/qr-http-server-D_Aj5Vq6.cjs.map +1 -0
  29. package/dist/qr-http-server-N4mX8GaC.js +901 -0
  30. package/dist/qr-http-server-N4mX8GaC.js.map +1 -0
  31. package/dist/qr-http-server-kYvmlXlg.js +901 -0
  32. package/dist/qr-http-server-kYvmlXlg.js.map +1 -0
  33. package/dist/relay-secret-store-5A7_7zOp.js +111 -0
  34. package/dist/relay-secret-store-5A7_7zOp.js.map +1 -0
  35. package/dist/{relay-secret-store-DqyUoeXy.js → relay-secret-store-C4QQN5NA.js} +3 -3
  36. package/dist/{relay-secret-store-DqyUoeXy.js.map → relay-secret-store-C4QQN5NA.js.map} +1 -1
  37. package/dist/{relay-secret-store-DnTNl-9z.cjs → relay-secret-store-CLkF8Pa0.cjs} +3 -2
  38. package/dist/{relay-secret-store-DnTNl-9z.cjs.map → relay-secret-store-CLkF8Pa0.cjs.map} +1 -1
  39. package/dist/relay-url-store-COG2dSql.cjs +113 -0
  40. package/dist/relay-url-store-COG2dSql.cjs.map +1 -0
  41. package/dist/relay-url-store-WKfo0VQV.js +112 -0
  42. package/dist/relay-url-store-WKfo0VQV.js.map +1 -0
  43. package/dist/relay-url-store-qaoe0zOD.js +118 -0
  44. package/dist/relay-url-store-qaoe0zOD.js.map +1 -0
  45. package/dist/totp-86i_CNqh.js +3 -0
  46. package/dist/{totp-D0a8VwoR.js → totp-BIrJHsQn.js} +1 -1
  47. package/dist/{totp-D0a8VwoR.js.map → totp-BIrJHsQn.js.map} +1 -1
  48. package/dist/{totp-BkP5yU2K.js → totp-BjtKFt88.js} +2 -2
  49. package/dist/{totp-BkP5yU2K.js.map → totp-BjtKFt88.js.map} +1 -1
  50. package/dist/totp-BxtxuEt4.js +64 -0
  51. package/dist/totp-BxtxuEt4.js.map +1 -0
  52. package/dist/totp-D9rndqg_.cjs +64 -0
  53. package/dist/totp-D9rndqg_.cjs.map +1 -0
  54. package/dist/{totp-DLgGbySX.cjs → totp-DA8vjAi7.cjs} +2 -1
  55. package/dist/{totp-DLgGbySX.cjs.map → totp-DA8vjAi7.cjs.map} +1 -1
  56. package/dist/{tunnel-nKYPtc-g.cjs → tunnel-GieyWa22.cjs} +114 -3
  57. package/dist/tunnel-GieyWa22.cjs.map +1 -0
  58. package/dist/{tunnel-CI61NvPI.js → tunnel-JuZ5_Pci.js} +114 -4
  59. package/dist/tunnel-JuZ5_Pci.js.map +1 -0
  60. package/dist/unplugin/index.cjs +116 -4
  61. package/dist/unplugin/index.cjs.map +1 -1
  62. package/dist/unplugin/index.d.cts +20 -1
  63. package/dist/unplugin/index.d.cts.map +1 -1
  64. package/dist/unplugin/index.d.ts +20 -1
  65. package/dist/unplugin/index.d.ts.map +1 -1
  66. package/dist/unplugin/index.js +116 -5
  67. package/dist/unplugin/index.js.map +1 -1
  68. package/dist/unplugin/tunnel.cjs +114 -2
  69. package/dist/unplugin/tunnel.cjs.map +1 -1
  70. package/dist/unplugin/tunnel.d.cts +62 -1
  71. package/dist/unplugin/tunnel.d.cts.map +1 -1
  72. package/dist/unplugin/tunnel.d.ts +62 -1
  73. package/dist/unplugin/tunnel.d.ts.map +1 -1
  74. package/dist/unplugin/tunnel.js +113 -3
  75. package/dist/unplugin/tunnel.js.map +1 -1
  76. package/package.json +11 -3
  77. package/dist/totp-CQFmgOhM.js +0 -3
  78. package/dist/tunnel-CI61NvPI.js.map +0 -1
  79. package/dist/tunnel-nKYPtc-g.cjs.map +0 -1
@@ -0,0 +1,901 @@
1
+ let node_http = require("node:http");
2
+ //#region src/i18n/en.ts
3
+ const en = {
4
+ "panel.title": "AIT DevTools",
5
+ "panel.toggle.title": "AIT DevTools",
6
+ "panel.close": "Close",
7
+ "panel.editMode.on": "EDIT",
8
+ "panel.editMode.off": "READ-ONLY",
9
+ "panel.editMode.toggleTitle": "Toggle panel edit mode",
10
+ "panel.tabError": "Error rendering \"{tab}\" tab.",
11
+ "panel.tab.env": "Environment",
12
+ "panel.tab.presets": "Presets",
13
+ "panel.tab.viewport": "Viewport",
14
+ "panel.tab.permissions": "Permissions",
15
+ "panel.tab.notifications": "Notifications",
16
+ "panel.tab.location": "Location",
17
+ "panel.tab.device": "Device",
18
+ "panel.tab.iap": "IAP",
19
+ "panel.tab.ads": "Ads",
20
+ "panel.tab.events": "Events",
21
+ "panel.tab.analytics": "Analytics",
22
+ "panel.tab.storage": "Storage",
23
+ "common.readOnly": "Read-only — mock responses are controlled at build time.",
24
+ "toast.consent.title": "Send anonymous usage stats?",
25
+ "toast.consent.body": "We collect anonymous events only, to improve the tool. You can turn this off anytime in the Environment tab.",
26
+ "toast.consent.learnMore": "Learn more",
27
+ "toast.consent.accept": "Yes, send",
28
+ "toast.consent.deny": "No, thanks",
29
+ "env.section.platform": "Platform",
30
+ "env.row.os": "OS",
31
+ "env.row.appVersion": "App Version",
32
+ "env.row.environment": "Environment",
33
+ "env.row.locale": "Locale",
34
+ "env.section.network": "Network",
35
+ "env.row.networkStatus": "Status",
36
+ "env.section.safeArea": "Safe Area Insets",
37
+ "env.row.safeArea.top": "Top",
38
+ "env.row.safeArea.bottom": "Bottom",
39
+ "env.section.navigation": "Navigation",
40
+ "env.row.iosSwipeGesture": "iOS swipe-back",
41
+ "env.value.iosSwipeGesture.unset": "not called",
42
+ "env.value.iosSwipeGesture.enabled": "enabled",
43
+ "env.value.iosSwipeGesture.disabled": "disabled",
44
+ "env.hint.iosSwipeGesture": "Last value passed to setIosSwipeGestureEnabled. Switching Environment to toss lets a toss-gated guard toggle this.",
45
+ "env.telemetry.section": "Telemetry",
46
+ "env.telemetry.t0Row": "Anonymous usage signal (Tier 0)",
47
+ "env.telemetry.t0On": "On",
48
+ "env.telemetry.t0Off": "Off",
49
+ "env.telemetry.t0TurnOn": "Turn on",
50
+ "env.telemetry.t0TurnOff": "Turn off",
51
+ "env.telemetry.t0Desc": "Version + date only, no PII. Once per day. Helps improve the package.",
52
+ "env.telemetry.row": "Extended telemetry (Tier 1)",
53
+ "env.telemetry.on": "On",
54
+ "env.telemetry.off": "Off",
55
+ "env.telemetry.turnOn": "Turn on",
56
+ "env.telemetry.turnOff": "Turn off",
57
+ "env.telemetry.anonIdLabel": "anon_id: {value}",
58
+ "env.telemetry.anonIdNotSet": "(not yet set)",
59
+ "env.telemetry.anonIdCopyTitle": "Click to copy full anon_id",
60
+ "env.telemetry.deleteBtn": "Delete my data",
61
+ "env.telemetry.deleting": "Deleting…",
62
+ "env.telemetry.deleted": "Deleted",
63
+ "env.telemetry.deleteFailedRetry": "Delete failed (please retry)",
64
+ "env.telemetry.deleteFailed": "Delete failed",
65
+ "env.telemetry.privacyLink": "Privacy policy →",
66
+ "env.section.language": "Language",
67
+ "env.language.row": "Language",
68
+ "env.language.ko": "한국어",
69
+ "env.language.en": "English",
70
+ "permissions.section.device": "Device Permissions",
71
+ "location.section.current": "Current Location",
72
+ "location.row.latitude": "Latitude",
73
+ "location.row.longitude": "Longitude",
74
+ "location.row.accuracy": "Accuracy",
75
+ "device.section.modes": "Device API Modes",
76
+ "device.row.camera": "Camera",
77
+ "device.row.photos": "Photos",
78
+ "device.row.location": "Location",
79
+ "device.row.network": "Network",
80
+ "device.row.clipboard": "Clipboard",
81
+ "device.section.mockImages": "Mock Images ({count})",
82
+ "device.btn.add": "+ Add",
83
+ "device.btn.useDefaults": "Use defaults",
84
+ "device.btn.clear": "Clear",
85
+ "device.prompt.camera.title": "Camera Prompt — Select an image",
86
+ "device.prompt.photos.title": "Photos Prompt — Select images",
87
+ "device.prompt.location.title": "Location Prompt — Enter coordinates",
88
+ "device.prompt.locationUpdate.title": "Location Update — Send coordinates",
89
+ "device.prompt.fallbackTitle": "Prompt: {type}",
90
+ "device.prompt.label.lat": "Lat",
91
+ "device.prompt.label.lng": "Lng",
92
+ "device.prompt.send": "Send",
93
+ "device.prompt.cancel": "Cancel",
94
+ "device.section.haptic": "Haptic",
95
+ "device.haptic.lastCall": "Last haptic",
96
+ "device.haptic.noneYet": "(none yet)",
97
+ "device.haptic.trigger": "Trigger haptic",
98
+ "viewport.section.device": "Device",
99
+ "viewport.row.preset": "Preset",
100
+ "viewport.row.orientation": "Orientation",
101
+ "viewport.row.notchSide": "Notch side",
102
+ "viewport.section.custom": "Custom size",
103
+ "viewport.row.width": "Width (px)",
104
+ "viewport.row.height": "Height (px)",
105
+ "viewport.section.appearance": "Appearance",
106
+ "viewport.row.showFrame": "Show frame",
107
+ "viewport.row.showAitNavBar": "Show Apps in Toss nav bar",
108
+ "viewport.row.navBarType": "Nav bar type",
109
+ "viewport.status.noConstraint": "No viewport constraint — body fills the window.",
110
+ "viewport.status.cssPhysical": "CSS / physical",
111
+ "viewport.status.safeArea": "Safe area",
112
+ "viewport.status.aitNavBar": "AIT nav bar",
113
+ "viewport.status.aitNavBarValue": "{height}px → SafeArea top · {type}",
114
+ "viewport.orientation.autoSuffix": "{orient} (auto)",
115
+ "iap.section.simulator": "IAP Simulator",
116
+ "iap.row.nextResult": "Next Purchase Result",
117
+ "iap.section.tossPay": "TossPay",
118
+ "iap.row.tossPayResult": "Next Payment Result",
119
+ "iap.section.pending": "Pending Orders ({count})",
120
+ "iap.empty.pending": "(no pending orders)",
121
+ "iap.section.completed": "Completed Orders ({count})",
122
+ "iap.empty.completed": "(no completed orders)",
123
+ "iap.btn.complete": "Complete",
124
+ "iap.label.pending": "PENDING",
125
+ "events.section.navigation": "Navigation Events",
126
+ "events.btn.triggerBack": "Trigger Back Event",
127
+ "events.btn.triggerHome": "Trigger Home Event",
128
+ "events.section.login": "Login",
129
+ "events.row.loggedIn": "Logged In",
130
+ "events.row.tossLoginIntegrated": "Toss Login Integrated",
131
+ "analytics.section.log": "Analytics Log ({count})",
132
+ "analytics.btn.clear": "Clear",
133
+ "analytics.calls.section": "SDK Calls ({count})",
134
+ "analytics.calls.btn.clear": "Clear",
135
+ "analytics.calls.empty": "(no SDK calls yet)",
136
+ "storage.section.title": "Storage ({count} items)",
137
+ "storage.btn.clearAll": "Clear All",
138
+ "storage.empty": "No items in storage",
139
+ "presets.section.builtIn": "Built-in scenarios",
140
+ "presets.section.saved": "Saved presets ({count})",
141
+ "presets.section.save": "Save",
142
+ "presets.save.description": "Capture network / permissions / auth / IAP / ads / payment slices.",
143
+ "presets.btn.saveCurrent": "Save current as preset",
144
+ "presets.btn.apply": "Apply",
145
+ "presets.btn.reApply": "Re-apply",
146
+ "presets.btn.delete": "Delete",
147
+ "presets.empty.saved": "No saved presets yet.",
148
+ "presets.empty.builtIn": "No built-in presets.",
149
+ "presets.prompt.label": "Preset label?",
150
+ "presets.confirm.delete": "Delete preset \"{label}\"?",
151
+ "ads.section.state": "Ads State",
152
+ "ads.row.isLoaded": "isLoaded",
153
+ "ads.row.forceNoFill": "Force \"no fill\"",
154
+ "ads.empty.events": "No events yet",
155
+ "ads.section.googleAdMob": "GoogleAdMob",
156
+ "ads.section.tossAds": "TossAds",
157
+ "ads.section.fullScreenAd": "FullScreenAd",
158
+ "ads.btn.load": "Load",
159
+ "ads.btn.show": "Show",
160
+ "ads.section.tossAdsBanner": "TossAds Banner",
161
+ "ads.row.rewardUnitType": "Reward unit type",
162
+ "ads.row.rewardAmount": "Reward amount",
163
+ "ads.btn.render": "Render",
164
+ "ads.btn.noFill": "No-fill",
165
+ "ads.btn.click": "Click",
166
+ "ads.btn.destroy": "Destroy",
167
+ "notifications.section.title": "requestNotificationAgreement",
168
+ "notifications.option.newAgreement": "newAgreement (first-time agree)",
169
+ "notifications.option.alreadyAgreed": "alreadyAgreed (already opted-in)",
170
+ "notifications.option.agreementRejected": "agreementRejected (user declined)",
171
+ "dashboard.title": "AIT Debug Dashboard",
172
+ "dashboard.updated": "Last updated: {ts}",
173
+ "dashboard.tunnel.section": "Tunnel status",
174
+ "dashboard.tunnel.up": "Connected",
175
+ "dashboard.tunnel.down": "Disconnected",
176
+ "dashboard.attach.section": "Attach QR",
177
+ "dashboard.attach.hint": "Call the build_attach_url MCP tool to show the QR here.",
178
+ "dashboard.pages.section": "Connected Pages",
179
+ "dashboard.pages.empty": "No attached pages",
180
+ "attach.title": "AIT Debug Session — QR Scan",
181
+ "attach.deployment": "deployment: {label}",
182
+ "attach.steps.section": "How to scan",
183
+ "attach.step1": "Open the Toss app.",
184
+ "attach.step2": "Scan the QR code with your phone camera app.",
185
+ "attach.step3": "Tap <strong>\"Open in Toss\"</strong> when the popup appears.",
186
+ "attach.step4": "The mini-app opens and the debug session attaches automatically.",
187
+ "attach.faq.section": "Troubleshooting checklist",
188
+ "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)",
189
+ "attach.faq.prepare": "<strong>Mini-app stuck in PREPARE state</strong> — verify the deep-link has a <code>_deploymentId</code> parameter",
190
+ "attach.faq.chii": "<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import",
191
+ "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",
192
+ "attach.url.section": "URL (fallback)",
193
+ "launcher.title": "AITC DevTools Launcher",
194
+ "launcher.description": "Scan the terminal QR code or paste the tunnel URL.",
195
+ "launcher.installCta": "Install launcher to your phone",
196
+ "launcher.urlPlaceholder": "https://example.trycloudflare.com",
197
+ "launcher.openBtn": "Open",
198
+ "launcher.scanBtn": "Scan QR with camera",
199
+ "launcher.rescanBtn": "Rescan",
200
+ "launcher.noCamera": "No camera available — paste the URL instead.",
201
+ "launcher.cameraError": "Could not access the camera — paste the URL instead.",
202
+ "launcher.invalidUrlHttps": "Enter a valid https:// URL (the tunnel URL from your terminal).",
203
+ "launcher.invalidUrl": "Enter a valid http(s):// URL."
204
+ };
205
+ //#endregion
206
+ //#region src/i18n/index.ts
207
+ /**
208
+ * Vanilla TS i18n for the floating DevTools panel.
209
+ *
210
+ * Public surface:
211
+ * - `t(key, vars?)` — look up a UI string, with `{name}` placeholder
212
+ * interpolation. Falls back to the key itself if a translation is missing.
213
+ * - `getLocale()` / `setLocale(locale)` — read/persist the active locale.
214
+ * `setLocale` dispatches `__ait:localechange` so the panel can remount.
215
+ * - `detectLocale()` — first-run heuristic from `navigator.language`.
216
+ *
217
+ * `ko` is the source of truth (keys are typed from it). `en` is also a full
218
+ * `Record<StringKey, string>` (devtools is developer-facing, en is a real
219
+ * audience). The `Partial` lookup table preserves the runtime `?? key` safety
220
+ * net even though we ship complete catalogs today.
221
+ */
222
+ const tables = {
223
+ ko: {
224
+ "panel.title": "AIT DevTools",
225
+ "panel.toggle.title": "AIT DevTools",
226
+ "panel.close": "Close",
227
+ "panel.editMode.on": "EDIT",
228
+ "panel.editMode.off": "READ-ONLY",
229
+ "panel.editMode.toggleTitle": "패널 편집 모드 전환",
230
+ "panel.tabError": "\"{tab}\" 탭 렌더링 중 오류가 발생했습니다.",
231
+ "panel.tab.env": "Environment",
232
+ "panel.tab.presets": "Presets",
233
+ "panel.tab.viewport": "Viewport",
234
+ "panel.tab.permissions": "Permissions",
235
+ "panel.tab.notifications": "Notifications",
236
+ "panel.tab.location": "Location",
237
+ "panel.tab.device": "Device",
238
+ "panel.tab.iap": "IAP",
239
+ "panel.tab.ads": "Ads",
240
+ "panel.tab.events": "Events",
241
+ "panel.tab.analytics": "Analytics",
242
+ "panel.tab.storage": "Storage",
243
+ "common.readOnly": "읽기 전용 — mock 응답은 빌드 타임에 고정됩니다.",
244
+ "toast.consent.title": "익명 사용 통계를 보낼까요?",
245
+ "toast.consent.body": "도구 개선을 위해 익명 이벤트만 수집해요. 언제든 환경 탭에서 끌 수 있어요.",
246
+ "toast.consent.learnMore": "더 알아보기",
247
+ "toast.consent.accept": "네, 보낼게요",
248
+ "toast.consent.deny": "아니요",
249
+ "env.section.platform": "Platform",
250
+ "env.row.os": "OS",
251
+ "env.row.appVersion": "App Version",
252
+ "env.row.environment": "Environment",
253
+ "env.row.locale": "Locale",
254
+ "env.section.network": "Network",
255
+ "env.row.networkStatus": "Status",
256
+ "env.section.safeArea": "Safe Area Insets",
257
+ "env.row.safeArea.top": "Top",
258
+ "env.row.safeArea.bottom": "Bottom",
259
+ "env.section.navigation": "Navigation",
260
+ "env.row.iosSwipeGesture": "iOS swipe-back",
261
+ "env.value.iosSwipeGesture.unset": "미호출",
262
+ "env.value.iosSwipeGesture.enabled": "enabled",
263
+ "env.value.iosSwipeGesture.disabled": "disabled",
264
+ "env.hint.iosSwipeGesture": "setIosSwipeGestureEnabled의 마지막 호출값. Environment를 toss로 바꾸면 toss-gated 가드가 이 값을 토글합니다.",
265
+ "env.telemetry.section": "Telemetry",
266
+ "env.telemetry.t0Row": "익명 사용 신호 (Tier 0)",
267
+ "env.telemetry.t0On": "On",
268
+ "env.telemetry.t0Off": "Off",
269
+ "env.telemetry.t0TurnOn": "Turn on",
270
+ "env.telemetry.t0TurnOff": "Turn off",
271
+ "env.telemetry.t0Desc": "버전·날짜만 수집, PII 없음. 하루 1회. 패키지 개선에 사용됩니다.",
272
+ "env.telemetry.row": "확장 텔레메트리 (Tier 1)",
273
+ "env.telemetry.on": "On",
274
+ "env.telemetry.off": "Off",
275
+ "env.telemetry.turnOn": "Turn on",
276
+ "env.telemetry.turnOff": "Turn off",
277
+ "env.telemetry.anonIdLabel": "anon_id: {value}",
278
+ "env.telemetry.anonIdNotSet": "(not yet set)",
279
+ "env.telemetry.anonIdCopyTitle": "전체 anon_id 복사",
280
+ "env.telemetry.deleteBtn": "내 데이터 삭제",
281
+ "env.telemetry.deleting": "삭제 중…",
282
+ "env.telemetry.deleted": "삭제 완료",
283
+ "env.telemetry.deleteFailedRetry": "삭제 실패 (다시 시도해주세요)",
284
+ "env.telemetry.deleteFailed": "삭제 실패",
285
+ "env.telemetry.privacyLink": "개인정보 처리방침 →",
286
+ "env.section.language": "Language",
287
+ "env.language.row": "Language",
288
+ "env.language.ko": "한국어",
289
+ "env.language.en": "English",
290
+ "permissions.section.device": "Device Permissions",
291
+ "location.section.current": "Current Location",
292
+ "location.row.latitude": "Latitude",
293
+ "location.row.longitude": "Longitude",
294
+ "location.row.accuracy": "Accuracy",
295
+ "device.section.modes": "Device API Modes",
296
+ "device.row.camera": "Camera",
297
+ "device.row.photos": "Photos",
298
+ "device.row.location": "Location",
299
+ "device.row.network": "Network",
300
+ "device.row.clipboard": "Clipboard",
301
+ "device.section.mockImages": "Mock Images ({count})",
302
+ "device.btn.add": "+ Add",
303
+ "device.btn.useDefaults": "Use defaults",
304
+ "device.btn.clear": "Clear",
305
+ "device.prompt.camera.title": "Camera Prompt — 이미지를 선택하세요",
306
+ "device.prompt.photos.title": "Photos Prompt — 이미지를 선택하세요",
307
+ "device.prompt.location.title": "Location Prompt — 좌표 입력",
308
+ "device.prompt.locationUpdate.title": "Location Update — 좌표 전송",
309
+ "device.prompt.fallbackTitle": "Prompt: {type}",
310
+ "device.prompt.label.lat": "Lat",
311
+ "device.prompt.label.lng": "Lng",
312
+ "device.prompt.send": "Send",
313
+ "device.prompt.cancel": "Cancel",
314
+ "device.section.haptic": "Haptic",
315
+ "device.haptic.lastCall": "마지막 haptic",
316
+ "device.haptic.noneYet": "(아직 없음)",
317
+ "device.haptic.trigger": "Haptic 트리거",
318
+ "viewport.section.device": "Device",
319
+ "viewport.row.preset": "Preset",
320
+ "viewport.row.orientation": "Orientation",
321
+ "viewport.row.notchSide": "Notch side",
322
+ "viewport.section.custom": "Custom size",
323
+ "viewport.row.width": "Width (px)",
324
+ "viewport.row.height": "Height (px)",
325
+ "viewport.section.appearance": "Appearance",
326
+ "viewport.row.showFrame": "Show frame",
327
+ "viewport.row.showAitNavBar": "Apps in Toss 내비게이션 바 표시",
328
+ "viewport.row.navBarType": "Nav bar type",
329
+ "viewport.status.noConstraint": "뷰포트 제약 없음 — body가 창을 가득 채웁니다.",
330
+ "viewport.status.cssPhysical": "CSS / physical",
331
+ "viewport.status.safeArea": "Safe area",
332
+ "viewport.status.aitNavBar": "AIT nav bar",
333
+ "viewport.status.aitNavBarValue": "{height}px → SafeArea top · {type}",
334
+ "viewport.orientation.autoSuffix": "{orient} (auto)",
335
+ "iap.section.simulator": "IAP Simulator",
336
+ "iap.row.nextResult": "Next Purchase Result",
337
+ "iap.section.tossPay": "TossPay",
338
+ "iap.row.tossPayResult": "Next Payment Result",
339
+ "iap.section.pending": "Pending Orders ({count})",
340
+ "iap.empty.pending": "(대기 중인 주문 없음)",
341
+ "iap.section.completed": "Completed Orders ({count})",
342
+ "iap.empty.completed": "(완료된 주문 없음)",
343
+ "iap.btn.complete": "Complete",
344
+ "iap.label.pending": "PENDING",
345
+ "events.section.navigation": "Navigation Events",
346
+ "events.btn.triggerBack": "Back 이벤트 발생",
347
+ "events.btn.triggerHome": "Home 이벤트 발생",
348
+ "events.section.login": "Login",
349
+ "events.row.loggedIn": "Logged In",
350
+ "events.row.tossLoginIntegrated": "Toss Login Integrated",
351
+ "analytics.section.log": "Analytics Log ({count})",
352
+ "analytics.btn.clear": "Clear",
353
+ "analytics.calls.section": "SDK Calls ({count})",
354
+ "analytics.calls.btn.clear": "Clear",
355
+ "analytics.calls.empty": "(아직 SDK 호출 없음)",
356
+ "storage.section.title": "Storage ({count} items)",
357
+ "storage.btn.clearAll": "Clear All",
358
+ "storage.empty": "저장된 항목이 없습니다",
359
+ "presets.section.builtIn": "Built-in scenarios",
360
+ "presets.section.saved": "Saved presets ({count})",
361
+ "presets.section.save": "Save",
362
+ "presets.save.description": "network / permissions / auth / IAP / ads / payment 슬라이스를 캡처합니다.",
363
+ "presets.btn.saveCurrent": "현재 상태를 프리셋으로 저장",
364
+ "presets.btn.apply": "Apply",
365
+ "presets.btn.reApply": "Re-apply",
366
+ "presets.btn.delete": "Delete",
367
+ "presets.empty.saved": "저장된 프리셋이 아직 없습니다.",
368
+ "presets.empty.builtIn": "내장 프리셋이 없습니다.",
369
+ "presets.prompt.label": "프리셋 라벨을 입력하세요",
370
+ "presets.confirm.delete": "\"{label}\" 프리셋을 삭제할까요?",
371
+ "ads.section.state": "Ads State",
372
+ "ads.row.isLoaded": "isLoaded",
373
+ "ads.row.forceNoFill": "강제 \"no fill\"",
374
+ "ads.empty.events": "아직 이벤트가 없습니다",
375
+ "ads.section.googleAdMob": "GoogleAdMob",
376
+ "ads.section.tossAds": "TossAds",
377
+ "ads.section.fullScreenAd": "FullScreenAd",
378
+ "ads.btn.load": "Load",
379
+ "ads.btn.show": "Show",
380
+ "ads.section.tossAdsBanner": "TossAds 배너",
381
+ "ads.row.rewardUnitType": "리워드 단위 타입",
382
+ "ads.row.rewardAmount": "리워드 수량",
383
+ "ads.btn.render": "Render",
384
+ "ads.btn.noFill": "No-fill",
385
+ "ads.btn.click": "Click",
386
+ "ads.btn.destroy": "Destroy",
387
+ "notifications.section.title": "requestNotificationAgreement",
388
+ "notifications.option.newAgreement": "newAgreement (최초 동의)",
389
+ "notifications.option.alreadyAgreed": "alreadyAgreed (이미 동의됨)",
390
+ "notifications.option.agreementRejected": "agreementRejected (사용자 거절)",
391
+ "dashboard.title": "AIT 디버그 Dashboard",
392
+ "dashboard.updated": "마지막 갱신: {ts}",
393
+ "dashboard.tunnel.section": "터널 상태",
394
+ "dashboard.tunnel.up": "연결됨",
395
+ "dashboard.tunnel.down": "끊어짐",
396
+ "dashboard.attach.section": "Attach QR",
397
+ "dashboard.attach.hint": "build_attach_url MCP tool을 호출하면 QR이 여기에 표시됩니다.",
398
+ "dashboard.pages.section": "연결된 Pages",
399
+ "dashboard.pages.empty": "attach된 페이지 없음",
400
+ "attach.title": "AIT 디버그 세션 — QR 스캔",
401
+ "attach.deployment": "deployment: {label}",
402
+ "attach.steps.section": "스캔 절차",
403
+ "attach.step1": "토스 앱을 실행하세요.",
404
+ "attach.step2": "폰 카메라 앱으로 QR 코드를 스캔하세요.",
405
+ "attach.step3": "팝업이 뜨면 <strong>\"토스로 열기\"</strong>를 탭하세요.",
406
+ "attach.step4": "미니앱이 열리고 디버그 세션이 자동으로 attach됩니다.",
407
+ "attach.faq.section": "진단 체크리스트",
408
+ "attach.faq.appNotOpen": "<strong>토스 앱이 안 열리는 경우</strong> — 앱 버전 확인, 카메라 앱으로 스캔 (토스 앱 내 QR 리더 X)",
409
+ "attach.faq.prepare": "<strong>미니앱이 PREPARE 상태에서 멈추는 경우</strong> — deep-link에 <code>_deploymentId</code> 파라미터가 있는지 확인",
410
+ "attach.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
411
+ "attach.faq.totp": "<strong>TOTP gate Layer C가 비활성인 경우</strong> — relay 서버에 <code>AIT_DEBUG_TOTP_SECRET</code>이 설정돼 있는지 확인",
412
+ "attach.url.section": "URL (fallback)",
413
+ "launcher.title": "AITC DevTools Launcher",
414
+ "launcher.description": "터미널 QR을 스캔하거나 URL을 입력하세요.",
415
+ "launcher.installCta": "폰에 런처 설치하기",
416
+ "launcher.urlPlaceholder": "https://example.trycloudflare.com",
417
+ "launcher.openBtn": "Open",
418
+ "launcher.scanBtn": "QR 카메라로 스캔",
419
+ "launcher.rescanBtn": "Rescan",
420
+ "launcher.noCamera": "카메라를 사용할 수 없습니다 — URL을 직접 붙여넣으세요.",
421
+ "launcher.cameraError": "카메라에 접근할 수 없습니다 — URL을 직접 붙여넣으세요.",
422
+ "launcher.invalidUrlHttps": "올바른 https:// URL을 입력하세요 (터미널의 터널 URL).",
423
+ "launcher.invalidUrl": "올바른 http(s):// URL을 입력하세요."
424
+ },
425
+ en
426
+ };
427
+ /**
428
+ * Decide a locale from a BCP-47 language tag. `ko` (and `ko-*`) → `'ko'`,
429
+ * everything else → `'en'`. Shared by the browser (`navigator.language`) and
430
+ * Node (`Accept-Language` header) paths so both resolve identically.
431
+ */
432
+ function localeFromLanguageTag(lang) {
433
+ return /^ko\b/i.test(lang) ? "ko" : "en";
434
+ }
435
+ /**
436
+ * Decide a locale from an HTTP `Accept-Language` header value. The Node-served
437
+ * surfaces (e.g. the qr-http-server dashboard) have no `navigator`, so the
438
+ * request header is the only language signal. Reads the FIRST language tag
439
+ * (highest priority, ignoring `q=` weights — good enough for ko/en) and feeds
440
+ * it through the same `ko`-vs-`en` heuristic `detectLocale` uses. Returns `'en'`
441
+ * for an empty/missing header.
442
+ */
443
+ function parseAcceptLanguage(header) {
444
+ if (!header) return "en";
445
+ return localeFromLanguageTag(header.split(",")[0]?.trim().split(";")[0]?.trim() ?? "");
446
+ }
447
+ /**
448
+ * A locale-bound string resolver for surfaces that can't use the in-memory
449
+ * `getLocale()` cache — notably the Node HTTP server, which resolves locale
450
+ * per-request from `Accept-Language` rather than from a process-global. Returns
451
+ * a `t`-compatible closure over the SAME `ko`/`en` tables (single source of
452
+ * truth), so the dashboard/attach HTML shares the exact 169-key catalog the
453
+ * browser surfaces use. The `key: StringKey` signature keeps compile-time key
454
+ * safety on the Node path identical to `t()`.
455
+ */
456
+ function resolveLocaleStrings(locale) {
457
+ const table = tables[locale];
458
+ return (key, vars) => {
459
+ const raw = table[key] ?? key;
460
+ if (!vars) return raw;
461
+ return raw.replace(/\{(\w+)\}/g, (match, name) => {
462
+ const value = vars[name];
463
+ return value === void 0 ? match : String(value);
464
+ });
465
+ };
466
+ }
467
+ //#endregion
468
+ //#region src/mcp/dashboard.generated.ts
469
+ const dashboardChromeHtmlKo = `<!DOCTYPE html>
470
+ <html lang="ko"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>AIT 디버그 Dashboard</title><style>
471
+ *, *::before, *::after { box-sizing: border-box; }
472
+ body {
473
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
474
+ background: #0d1117; color: #c9d1d9;
475
+ display: flex; flex-direction: column; align-items: center;
476
+ min-height: 100vh; margin: 0; padding: 2rem 1rem;
477
+ gap: 1.5rem;
478
+ }
479
+ h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
480
+ .updated { font-size: 0.75rem; opacity: 0.4; font-family: monospace; margin: 0; }
481
+ section { width: 100%; max-width: 520px; }
482
+ h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
483
+ .status { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }
484
+ .status-up { background: #238636; color: #fff; }
485
+ .status-down { background: #6e7681; color: #fff; }
486
+ img.qr {
487
+ width: min(80vw, 300px); height: auto;
488
+ image-rendering: pixelated;
489
+ background: #fff; padding: 0.75rem; border-radius: 10px;
490
+ display: block; margin: 0.5rem auto;
491
+ }
492
+ .url-box {
493
+ font-family: monospace; font-size: 0.7rem;
494
+ word-break: break-all; opacity: 0.45;
495
+ background: #161b22; padding: 0.6rem 0.85rem;
496
+ border-radius: 6px; border: 1px solid #30363d; margin: 0.5rem 0 0;
497
+ }
498
+ .hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }
499
+ ul { margin: 0; padding-left: 1.25rem; }
500
+ li { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }
501
+ li.empty { opacity: 0.4; list-style: none; padding-left: 0; }
502
+ .page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }
503
+ .page-url { word-break: break-all; }
504
+ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
505
+ </style></head><body><h1>AIT 디버그 Dashboard</h1><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>`;
506
+ const attachChromeHtmlKo = `<!DOCTYPE html>
507
+ <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>
508
+ *, *::before, *::after { box-sizing: border-box; }
509
+ body {
510
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
511
+ background: #0d1117; color: #c9d1d9;
512
+ display: flex; flex-direction: column; align-items: center;
513
+ min-height: 100vh; margin: 0; padding: 2rem 1rem;
514
+ gap: 1.5rem;
515
+ }
516
+ h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
517
+ .label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
518
+ img.qr {
519
+ width: min(90vw, 360px); height: auto;
520
+ image-rendering: pixelated;
521
+ background: #fff; padding: 1rem; border-radius: 12px;
522
+ }
523
+ section { width: 100%; max-width: 480px; }
524
+ h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
525
+ ol, ul { margin: 0; padding-left: 1.25rem; }
526
+ li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
527
+ .url-box {
528
+ font-family: monospace; font-size: 0.72rem;
529
+ word-break: break-all; opacity: 0.4;
530
+ background: #161b22; padding: 0.75rem 1rem;
531
+ border-radius: 6px; border: 1px solid #30363d;
532
+ }
533
+ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
534
+ </style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1><p class="label">deployment: __SAFE_LABEL__</p><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/><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><h2>URL (fallback)</h2><p class="url-box">__SAFE_ATTACH_URL__</p></section></body></html>`;
535
+ const dashboardChromeHtmlEn = `<!DOCTYPE html>
536
+ <html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><title>AIT Debug Dashboard</title><style>
537
+ *, *::before, *::after { box-sizing: border-box; }
538
+ body {
539
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
540
+ background: #0d1117; color: #c9d1d9;
541
+ display: flex; flex-direction: column; align-items: center;
542
+ min-height: 100vh; margin: 0; padding: 2rem 1rem;
543
+ gap: 1.5rem;
544
+ }
545
+ h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
546
+ .updated { font-size: 0.75rem; opacity: 0.4; font-family: monospace; margin: 0; }
547
+ section { width: 100%; max-width: 520px; }
548
+ h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
549
+ .status { display: inline-block; padding: 0.25rem 0.75rem; border-radius: 999px; font-size: 0.8rem; font-weight: 600; }
550
+ .status-up { background: #238636; color: #fff; }
551
+ .status-down { background: #6e7681; color: #fff; }
552
+ img.qr {
553
+ width: min(80vw, 300px); height: auto;
554
+ image-rendering: pixelated;
555
+ background: #fff; padding: 0.75rem; border-radius: 10px;
556
+ display: block; margin: 0.5rem auto;
557
+ }
558
+ .url-box {
559
+ font-family: monospace; font-size: 0.7rem;
560
+ word-break: break-all; opacity: 0.45;
561
+ background: #161b22; padding: 0.6rem 0.85rem;
562
+ border-radius: 6px; border: 1px solid #30363d; margin: 0.5rem 0 0;
563
+ }
564
+ .hint { font-size: 0.85rem; opacity: 0.5; margin: 0.25rem 0 0; }
565
+ ul { margin: 0; padding-left: 1.25rem; }
566
+ li { margin-bottom: 0.35rem; font-size: 0.85rem; line-height: 1.5; }
567
+ li.empty { opacity: 0.4; list-style: none; padding-left: 0; }
568
+ .page-id { font-family: monospace; font-size: 0.75rem; opacity: 0.5; margin-right: 0.4rem; }
569
+ .page-url { word-break: break-all; }
570
+ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0; }
571
+ </style></head><body><h1>AIT Debug Dashboard</h1><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>`;
572
+ const attachChromeHtmlEn = `<!DOCTYPE html>
573
+ <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>
574
+ *, *::before, *::after { box-sizing: border-box; }
575
+ body {
576
+ font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
577
+ background: #0d1117; color: #c9d1d9;
578
+ display: flex; flex-direction: column; align-items: center;
579
+ min-height: 100vh; margin: 0; padding: 2rem 1rem;
580
+ gap: 1.5rem;
581
+ }
582
+ h1 { font-size: 1.25rem; font-weight: 600; color: #e6edf3; margin: 0; text-align: center; }
583
+ .label { font-size: 0.8rem; opacity: 0.5; font-family: monospace; margin: 0; }
584
+ img.qr {
585
+ width: min(90vw, 360px); height: auto;
586
+ image-rendering: pixelated;
587
+ background: #fff; padding: 1rem; border-radius: 12px;
588
+ }
589
+ section { width: 100%; max-width: 480px; }
590
+ h2 { font-size: 1rem; font-weight: 600; color: #e6edf3; margin: 0 0 0.5rem; }
591
+ ol, ul { margin: 0; padding-left: 1.25rem; }
592
+ li { margin-bottom: 0.4rem; font-size: 0.9rem; line-height: 1.5; }
593
+ .url-box {
594
+ font-family: monospace; font-size: 0.72rem;
595
+ word-break: break-all; opacity: 0.4;
596
+ background: #161b22; padding: 0.75rem 1rem;
597
+ border-radius: 6px; border: 1px solid #30363d;
598
+ }
599
+ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0; }
600
+ </style></head><body><h1>AIT Debug Session — QR Scan</h1><p class="label">deployment: __SAFE_LABEL__</p><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/><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><h2>URL (fallback)</h2><p class="url-box">__SAFE_ATTACH_URL__</p></section></body></html>`;
601
+ /** Map from Locale to the precompiled dashboard chrome string. */
602
+ const dashboardChromeByLocale = {
603
+ ko: dashboardChromeHtmlKo,
604
+ en: dashboardChromeHtmlEn
605
+ };
606
+ /** Map from Locale to the precompiled attach page chrome string. */
607
+ const attachChromeByLocale = {
608
+ ko: attachChromeHtmlKo,
609
+ en: attachChromeHtmlEn
610
+ };
611
+ //#endregion
612
+ //#region src/mcp/qr-http-server.ts
613
+ /** HTML 특수문자를 이스케이프한다. */
614
+ function escapeHtml(s) {
615
+ return s.replace(/[<>&"']/g, (c) => `&#${c.charCodeAt(0)};`);
616
+ }
617
+ /**
618
+ * Dashboard HTML — precompiled chrome에 per-request 동적 값을 채워 완성한다.
619
+ *
620
+ * 토큰 채우기 순서:
621
+ * 1. chrome string(locale별 precompile)을 가져온다.
622
+ * 2. 동적 부분을 단순 replaceAll로 채운다 (토큰이 HTML context 밖에 있으므로 안전).
623
+ * 3. inline SSE <script>를 </body> 직전에 주입한다.
624
+ *
625
+ * 동적 파트 분류:
626
+ * - "token-fill": 단일 값 교체 (__NOW__, __TUNNEL_CLASS__, __TUNNEL_STATUS__,
627
+ * __ATTACH_SECTION__)
628
+ * - "runtime builder": 가변 길이 구조 (__PAGES_SECTION__ — 조건부 렌더 + 가변 rows)
629
+ * - "suffix": inline SSE <script> (빌드 파이프라인 없는 클라이언트 스크립트, locale
630
+ * aware 문자열 포함)
631
+ *
632
+ * SECRET-HANDLING:
633
+ * - attachUrl은 url-box 안에서만 노출 (TOTP at= 코드 캡슐 그대로).
634
+ * - tunnel wssUrl은 "터널 연결됨" 상태 표시에서 UP/DOWN만 노출.
635
+ * wssUrl 값 자체는 dashboard HTML에 넣지 않는다.
636
+ */
637
+ function buildDashboardHtml(state, qrDataUrl, locale) {
638
+ const s = resolveLocaleStrings(locale);
639
+ const now = (/* @__PURE__ */ new Date()).toISOString();
640
+ const tunnelStatus = state.tunnel.up ? s("dashboard.tunnel.up") : s("dashboard.tunnel.down");
641
+ const tunnelClass = state.tunnel.up ? "status-up" : "status-down";
642
+ let attachSection;
643
+ if (qrDataUrl && state.attachUrl) attachSection = `<img class="qr" src="${qrDataUrl}" alt="attach QR" /><p class="url-box">${escapeHtml(state.attachUrl)}</p>`;
644
+ else attachSection = `<p class="hint">${escapeHtml(s("dashboard.attach.hint"))}</p>`;
645
+ 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) => {
646
+ return `<li><span class="page-id">${escapeHtml(p.id)}</span> <span class="page-url">${escapeHtml(p.url.slice(0, 120))}</span></li>`;
647
+ }).join("\n") : `<li class="empty">${escapeHtml(s("dashboard.pages.empty"))}</li>`}</ul></section>`;
648
+ const sseStrings = {
649
+ tunnelUp: JSON.stringify(s("dashboard.tunnel.up")),
650
+ tunnelDown: JSON.stringify(s("dashboard.tunnel.down")),
651
+ pagesEmpty: JSON.stringify(s("dashboard.pages.empty")),
652
+ attachHint: JSON.stringify(s("dashboard.attach.hint"))
653
+ };
654
+ const filled = dashboardChromeByLocale[locale].replaceAll("__NOW__", escapeHtml(now)).replaceAll("__TUNNEL_CLASS__", tunnelClass).replaceAll("__TUNNEL_STATUS__", escapeHtml(tunnelStatus)).replaceAll("__ATTACH_SECTION__", attachSection).replaceAll("__PAGES_SECTION__", pagesSection);
655
+ const sseScript = buildSseScript(sseStrings);
656
+ return filled.replace("</body>", `${sseScript}\n</body>`);
657
+ }
658
+ /**
659
+ * Inline SSE client <script> — injected into the dashboard HTML at runtime.
660
+ *
661
+ * Subscribes to /events and updates the DOM without a build pipeline.
662
+ * client side: attachUrl은 DOM에 렌더링, wssUrl은 절대 렌더링하지 않는다.
663
+ * pages === null 이면 섹션을 건드리지 않는다 (#411).
664
+ *
665
+ * 문자열 인자는 빌드타임에 ko/en 테이블에서 가져와 JSON.stringify로 이미 escape됨.
666
+ */
667
+ function buildSseScript(strings) {
668
+ return `<script>
669
+ // SSE — /events 구독해 상태 자동 갱신. 빌드 파이프라인 없는 인라인 스크립트.
670
+ (function () {
671
+ var TUNNEL_UP = ${strings.tunnelUp};
672
+ var TUNNEL_DOWN = ${strings.tunnelDown};
673
+ var PAGES_EMPTY = ${strings.pagesEmpty};
674
+ var ATTACH_HINT = ${strings.attachHint};
675
+ var src = new EventSource('/events');
676
+ src.onmessage = function (e) {
677
+ try {
678
+ var s = JSON.parse(e.data);
679
+ // 터널 상태 갱신
680
+ var el = document.getElementById('tunnel-status');
681
+ if (el) {
682
+ el.textContent = s.tunnel && s.tunnel.up ? TUNNEL_UP : TUNNEL_DOWN;
683
+ el.className = 'status ' + (s.tunnel && s.tunnel.up ? 'status-up' : 'status-down');
684
+ }
685
+ // page 목록 갱신 — pages === null(env 2)이면 섹션 자체를 숨긴 채 둔다.
686
+ // 정적 렌더가 #pages-section을 아예 안 그렸으므로 여기서도 손대지 않아
687
+ // SSE push 때 섹션이 되살아나지 않는다(#411). 배열일 때만 목록을 채운다.
688
+ if (s.pages !== null && s.pages !== undefined) {
689
+ var ul = document.getElementById('pages-list');
690
+ if (ul) {
691
+ if (s.pages.length === 0) {
692
+ ul.innerHTML = '<li class="empty">' + PAGES_EMPTY + '</li>';
693
+ } else {
694
+ ul.innerHTML = s.pages.map(function (p) {
695
+ var sid = String(p.id || '').slice(0, 36).replace(/[<>&"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });
696
+ var su = String(p.url || '').slice(0, 120).replace(/[<>&"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });
697
+ return '<li><span class="page-id">' + sid + '</span> <span class="page-url">' + su + '</span></li>';
698
+ }).join('');
699
+ }
700
+ }
701
+ }
702
+ // attachUrl QR 갱신 — attachUrl이 없으면 hint 표시.
703
+ var sec = document.getElementById('attach-section');
704
+ if (sec) {
705
+ if (s.attachUrl) {
706
+ // QR은 서버에서 새로 렌더한 /qr.png?u= 로 img src 교체.
707
+ // TOTP at= 코드는 attachUrl 안에 캡슐화 — 별도 노출 없음.
708
+ // wssUrl은 절대 DOM에 렌더하지 않는다 (SECRET-HANDLING).
709
+ var encoded = encodeURIComponent(s.attachUrl);
710
+ var safeUrl = String(s.attachUrl).slice(0, 2000).replace(/[<>&"']/g, function (c) { return '&#' + c.charCodeAt(0) + ';'; });
711
+ sec.innerHTML =
712
+ '<img class="qr" src="/qr.png?u=' + encoded + '" alt="attach QR" />' +
713
+ '<p class="url-box">' + safeUrl + '</p>';
714
+ } else {
715
+ sec.innerHTML = '<p class="hint">' + ATTACH_HINT + '</p>';
716
+ }
717
+ }
718
+ // 갱신 시각
719
+ var upd = document.getElementById('updated');
720
+ if (upd) upd.textContent = upd.textContent.replace(/[^ ]+$/, new Date().toISOString());
721
+ } catch (_) { /* 파싱 오류 무시 */ }
722
+ };
723
+ src.onerror = function () {
724
+ // 재연결은 EventSource가 자동 처리 (spec 기본 동작).
725
+ };
726
+ })();
727
+ <\/script>`;
728
+ }
729
+ /**
730
+ * Attach 페이지 HTML — precompiled chrome에 per-request 동적 값을 채워 완성한다.
731
+ *
732
+ * 동적 파트:
733
+ * - __QR_DATA_URL__ : base64 data URL (QR 이미지)
734
+ * - __SAFE_LABEL__ : HTML-escaped deploymentId label
735
+ * - __SAFE_ATTACH_URL__ : HTML-escaped attach URL (TOTP at= 코드 포함 — 의도된 전달)
736
+ *
737
+ * SECRET-HANDLING: TOTP at= 코드는 attachUrl 캡슐 안에서만 노출 — 의도된 transport.
738
+ */
739
+ function buildAttachHtml(qrDataUrl, safeLabel, safeAttachUrl, locale) {
740
+ return attachChromeByLocale[locale].replaceAll("__QR_DATA_URL__", qrDataUrl).replaceAll("__SAFE_LABEL__", safeLabel).replaceAll("__SAFE_ATTACH_URL__", safeAttachUrl);
741
+ }
742
+ /**
743
+ * 로컬 HTTP 서버를 127.0.0.1 random port(또는 `AIT_DEBUG_HTTP_PORT` env)로 시작한다.
744
+ * MCP debug server 생애주기에 묶어 사용 — `runDebugServer` shutdown 시 `close()`로 정리.
745
+ *
746
+ * @param getDashboardState - dashboard 상태를 반환하는 클로저. 주입 시 `GET /` dashboard와
747
+ * `GET /events` SSE 스트림이 활성화된다. 미주입 시 두 라우트는 204/서비스 없음으로 응답.
748
+ */
749
+ async function startQrHttpServer(getDashboardState) {
750
+ const { default: QRCode } = await import("qrcode");
751
+ /** SSE 활성 연결 목록 — `notifyStateChange()` 시 전체 push. */
752
+ const sseClients = [];
753
+ /** SSE 연결 하나에 상태 이벤트를 flush한다. */
754
+ function pushStateToClient(res, state) {
755
+ const payload = JSON.stringify({
756
+ tunnel: {
757
+ up: state.tunnel.up,
758
+ wssUrl: state.tunnel.wssUrl
759
+ },
760
+ pages: state.pages,
761
+ attachUrl: state.attachUrl
762
+ });
763
+ res.write(`data: ${payload}\n\n`);
764
+ }
765
+ const server = (0, node_http.createServer)(async (req, res) => {
766
+ const [path, query = ""] = (req.url ?? "/").split("?", 2);
767
+ const params = new URLSearchParams(query ?? "");
768
+ const locale = parseAcceptLanguage(req.headers["accept-language"]);
769
+ if (path === "/") {
770
+ if (!getDashboardState) {
771
+ res.writeHead(204, { "Content-Type": "text/plain; charset=utf-8" });
772
+ res.end();
773
+ return;
774
+ }
775
+ const state = getDashboardState();
776
+ let qrDataUrl = null;
777
+ if (state.attachUrl) try {
778
+ qrDataUrl = await QRCode.toDataURL(state.attachUrl, {
779
+ type: "image/png",
780
+ errorCorrectionLevel: "M"
781
+ });
782
+ } catch {}
783
+ const html = buildDashboardHtml(state, qrDataUrl, locale);
784
+ res.writeHead(200, {
785
+ "Content-Type": "text/html; charset=utf-8",
786
+ "Cache-Control": "no-store"
787
+ });
788
+ res.end(html);
789
+ return;
790
+ }
791
+ if (path === "/events") {
792
+ if (!getDashboardState) {
793
+ res.writeHead(204, { "Content-Type": "text/plain; charset=utf-8" });
794
+ res.end();
795
+ return;
796
+ }
797
+ res.writeHead(200, {
798
+ "Content-Type": "text/event-stream",
799
+ "Cache-Control": "no-cache",
800
+ Connection: "keep-alive",
801
+ "X-Accel-Buffering": "no"
802
+ });
803
+ pushStateToClient(res, getDashboardState());
804
+ sseClients.push(res);
805
+ req.once("close", () => {
806
+ const idx = sseClients.indexOf(res);
807
+ if (idx !== -1) sseClients.splice(idx, 1);
808
+ });
809
+ return;
810
+ }
811
+ if (path === "/attach") {
812
+ const encodedU = params.get("u") ?? "";
813
+ let attachUrl;
814
+ try {
815
+ attachUrl = decodeURIComponent(encodedU);
816
+ } catch {
817
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
818
+ res.end("잘못된 u 파라미터입니다.");
819
+ return;
820
+ }
821
+ let deploymentIdLabel = "attach";
822
+ try {
823
+ const dpMatch = attachUrl.match(/[?&]_deploymentId=([^&]+)/);
824
+ if (dpMatch?.[1]) deploymentIdLabel = decodeURIComponent(dpMatch[1]).slice(0, 36);
825
+ } catch {}
826
+ QRCode.toDataURL(attachUrl, {
827
+ type: "image/png",
828
+ errorCorrectionLevel: "M"
829
+ }).then((dataUrl) => {
830
+ const html = buildAttachHtml(dataUrl, escapeHtml(deploymentIdLabel), escapeHtml(attachUrl), locale);
831
+ res.writeHead(200, {
832
+ "Content-Type": "text/html; charset=utf-8",
833
+ "Cache-Control": "no-store"
834
+ });
835
+ res.end(html);
836
+ }).catch(() => {
837
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
838
+ res.end("QR 생성에 실패했습니다.");
839
+ });
840
+ return;
841
+ }
842
+ if (path === "/qr.png") {
843
+ const encodedU = params.get("u") ?? "";
844
+ let attachUrl;
845
+ try {
846
+ attachUrl = decodeURIComponent(encodedU);
847
+ } catch {
848
+ res.writeHead(400, { "Content-Type": "text/plain; charset=utf-8" });
849
+ res.end("잘못된 u 파라미터입니다.");
850
+ return;
851
+ }
852
+ QRCode.toBuffer(attachUrl, {
853
+ type: "png",
854
+ errorCorrectionLevel: "M"
855
+ }).then((buf) => {
856
+ res.writeHead(200, {
857
+ "Content-Type": "image/png",
858
+ "Cache-Control": "no-store",
859
+ "Content-Length": String(buf.length)
860
+ });
861
+ res.end(buf);
862
+ }).catch(() => {
863
+ res.writeHead(500, { "Content-Type": "text/plain; charset=utf-8" });
864
+ res.end("QR PNG 생성에 실패했습니다.");
865
+ });
866
+ return;
867
+ }
868
+ res.writeHead(404, { "Content-Type": "text/plain; charset=utf-8" });
869
+ res.end("Not Found");
870
+ });
871
+ const listenPort = Number(process.env.AIT_DEBUG_HTTP_PORT ?? 0);
872
+ await new Promise((resolve, reject) => {
873
+ server.listen(listenPort, "127.0.0.1", () => resolve());
874
+ server.once("error", reject);
875
+ });
876
+ const address = server.address();
877
+ if (!address || typeof address === "string") throw new Error("qr-http-server: server.address()가 예상하지 못한 형태입니다.");
878
+ const port = address.port;
879
+ return {
880
+ port,
881
+ buildAttachPageUrl(attachUrl) {
882
+ return `http://127.0.0.1:${port}/attach?u=${encodeURIComponent(attachUrl)}`;
883
+ },
884
+ notifyStateChange() {
885
+ if (!getDashboardState) return;
886
+ const state = getDashboardState();
887
+ for (const client of sseClients) try {
888
+ pushStateToClient(client, state);
889
+ } catch {}
890
+ },
891
+ close() {
892
+ return new Promise((resolve, reject) => {
893
+ server.close((err) => err ? reject(err) : resolve());
894
+ });
895
+ }
896
+ };
897
+ }
898
+ //#endregion
899
+ exports.startQrHttpServer = startQrHttpServer;
900
+
901
+ //# sourceMappingURL=qr-http-server-Byk0Yjk_.cjs.map