@ait-co/devtools 0.1.54 → 0.1.56
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/README.en.md +64 -17
- package/README.md +63 -16
- package/dist/chii-relay-57BfqF_5.cjs +88 -0
- package/dist/chii-relay-57BfqF_5.cjs.map +1 -0
- package/dist/chii-relay-itXOz7kS.js +89 -0
- package/dist/chii-relay-itXOz7kS.js.map +1 -0
- package/dist/in-app/index.d.ts +45 -12
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +38 -7
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.d.ts +8 -3
- package/dist/mcp/cli.d.ts.map +1 -1
- package/dist/mcp/cli.js +782 -226
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +18 -13
- package/dist/mcp/server.js.map +1 -1
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +18 -2
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +66 -4
- package/dist/panel/index.js.map +1 -1
- package/dist/totp-BkKP4m8H.cjs +185 -0
- package/dist/totp-BkKP4m8H.cjs.map +1 -0
- package/dist/totp-CxHsagqY.js +184 -0
- package/dist/totp-CxHsagqY.js.map +1 -0
- package/dist/{tunnel-D0_TwDNE.js → tunnel-Cj8g1LIL.js} +12 -4
- package/dist/tunnel-Cj8g1LIL.js.map +1 -0
- package/dist/{tunnel-BYP0yRBN.cjs → tunnel-p-q6eVWT.cjs} +12 -4
- package/dist/tunnel-p-q6eVWT.cjs.map +1 -0
- package/dist/unplugin/index.cjs +29 -3
- package/dist/unplugin/index.cjs.map +1 -1
- package/dist/unplugin/index.d.cts +11 -0
- package/dist/unplugin/index.d.cts.map +1 -1
- package/dist/unplugin/index.d.ts +12 -1
- package/dist/unplugin/index.d.ts.map +1 -1
- package/dist/unplugin/index.js +29 -3
- package/dist/unplugin/index.js.map +1 -1
- package/dist/unplugin/tunnel.cjs +11 -3
- package/dist/unplugin/tunnel.cjs.map +1 -1
- package/dist/unplugin/tunnel.d.cts +13 -1
- package/dist/unplugin/tunnel.d.cts.map +1 -1
- package/dist/unplugin/tunnel.d.ts +13 -1
- package/dist/unplugin/tunnel.d.ts.map +1 -1
- package/dist/unplugin/tunnel.js +11 -3
- package/dist/unplugin/tunnel.js.map +1 -1
- package/package.json +2 -2
- package/dist/tunnel-BYP0yRBN.cjs.map +0 -1
- package/dist/tunnel-D0_TwDNE.js.map +0 -1
package/README.en.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
A mock library for the `@apps-in-toss/web-framework` SDK. Imports of `@apps-in-toss/webview-bridge` are
|
|
9
|
+
A mock library for the `@apps-in-toss/web-framework` SDK. Imports of `@apps-in-toss/webview-bridge` are intercepted by the unplugin too (only the high-level SDK functions are exposed — bridge primitives are not). (2.x packages `@apps-in-toss/web-bridge` and `@apps-in-toss/web-analytics` are supported for back-compat.)
|
|
10
10
|
|
|
11
11
|
Lets you develop and test Apps in Toss mini-apps in a **regular browser** — without the Toss app. All SDK features are simulated so you can move fast.
|
|
12
12
|
|
|
@@ -47,6 +47,8 @@ pnpm dev:phone # same as AIT_TUNNEL=1 pnpm dev
|
|
|
47
47
|
# QR appears in the terminal → scan with your phone camera → opens in the launcher PWA
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
With `tunnel: { cdp: true }`, a single QR scan opens both the screen preview and on-device CDP — inspect the real WebKit DOM, console, and exceptions from your MCP host (`call_sdk` still hits the mock on environment 2; the real SDK lives on environments 3·4).
|
|
51
|
+
|
|
50
52
|
One-time prerequisite: add `https://devtools.aitc.dev/launcher/` to your phone's home screen. Details: [`docs/scenarios/env-2.md`](./docs/scenarios/env-2.md)
|
|
51
53
|
|
|
52
54
|
---
|
|
@@ -70,12 +72,14 @@ No HMR (Toss WebView cold-load only). Details: [`docs/scenarios/env-3.md`](./doc
|
|
|
70
72
|
Attach a relay to a live OPENED app to observe runtime behavior.
|
|
71
73
|
|
|
72
74
|
```bash
|
|
73
|
-
|
|
75
|
+
devtools-mcp # start MCP server
|
|
76
|
+
# In Claude Code: start_debug({mode: 'live', confirm: true}) ← arms LIVE guard
|
|
74
77
|
# call build_attach_url → scan QR → live app loads + relay attaches
|
|
75
78
|
# call_sdk / evaluate: confirm: true required (LIVE guard — real users affected)
|
|
79
|
+
# (deprecated alias: MCP_ENV=relay-live devtools-mcp — kept for boot-time liveIntent seeding only)
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
`
|
|
82
|
+
`start_debug({mode: 'live', confirm: true})` arms the LIVE guard in-session. Details: [`docs/scenarios/env-4.md`](./docs/scenarios/env-4.md)
|
|
79
83
|
|
|
80
84
|
---
|
|
81
85
|
|
|
@@ -154,7 +158,7 @@ config.plugins.push(aitDevtools.webpack());
|
|
|
154
158
|
|
|
155
159
|
Turbopack does not support a plugin system, so use `resolveAlias` instead.
|
|
156
160
|
|
|
157
|
-
-
|
|
161
|
+
- Aliasing `@apps-in-toss/web-framework` alone is enough. Every SDK call goes through this package, so replacing it with the mock drops the whole web-framework module from the graph, and its internal `@apps-in-toss/webview-bridge` imports disappear with it.
|
|
158
162
|
- Turbopack is generally only used with `next dev`, so no extra production guard is needed.
|
|
159
163
|
|
|
160
164
|
```js
|
|
@@ -163,7 +167,6 @@ module.exports = {
|
|
|
163
167
|
turbo: {
|
|
164
168
|
resolveAlias: {
|
|
165
169
|
'@apps-in-toss/web-framework': '@ait-co/devtools/mock',
|
|
166
|
-
'@apps-in-toss/webview-bridge': '@ait-co/devtools/mock',
|
|
167
170
|
},
|
|
168
171
|
},
|
|
169
172
|
};
|
|
@@ -178,7 +181,6 @@ module.exports = {
|
|
|
178
181
|
turbo: {
|
|
179
182
|
resolveAlias: {
|
|
180
183
|
'@apps-in-toss/web-framework': '@ait-co/devtools/mock',
|
|
181
|
-
'@apps-in-toss/webview-bridge': '@ait-co/devtools/mock',
|
|
182
184
|
},
|
|
183
185
|
},
|
|
184
186
|
},
|
|
@@ -221,7 +223,6 @@ export default defineConfig({
|
|
|
221
223
|
resolve: {
|
|
222
224
|
alias: {
|
|
223
225
|
'@apps-in-toss/web-framework': '@ait-co/devtools/mock',
|
|
224
|
-
'@apps-in-toss/webview-bridge': '@ait-co/devtools/mock',
|
|
225
226
|
},
|
|
226
227
|
},
|
|
227
228
|
});
|
|
@@ -233,7 +234,6 @@ module.exports = {
|
|
|
233
234
|
resolve: {
|
|
234
235
|
alias: {
|
|
235
236
|
'@apps-in-toss/web-framework': require.resolve('@ait-co/devtools/mock'),
|
|
236
|
-
'@apps-in-toss/webview-bridge': require.resolve('@ait-co/devtools/mock'),
|
|
237
237
|
},
|
|
238
238
|
},
|
|
239
239
|
};
|
|
@@ -252,7 +252,7 @@ module.exports = {
|
|
|
252
252
|
| `forceEnable` | `boolean` | `false` | Enable devtools even in production |
|
|
253
253
|
| `mock` | `boolean` | `true` (dev) / `false` (prod+forceEnable) | Enable mock alias |
|
|
254
254
|
| `mcp` | `boolean` | `false` | Add an MCP state endpoint to the Vite dev server (Vite only — see [MCP Server](#mcp-server)) |
|
|
255
|
-
| `tunnel` | `boolean \| { port?: number; qr?: boolean }` | `false` | Expose the Vite dev server via a Cloudflare quick tunnel for real-device preview (see [below](#run-on-a-real-phone)). **Vite dev mode only** |
|
|
255
|
+
| `tunnel` | `boolean \| { port?: number; qr?: boolean; cdp?: boolean }` | `false` | Expose the Vite dev server via a Cloudflare quick tunnel for real-device preview (see [below](#run-on-a-real-phone)). `cdp: true` also wires on-device CDP debugging for environment 2 (PWA). **Vite dev mode only** |
|
|
256
256
|
|
|
257
257
|
```ts
|
|
258
258
|
aitDevtools.vite({ panel: false }); // mock only, no panel
|
|
@@ -260,6 +260,7 @@ aitDevtools.vite({ forceEnable: true }); // enable in production (mock OFF by de
|
|
|
260
260
|
aitDevtools.vite({ forceEnable: true, mock: true }); // enable mock in production too
|
|
261
261
|
aitDevtools.vite({ mcp: true }); // enable MCP endpoint for AI agents
|
|
262
262
|
aitDevtools.vite({ tunnel: true }); // expose dev server at *.trycloudflare.com
|
|
263
|
+
aitDevtools.vite({ tunnel: { cdp: true } }); // real-device preview + on-device CDP debugging
|
|
263
264
|
```
|
|
264
265
|
|
|
265
266
|
## Production builds
|
|
@@ -328,6 +329,8 @@ export default defineConfig({
|
|
|
328
329
|
|
|
329
330
|
> `process.env.AIT_TUNNEL` is evaluated when `vite.config.ts` is loaded (i.e. when the vite process starts). The env variable must therefore be set **before** vite launches (the `dev:phone` script in step (c) handles this automatically).
|
|
330
331
|
|
|
332
|
+
> To also enable on-device CDP debugging, pass the object form: `tunnel: process.env.AIT_TUNNEL ? { cdp: true } : false`. A Chii relay then starts alongside the HTTP tunnel, so a single QR scan opens both the screen preview and a CDP attach. Connect your AI host MCP to that relay to inspect the real WebKit DOM, console, exceptions, and `measure_safe_area` (`call_sdk` still hits the mock on environment 2).
|
|
333
|
+
|
|
331
334
|
(b) **Allow the pnpm 10+ build script** — pnpm blocks dependency postinstall scripts by default for security. `cloudflared` downloads its binary (~38 MB) in postinstall, so you need to explicitly allow it:
|
|
332
335
|
|
|
333
336
|
```json
|
|
@@ -942,14 +945,58 @@ AI coding agents (Claude Code, Cursor, etc.) can observe a running mini-app dire
|
|
|
942
945
|
|
|
943
946
|
A local browser (env 1) and a phone Toss WebView (env 2/3) both speak CDP, so every tool works identically in both environments — the only difference is the attach strategy (`--target=relay` vs `--target=local`).
|
|
944
947
|
|
|
945
|
-
| Mode + target | Invocation | Env
|
|
948
|
+
| Mode + target | Invocation | Env vars | Target | Tools |
|
|
946
949
|
|---|---|---|---|---|
|
|
947
|
-
| `--
|
|
948
|
-
| `--mode=debug --target=relay`
|
|
949
|
-
| `--mode=debug --target=
|
|
950
|
+
| `--target=mobile` (env 2) | `devtools-mcp` → `start_debug({mode:'mobile'})` | `AIT_RELAY_BASE_URL`, `AIT_TUNNEL_BASE_URL` | Real-device Safari/WebKit PWA (external Chii relay + cloudflared tunnel, env 2) | console/network/page + DOM/snapshot/screenshot |
|
|
951
|
+
| `--mode=debug --target=relay` (default, env 3) | `devtools-mcp` → `start_debug({mode: 'staging'})` | `MCP_ENV=relay-dev` (deprecated boot alias) | Dogfood bundle on a phone (CDP/Chii relay + cloudflared tunnel, env 3) | same + `AIT.*` |
|
|
952
|
+
| `--mode=debug --target=relay` LIVE (env 4) | `devtools-mcp` → `start_debug({mode: 'live', confirm: true})` | `MCP_ENV=relay-live` (deprecated boot alias, env 4 LIVE guard) | Live deployed app (env 4) — `call_sdk`/`evaluate` require `confirm: true` | same |
|
|
953
|
+
| `--mode=debug --target=local` (env 1) | `devtools-mcp --target=local` | `MCP_ENV=mock` (auto) | Local Chromium launched by the MCP server (CDP direct-attach, no relay needed, env 1) | same |
|
|
950
954
|
| `--mode=dev` | `devtools-mcp --mode=dev` | `MCP_ENV=mock` (auto) | Mock state from a running Vite dev server (AIT.* only, no CDP) | `AIT.*` (+ `devtools_get_mock_state` alias) |
|
|
951
955
|
|
|
952
|
-
`--target=local` opens `AIT_DEVTOOLS_URL` (default `http://localhost:5173`) and attaches directly to a local Chromium — no relay or tunnel required. `--mode=dev` reads the mock-state HTTP endpoint of the Vite dev server and does not provide CDP tools.
|
|
956
|
+
`--target=local` opens `AIT_DEVTOOLS_URL` (default `http://localhost:5173`) and attaches directly to a local Chromium — no relay or tunnel required. `--mode=dev` reads the mock-state HTTP endpoint of the Vite dev server and does not provide CDP tools. Switch environments in-session with `start_debug(mode)`: `mobile` (env 2 PWA), `staging` (env 3 dogfood), `live` (env 4, arms LIVE guard — `confirm: true` required), `local` (env 1). `MCP_ENV=relay-dev`/`MCP_ENV=relay-live` are deprecated boot-time aliases for liveIntent seeding — prefer `start_debug` for in-session switching.
|
|
957
|
+
|
|
958
|
+
#### Environment 2 (real-device PWA CDP) — `--target=mobile`
|
|
959
|
+
|
|
960
|
+
Debug on a real phone using Safari/WebKit without Toss review. The Vite dev server with [`tunnel:{cdp:true}`](#tunnel-option) brings up both an app HTTP tunnel and a Chii relay tunnel. The MCP server attaches to that relay and provides `build_attach_url` → launcher QR.
|
|
961
|
+
|
|
962
|
+
**Setup procedure:**
|
|
963
|
+
|
|
964
|
+
1. Start the Vite dev server in CDP tunnel mode:
|
|
965
|
+
```bash
|
|
966
|
+
AIT_TUNNEL_CDP=1 pnpm exec vite --config e2e/fixture/vite.config.ts
|
|
967
|
+
```
|
|
968
|
+
The terminal banner prints two URLs:
|
|
969
|
+
- **App HTTP tunnel** `https://<A>.trycloudflare.com` → set as `AIT_TUNNEL_BASE_URL`
|
|
970
|
+
- **Relay wss tunnel** `wss://<B>.trycloudflare.com` → set `AIT_RELAY_BASE_URL` to its `https://` form
|
|
971
|
+
|
|
972
|
+
2. Start the MCP server in mobile mode (separate terminal):
|
|
973
|
+
```json
|
|
974
|
+
{
|
|
975
|
+
"mcpServers": {
|
|
976
|
+
"ait-debug": {
|
|
977
|
+
"command": "npx",
|
|
978
|
+
"args": ["-y", "@ait-co/devtools", "devtools-mcp"],
|
|
979
|
+
"env": {
|
|
980
|
+
"AIT_RELAY_BASE_URL": "https://<B>.trycloudflare.com",
|
|
981
|
+
"AIT_TUNNEL_BASE_URL": "https://<A>.trycloudflare.com"
|
|
982
|
+
}
|
|
983
|
+
}
|
|
984
|
+
}
|
|
985
|
+
}
|
|
986
|
+
```
|
|
987
|
+
|
|
988
|
+
3. In a Claude Code session:
|
|
989
|
+
```
|
|
990
|
+
start_debug({mode: 'mobile'})
|
|
991
|
+
build_attach_url()
|
|
992
|
+
```
|
|
993
|
+
Scan the QR with your phone camera. The launcher PWA opens the app in a frame and injects Chii target.js.
|
|
994
|
+
|
|
995
|
+
4. `list_pages()` → expect one page. Use `take_screenshot()` and other CDP tools.
|
|
996
|
+
|
|
997
|
+
**Env 2 fidelity boundary**: uses the mock SDK (`call_sdk` hits the mock). For real SDK fidelity, move to env 3. CDP runs on the real WebKit engine, so DOM, console, and screenshot reflect the real device screen.
|
|
998
|
+
|
|
999
|
+
**Local-PC verification**: `e2e/launcher-cdp.test.ts` automates node-side relay startup (`startChiiRelay({port:0})`) and launcher param forwarding (Playwright). Browser-side Chii target.js injection is not automated in CI due to the localhost host gate (Layer B1) and ws:// vs wss:// constraints — completed by the manual procedure above on a real device with a trycloudflare.com hostname.
|
|
953
1000
|
|
|
954
1001
|
### Debug mode (CDP via Chii)
|
|
955
1002
|
|
|
@@ -959,7 +1006,7 @@ Read-only tools only. Tools are registered in two tiers based on attach state
|
|
|
959
1006
|
|
|
960
1007
|
Running `devtools-mcp` as a stdio server starts a local Chii relay on an OS-assigned port and opens a cloudflared quick tunnel, printing a public `wss://*.trycloudflare.com` URL and a QR code in the terminal (secrets/auth codes are never printed). When the phone enters the dogfood entry point, the in-app attach UI connects to the relay with that URL, and the agent reads console/network/page state via `chrome-devtools-mcp`-compatible tools — diagnosing regressions without anyone watching the phone.
|
|
961
1008
|
|
|
962
|
-
Environment 3 (dogfood relay):
|
|
1009
|
+
Environment 3 (dogfood relay) — `start_debug({mode: 'staging'})` is the primary in-session entry:
|
|
963
1010
|
|
|
964
1011
|
```json
|
|
965
1012
|
{
|
|
@@ -975,7 +1022,7 @@ Environment 3 (dogfood relay):
|
|
|
975
1022
|
}
|
|
976
1023
|
```
|
|
977
1024
|
|
|
978
|
-
Environment 4 (LIVE relay, LIVE guard enabled):
|
|
1025
|
+
Environment 4 (LIVE relay, LIVE guard enabled) — `start_debug({mode: 'live', confirm: true})` is the primary in-session entry:
|
|
979
1026
|
|
|
980
1027
|
```json
|
|
981
1028
|
{
|
|
@@ -991,7 +1038,7 @@ Environment 4 (LIVE relay, LIVE guard enabled):
|
|
|
991
1038
|
}
|
|
992
1039
|
```
|
|
993
1040
|
|
|
994
|
-
|
|
1041
|
+
`MCP_ENV=relay-dev`/`MCP_ENV=relay-live` are deprecated boot-time aliases that ensure the relay tool surface is visible before the tunnel URL is auto-detected and seed liveIntent on boot. **Prefer `start_debug(mode)` for in-session switching**: `staging` (env 3), `live` (env 4, arms LIVE guard — `confirm: true` required). `MCP_ENV=relay` is a backward-compat alias for `relay-dev`, so **always use `relay-live` (or `start_debug(live)`) explicitly for env 4**.
|
|
995
1042
|
|
|
996
1043
|
| Tool | CDP / AIT backing | Description |
|
|
997
1044
|
|---|---|---|
|
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
|
|
7
7
|

|
|
8
8
|
|
|
9
|
-
`@apps-in-toss/web-framework` SDK의 mock 라이브러리입니다. `@apps-in-toss/webview-bridge` import도 함께
|
|
9
|
+
`@apps-in-toss/web-framework` SDK의 mock 라이브러리입니다. `@apps-in-toss/webview-bridge` import도 unplugin이 함께 인터셉트합니다(high-level SDK 함수만 노출 — bridge primitive는 미노출). (2.x의 `@apps-in-toss/web-bridge`, `@apps-in-toss/web-analytics`도 back-compat으로 지원.)
|
|
10
10
|
|
|
11
11
|
앱인토스(Apps in Toss) 미니앱을 **일반 브라우저**에서 개발하고 테스트할 수 있게 해줍니다. 토스 앱 없이도 SDK의 모든 기능을 시뮬레이션하여 빠른 개발 사이클을 지원합니다.
|
|
12
12
|
|
|
@@ -47,6 +47,8 @@ pnpm dev:phone # AIT_TUNNEL=1 pnpm dev 와 동일
|
|
|
47
47
|
# 터미널에 QR 출력 → 폰 카메라로 스캔 → launcher PWA에서 자동 열림
|
|
48
48
|
```
|
|
49
49
|
|
|
50
|
+
`tunnel: { cdp: true }`를 켜면 같은 QR 한 번으로 화면 미리보기 + on-device CDP가 함께 열려 실기기 WebKit의 DOM·콘솔·예외를 MCP로 관측합니다 (`call_sdk`는 환경 2에서 mock — 실 SDK는 환경 3·4).
|
|
51
|
+
|
|
50
52
|
사전: 폰에 `https://devtools.aitc.dev/launcher/` 를 홈 화면에 한 번 추가. 상세: [`docs/scenarios/env-2.md`](./docs/scenarios/env-2.md)
|
|
51
53
|
|
|
52
54
|
---
|
|
@@ -70,12 +72,14 @@ HMR 없음(토스 WebView cold-load만). 상세: [`docs/scenarios/env-3.md`](./d
|
|
|
70
72
|
검수를 통과하고 OPENED 상태인 실 출시 앱에 relay를 붙여 런타임을 관측합니다.
|
|
71
73
|
|
|
72
74
|
```bash
|
|
73
|
-
|
|
75
|
+
devtools-mcp # MCP 서버 시작
|
|
76
|
+
# Claude Code에서: start_debug({mode: 'live', confirm: true}) ← LIVE guard 활성화
|
|
74
77
|
# build_attach_url 호출 → QR 스캔 → LIVE 앱 로드 + relay attach
|
|
75
78
|
# call_sdk / evaluate 는 confirm: true 필수 (LIVE guard — 실유저 영향)
|
|
79
|
+
# (deprecated 별칭: MCP_ENV=relay-live devtools-mcp — 부팅 시 liveIntent 시드용으로만 남음)
|
|
76
80
|
```
|
|
77
81
|
|
|
78
|
-
`
|
|
82
|
+
`start_debug({mode: 'live', confirm: true})`가 LIVE guard를 활성화합니다. 상세: [`docs/scenarios/env-4.md`](./docs/scenarios/env-4.md)
|
|
79
83
|
|
|
80
84
|
---
|
|
81
85
|
|
|
@@ -154,7 +158,7 @@ config.plugins.push(aitDevtools.webpack());
|
|
|
154
158
|
|
|
155
159
|
Turbopack은 플러그인 시스템을 지원하지 않으므로 `resolveAlias`를 사용합니다.
|
|
156
160
|
|
|
157
|
-
- `@apps-in-toss/
|
|
161
|
+
- `@apps-in-toss/web-framework` 하나만 alias하면 됩니다. SDK 호출은 모두 이 패키지를 거치므로, mock으로 치환하면 web-framework 모듈 자체가 모듈 그래프에서 빠지고, 그 안의 `@apps-in-toss/webview-bridge` import도 함께 사라집니다.
|
|
158
162
|
- Turbopack은 일반적으로 `next dev`에서만 사용되므로 별도의 production 가드가 필요하지 않습니다.
|
|
159
163
|
|
|
160
164
|
```js
|
|
@@ -163,7 +167,6 @@ module.exports = {
|
|
|
163
167
|
turbo: {
|
|
164
168
|
resolveAlias: {
|
|
165
169
|
'@apps-in-toss/web-framework': '@ait-co/devtools/mock',
|
|
166
|
-
'@apps-in-toss/webview-bridge': '@ait-co/devtools/mock',
|
|
167
170
|
},
|
|
168
171
|
},
|
|
169
172
|
};
|
|
@@ -178,7 +181,6 @@ module.exports = {
|
|
|
178
181
|
turbo: {
|
|
179
182
|
resolveAlias: {
|
|
180
183
|
'@apps-in-toss/web-framework': '@ait-co/devtools/mock',
|
|
181
|
-
'@apps-in-toss/webview-bridge': '@ait-co/devtools/mock',
|
|
182
184
|
},
|
|
183
185
|
},
|
|
184
186
|
},
|
|
@@ -221,7 +223,6 @@ export default defineConfig({
|
|
|
221
223
|
resolve: {
|
|
222
224
|
alias: {
|
|
223
225
|
'@apps-in-toss/web-framework': '@ait-co/devtools/mock',
|
|
224
|
-
'@apps-in-toss/webview-bridge': '@ait-co/devtools/mock',
|
|
225
226
|
},
|
|
226
227
|
},
|
|
227
228
|
});
|
|
@@ -233,7 +234,6 @@ module.exports = {
|
|
|
233
234
|
resolve: {
|
|
234
235
|
alias: {
|
|
235
236
|
'@apps-in-toss/web-framework': require.resolve('@ait-co/devtools/mock'),
|
|
236
|
-
'@apps-in-toss/webview-bridge': require.resolve('@ait-co/devtools/mock'),
|
|
237
237
|
},
|
|
238
238
|
},
|
|
239
239
|
};
|
|
@@ -252,7 +252,7 @@ module.exports = {
|
|
|
252
252
|
| `forceEnable` | `boolean` | `false` | production에서도 devtools 활성화 |
|
|
253
253
|
| `mock` | `boolean` | `true` (dev) / `false` (prod+forceEnable) | mock alias 활성화 여부 |
|
|
254
254
|
| `mcp` | `boolean` | `false` | Vite dev server에 MCP state endpoint 추가 (Vite 전용, [MCP 섹션](#mcp-server) 참조) |
|
|
255
|
-
| `tunnel` | `boolean \| { port?: number; qr?: boolean }` | `false` | Vite dev 서버를 Cloudflare quick tunnel로 노출 (실기기 미리보기, [아래](#run-on-a-real-phone-실기기-미리보기) 참고). **Vite dev 모드 전용** |
|
|
255
|
+
| `tunnel` | `boolean \| { port?: number; qr?: boolean; cdp?: boolean }` | `false` | Vite dev 서버를 Cloudflare quick tunnel로 노출 (실기기 미리보기, [아래](#run-on-a-real-phone-실기기-미리보기) 참고). `cdp: true`면 환경 2 PWA에 on-device CDP 디버깅도 배선. **Vite dev 모드 전용** |
|
|
256
256
|
|
|
257
257
|
```ts
|
|
258
258
|
aitDevtools.vite({ panel: false }); // Panel 없이 mock만 사용
|
|
@@ -260,6 +260,7 @@ aitDevtools.vite({ forceEnable: true }); // production에서도 활성화 (mock
|
|
|
260
260
|
aitDevtools.vite({ forceEnable: true, mock: true }); // production에서 mock도 활성화
|
|
261
261
|
aitDevtools.vite({ mcp: true }); // AI 에이전트용 MCP endpoint 활성화
|
|
262
262
|
aitDevtools.vite({ tunnel: true }); // dev 서버를 *.trycloudflare.com으로 노출
|
|
263
|
+
aitDevtools.vite({ tunnel: { cdp: true } }); // 실기기 미리보기 + on-device CDP 디버깅
|
|
263
264
|
```
|
|
264
265
|
|
|
265
266
|
## Production 빌드
|
|
@@ -328,6 +329,8 @@ export default defineConfig({
|
|
|
328
329
|
|
|
329
330
|
> `process.env.AIT_TUNNEL`은 `vite.config.ts`를 로드하는 시점(= vite 프로세스 기동 시)에 평가됩니다. 따라서 env 변수는 **vite를 띄우기 전에** 설정되어 있어야 합니다 (아래 (c)의 `dev:phone` 스크립트가 이를 자동으로 해결합니다).
|
|
330
331
|
|
|
332
|
+
> on-device CDP 디버깅까지 켜려면 `tunnel: process.env.AIT_TUNNEL ? { cdp: true } : false`처럼 객체 형태로 줍니다. 그러면 HTTP 터널과 별도로 Chii relay가 떠서, QR 한 번으로 화면 미리보기와 CDP attach가 동시에 열립니다. AI host MCP를 그 relay에 붙이면 실기기 WebKit의 DOM·콘솔·예외·`measure_safe_area`를 관측합니다 (`call_sdk`는 환경 2에서 mock).
|
|
333
|
+
|
|
331
334
|
(b) **`package.json`에 pnpm 10+ 빌드 스크립트 허용** — pnpm은 보안상 dependency의 postinstall을 기본 차단합니다. `cloudflared`는 postinstall에서 바이너리(~38 MB)를 받으므로 명시 허용 필요:
|
|
332
335
|
|
|
333
336
|
```json
|
|
@@ -974,12 +977,56 @@ AI 코딩 에이전트(Claude Code, Cursor 등)가 [MCP(Model Context Protocol)]
|
|
|
974
977
|
|
|
975
978
|
| 모드 + 타깃 | 호출 | 환경 변수 | 대상 | tool |
|
|
976
979
|
|---|---|---|---|---|
|
|
977
|
-
| `--
|
|
978
|
-
| `--mode=debug --target=relay`
|
|
979
|
-
| `--mode=debug --target=
|
|
980
|
+
| `--target=mobile` (env 2) | `devtools-mcp` → `start_debug({mode:'mobile'})` | `AIT_RELAY_BASE_URL`, `AIT_TUNNEL_BASE_URL` | 실기기 Safari/WebKit PWA (외부 Chii relay + cloudflared 터널, 환경 2) | console/network/page + DOM/snapshot/screenshot |
|
|
981
|
+
| `--mode=debug --target=relay` (기본값, env 3) | `devtools-mcp` → `start_debug({mode: 'staging'})` | `MCP_ENV=relay-dev` (deprecated 부팅 별칭) | 폰 안 dogfood 번들 (CDP/Chii relay + cloudflared 터널, 환경 3) | 동일 + `AIT.*` |
|
|
982
|
+
| `--mode=debug --target=relay` LIVE (env 4) | `devtools-mcp` → `start_debug({mode: 'live', confirm: true})` | `MCP_ENV=relay-live` (deprecated 부팅 별칭, **환경 4 LIVE guard**) | LIVE 배포 앱 (환경 4) — `call_sdk`/`evaluate`에 `confirm: true` 필요 | 동일 |
|
|
983
|
+
| `--mode=debug --target=local` (env 1) | `devtools-mcp --target=local` | `MCP_ENV=mock` (자동) | MCP가 직접 기동한 로컬 Chromium (CDP direct-attach, relay 불필요, 환경 1) | 동일 |
|
|
980
984
|
| `--mode=dev` | `devtools-mcp --mode=dev` | `MCP_ENV=mock` (자동) | 실행 중인 Vite dev server의 mock state (AIT.* 전용, CDP 없음) | `AIT.*` (+ `devtools_get_mock_state` alias) |
|
|
981
985
|
|
|
982
|
-
`--target=local`은 `AIT_DEVTOOLS_URL`(기본 `http://localhost:5173`)을 열고 로컬 Chromium에 CDP direct-attach합니다 — relay나 터널이 필요하지 않습니다. `--mode=dev`는 Vite dev server의 mock-state HTTP endpoint를 읽으며 CDP tool은 제공하지 않습니다.
|
|
986
|
+
`--target=local`은 `AIT_DEVTOOLS_URL`(기본 `http://localhost:5173`)을 열고 로컬 Chromium에 CDP direct-attach합니다 — relay나 터널이 필요하지 않습니다. `--mode=dev`는 Vite dev server의 mock-state HTTP endpoint를 읽으며 CDP tool은 제공하지 않습니다. 세션 내 환경 전환은 `start_debug(mode)` 한 번으로 처리됩니다: `mobile`(env 2 PWA), `staging`(env 3 dogfood), `live`(env 4 LIVE guard 활성화, `confirm: true` 필수), `local`(env 1). `MCP_ENV=relay-dev`/`MCP_ENV=relay-live`는 부팅 시 liveIntent 시드용 deprecated 별칭 — 새 세션에서는 `start_debug`로 전환하세요.
|
|
987
|
+
|
|
988
|
+
#### 환경 2 (실기기 PWA CDP) — `--target=mobile`
|
|
989
|
+
|
|
990
|
+
토스 검수 없이 실기기 WebKit 엔진에서 CDP 디버깅이 가능한 모드입니다. [`tunnel:{cdp:true}`](#tunnel-옵션)를 켠 Vite dev server가 앱 HTTP 터널과 Chii relay 터널을 두 개 띄우고, MCP는 그 relay에 붙어 `build_attach_url` → 런처 QR을 제공합니다.
|
|
991
|
+
|
|
992
|
+
**진입 절차:**
|
|
993
|
+
|
|
994
|
+
1. Vite dev server를 CDP 터널 모드로 기동:
|
|
995
|
+
```bash
|
|
996
|
+
AIT_TUNNEL_CDP=1 pnpm exec vite --config e2e/fixture/vite.config.ts
|
|
997
|
+
```
|
|
998
|
+
터미널 배너에 두 URL이 출력됩니다:
|
|
999
|
+
- **앱 HTTP 터널** `https://<A>.trycloudflare.com` → `AIT_TUNNEL_BASE_URL`로 설정
|
|
1000
|
+
- **relay wss 터널** `wss://<B>.trycloudflare.com` → `AIT_RELAY_BASE_URL`의 `https://` 형으로 설정
|
|
1001
|
+
|
|
1002
|
+
2. MCP server를 mobile 모드로 기동 (별도 터미널):
|
|
1003
|
+
```json
|
|
1004
|
+
{
|
|
1005
|
+
"mcpServers": {
|
|
1006
|
+
"ait-debug": {
|
|
1007
|
+
"command": "npx",
|
|
1008
|
+
"args": ["-y", "@ait-co/devtools", "devtools-mcp"],
|
|
1009
|
+
"env": {
|
|
1010
|
+
"AIT_RELAY_BASE_URL": "https://<B>.trycloudflare.com",
|
|
1011
|
+
"AIT_TUNNEL_BASE_URL": "https://<A>.trycloudflare.com"
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
}
|
|
1015
|
+
}
|
|
1016
|
+
```
|
|
1017
|
+
|
|
1018
|
+
3. Claude Code 세션에서 진입:
|
|
1019
|
+
```
|
|
1020
|
+
start_debug({mode: 'mobile'})
|
|
1021
|
+
build_attach_url()
|
|
1022
|
+
```
|
|
1023
|
+
QR을 폰 카메라로 스캔하면 런처 PWA가 앱을 프레임에 열고 Chii target.js를 주입합니다.
|
|
1024
|
+
|
|
1025
|
+
4. `list_pages()` → 페이지 1개 확인. `take_screenshot()` 등 CDP tool을 사용합니다.
|
|
1026
|
+
|
|
1027
|
+
**env 2의 fidelity 경계**: SDK mock을 씁니다 (실 SDK 호출 불가) — `call_sdk`는 환경 2에서 mock을 칩니다. 실 SDK fidelity가 필요하면 환경 3으로 올라가세요. CDP는 실 WebKit 엔진 위에서 동작하므로 DOM·console·screenshot은 실기기 화면을 그대로 반영합니다.
|
|
1028
|
+
|
|
1029
|
+
**로컬 PC 검증**: `e2e/launcher-cdp.test.ts`가 node-side relay 기동(`startChiiRelay({port:0})`)과 launcher 파라미터 포워딩(Playwright)을 자동 검증합니다. browser-side Chii target.js 주입은 localhost 호스트 게이트(Layer B1)와 ws:// vs wss:// 제약으로 CI에서 검증 불가 — 위 수동 절차(실기기 trycloudflare.com 호스트)에서 완성됩니다.
|
|
983
1030
|
|
|
984
1031
|
### Debug 모드 (CDP via Chii)
|
|
985
1032
|
|
|
@@ -997,7 +1044,7 @@ tunnel로 공개 `wss://*.trycloudflare.com` URL을 발급한 뒤 QR을 터미
|
|
|
997
1044
|
에이전트가 `chrome-devtools-mcp` 호환 tool로 console/network/page 상태를 read합니다. 사람이 폰을
|
|
998
1045
|
지켜볼 필요 없이 회귀를 단독 진단하는 것이 목표입니다.
|
|
999
1046
|
|
|
1000
|
-
환경 3 (dogfood relay):
|
|
1047
|
+
환경 3 (dogfood relay) — `start_debug({mode: 'staging'})`이 세션 내 1차 진입:
|
|
1001
1048
|
|
|
1002
1049
|
```json
|
|
1003
1050
|
{
|
|
@@ -1013,7 +1060,7 @@ tunnel로 공개 `wss://*.trycloudflare.com` URL을 발급한 뒤 QR을 터미
|
|
|
1013
1060
|
}
|
|
1014
1061
|
```
|
|
1015
1062
|
|
|
1016
|
-
환경 4 (LIVE relay, LIVE guard 활성화):
|
|
1063
|
+
환경 4 (LIVE relay, LIVE guard 활성화) — `start_debug({mode: 'live', confirm: true})`이 세션 내 1차 진입:
|
|
1017
1064
|
|
|
1018
1065
|
```json
|
|
1019
1066
|
{
|
|
@@ -1029,7 +1076,7 @@ tunnel로 공개 `wss://*.trycloudflare.com` URL을 발급한 뒤 QR을 터미
|
|
|
1029
1076
|
}
|
|
1030
1077
|
```
|
|
1031
1078
|
|
|
1032
|
-
`MCP_ENV=relay-dev
|
|
1079
|
+
`MCP_ENV=relay-dev`/`MCP_ENV=relay-live`는 부팅 시 env 힌트로 relay tool surface 노출을 앞당기는 deprecated 별칭입니다. **세션 내 환경 전환은 `start_debug(mode)` 사용이 1차 경로**: `staging`(env 3), `live`(env 4, LIVE guard 활성화 — `confirm: true` 필수). `MCP_ENV=relay`는 backward-compat alias로 `relay-dev`로 해석되므로 **환경 4에서는 `relay-live` 별칭(또는 `start_debug(live)`)을 명시해야 합니다**.
|
|
1033
1080
|
|
|
1034
1081
|
| Tool | CDP / AIT 백킹 | 설명 |
|
|
1035
1082
|
|---|---|---|
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
let node_http = require("node:http");
|
|
2
|
+
//#region src/mcp/chii-relay.ts
|
|
3
|
+
/**
|
|
4
|
+
* Boots the local Chii relay server.
|
|
5
|
+
*
|
|
6
|
+
* Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome
|
|
7
|
+
* WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.
|
|
8
|
+
* The relay accepts a `target` websocket from the phone's injected `target.js`
|
|
9
|
+
* and `client` websockets from CDP frontends (our MCP connection).
|
|
10
|
+
*
|
|
11
|
+
* Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app
|
|
12
|
+
* entries.
|
|
13
|
+
*
|
|
14
|
+
* TOTP auth (relay-side, authoritative gate):
|
|
15
|
+
* When `verifyAuth` is provided, this module registers an HTTP upgrade
|
|
16
|
+
* listener on the server BEFORE calling `chii.start({server})`. Node's
|
|
17
|
+
* `http.Server` allows multiple 'upgrade' listeners; the first to call
|
|
18
|
+
* `socket.destroy()` wins. Invalid auth → 401 + destroy (chii never sees
|
|
19
|
+
* the connection). Valid auth → return without side-effect (chii handles it).
|
|
20
|
+
*
|
|
21
|
+
* Threat model: "URL leak" — someone obtains the tunnel URL (Slack paste, QR
|
|
22
|
+
* screenshot, shoulder-surfing) but does not have the shared TOTP secret.
|
|
23
|
+
* Rotating 6-digit code makes the URL stale after 30 s.
|
|
24
|
+
* A determined attacker who extracts the secret from the dogfood bundle can
|
|
25
|
+
* still compute valid codes; that is out of scope (see umbrella CLAUDE.md §4).
|
|
26
|
+
*
|
|
27
|
+
* SECRET-HANDLING: The secret value and computed TOTP codes MUST NOT appear
|
|
28
|
+
* in any log, error message, or process output. `verifyAuth` is a black-box
|
|
29
|
+
* predicate from the caller's perspective; this module only forwards pass/fail.
|
|
30
|
+
*/
|
|
31
|
+
const require$1 = (0, require("node:module").createRequire)(require("url").pathToFileURL(__filename).href);
|
|
32
|
+
function loadChiiServer() {
|
|
33
|
+
const mod = require$1("chii");
|
|
34
|
+
if (typeof mod === "object" && mod !== null && "start" in mod && typeof mod.start === "function") return mod;
|
|
35
|
+
throw new Error("chii server module did not expose start()");
|
|
36
|
+
}
|
|
37
|
+
/**
|
|
38
|
+
* Starts the Chii relay and resolves once listening.
|
|
39
|
+
*
|
|
40
|
+
* Default port is 0 (OS-assigned). With port 0 the OS picks a free ephemeral
|
|
41
|
+
* port on every start, so a stale cloudflared orphan holding any particular
|
|
42
|
+
* port cannot cause EADDRINUSE. The resolved `ChiiRelay.port` and `baseUrl`
|
|
43
|
+
* always reflect the actual bound port.
|
|
44
|
+
*
|
|
45
|
+
* chii.start() is called with `server` (our pre-created httpServer) BEFORE
|
|
46
|
+
* httpServer.listen(). This is intentional: chii attaches its Koa handler and
|
|
47
|
+
* WS upgrade listener to the server object, but the actual TCP bind is
|
|
48
|
+
* performed by our httpServer.listen() call below. The `port`/`domain` values
|
|
49
|
+
* passed to chii.start() are used for display/banner purposes inside chii and
|
|
50
|
+
* do not affect which port the server binds. The connection path (clients
|
|
51
|
+
* connecting to `relay.baseUrl`) always uses the post-listen confirmed port.
|
|
52
|
+
*/
|
|
53
|
+
async function startChiiRelay(options = {}) {
|
|
54
|
+
const requestedPort = options.port ?? 0;
|
|
55
|
+
const host = options.host ?? "127.0.0.1";
|
|
56
|
+
const { verifyAuth } = options;
|
|
57
|
+
const httpServer = (0, node_http.createServer)();
|
|
58
|
+
if (verifyAuth) httpServer.on("upgrade", (req, socket) => {
|
|
59
|
+
if (!verifyAuth(req)) {
|
|
60
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n");
|
|
61
|
+
socket.destroy();
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
await loadChiiServer().start({
|
|
66
|
+
server: httpServer,
|
|
67
|
+
domain: `${host}:${requestedPort}`,
|
|
68
|
+
port: requestedPort
|
|
69
|
+
});
|
|
70
|
+
const actualPort = await new Promise((resolve, reject) => {
|
|
71
|
+
httpServer.once("error", reject);
|
|
72
|
+
httpServer.listen(requestedPort, host, () => {
|
|
73
|
+
httpServer.off("error", reject);
|
|
74
|
+
resolve(httpServer.address().port);
|
|
75
|
+
});
|
|
76
|
+
});
|
|
77
|
+
return {
|
|
78
|
+
port: actualPort,
|
|
79
|
+
baseUrl: `http://${host}:${actualPort}`,
|
|
80
|
+
close: () => new Promise((resolve) => {
|
|
81
|
+
httpServer.close(() => resolve());
|
|
82
|
+
})
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
//#endregion
|
|
86
|
+
exports.startChiiRelay = startChiiRelay;
|
|
87
|
+
|
|
88
|
+
//# sourceMappingURL=chii-relay-57BfqF_5.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chii-relay-57BfqF_5.cjs","names":["require"],"sources":["../src/mcp/chii-relay.ts"],"sourcesContent":["/**\n * Boots the local Chii relay server.\n *\n * Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome\n * WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.\n * The relay accepts a `target` websocket from the phone's injected `target.js`\n * and `client` websockets from CDP frontends (our MCP connection).\n *\n * Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app\n * entries.\n *\n * TOTP auth (relay-side, authoritative gate):\n * When `verifyAuth` is provided, this module registers an HTTP upgrade\n * listener on the server BEFORE calling `chii.start({server})`. Node's\n * `http.Server` allows multiple 'upgrade' listeners; the first to call\n * `socket.destroy()` wins. Invalid auth → 401 + destroy (chii never sees\n * the connection). Valid auth → return without side-effect (chii handles it).\n *\n * Threat model: \"URL leak\" — someone obtains the tunnel URL (Slack paste, QR\n * screenshot, shoulder-surfing) but does not have the shared TOTP secret.\n * Rotating 6-digit code makes the URL stale after 30 s.\n * A determined attacker who extracts the secret from the dogfood bundle can\n * still compute valid codes; that is out of scope (see umbrella CLAUDE.md §4).\n *\n * SECRET-HANDLING: The secret value and computed TOTP codes MUST NOT appear\n * in any log, error message, or process output. `verifyAuth` is a black-box\n * predicate from the caller's perspective; this module only forwards pass/fail.\n */\n\nimport { createServer, type IncomingMessage, type Server } from 'node:http';\nimport { createRequire } from 'node:module';\nimport type { AddressInfo } from 'node:net';\nimport type { Duplex } from 'node:stream';\n\nconst require = createRequire(import.meta.url);\n\n/** `chii/server` is CommonJS and shipped without TypeScript types. */\ninterface ChiiServerModule {\n start(options: {\n port?: number;\n host?: string;\n domain?: string;\n server?: Server;\n basePath?: string;\n }): Promise<void>;\n}\n\nfunction loadChiiServer(): ChiiServerModule {\n // `chii`'s package `main` is `./server/index.js`, exposing `{ start }`.\n const mod: unknown = require('chii');\n if (\n typeof mod === 'object' &&\n mod !== null &&\n 'start' in mod &&\n typeof (mod as { start: unknown }).start === 'function'\n ) {\n return mod as ChiiServerModule;\n }\n throw new Error('chii server module did not expose start()');\n}\n\nexport interface ChiiRelay {\n port: number;\n /** Base URL for the relay HTTP/WS server, e.g. `http://127.0.0.1:54321`. */\n baseUrl: string;\n close(): Promise<void>;\n}\n\nexport interface StartChiiRelayOptions {\n /**\n * Local port for the relay. Default 0 (OS-assigned ephemeral port).\n *\n * Using 0 means the OS picks a free port — this is the safe default because\n * a stale cloudflared child process (PPID 1, orphaned after SIGKILL) may still\n * be holding a fixed port. A fixed port causes EADDRINUSE on the next startup,\n * which makes the MCP handshake fail with -32000. With port 0 the new relay\n * always gets a fresh port, making any orphaned process harmless.\n *\n * Pass an explicit number to restore fixed-port behaviour (backwards-compatible).\n */\n port?: number;\n /** Bind host. Default 127.0.0.1 (tunnel reaches it locally). */\n host?: string;\n /**\n * Optional auth predicate for WebSocket upgrade requests.\n *\n * When provided, every inbound WebSocket upgrade is checked by calling\n * `verifyAuth(req)` before Chii processes it. Return `true` to allow the\n * upgrade; return `false` to reject with HTTP 401 and destroy the socket.\n *\n * The predicate MUST NOT log the secret or any TOTP code — it is a black-box\n * from this module's perspective.\n *\n * @param req - The raw HTTP `IncomingMessage` from the upgrade handshake.\n * Inspect `req.url` for query parameters (e.g. `at=<code>`).\n * @returns `true` if the upgrade is authorised, `false` to reject.\n */\n verifyAuth?: (req: IncomingMessage) => boolean;\n}\n\n/**\n * Starts the Chii relay and resolves once listening.\n *\n * Default port is 0 (OS-assigned). With port 0 the OS picks a free ephemeral\n * port on every start, so a stale cloudflared orphan holding any particular\n * port cannot cause EADDRINUSE. The resolved `ChiiRelay.port` and `baseUrl`\n * always reflect the actual bound port.\n *\n * chii.start() is called with `server` (our pre-created httpServer) BEFORE\n * httpServer.listen(). This is intentional: chii attaches its Koa handler and\n * WS upgrade listener to the server object, but the actual TCP bind is\n * performed by our httpServer.listen() call below. The `port`/`domain` values\n * passed to chii.start() are used for display/banner purposes inside chii and\n * do not affect which port the server binds. The connection path (clients\n * connecting to `relay.baseUrl`) always uses the post-listen confirmed port.\n */\nexport async function startChiiRelay(options: StartChiiRelayOptions = {}): Promise<ChiiRelay> {\n const requestedPort = options.port ?? 0;\n const host = options.host ?? '127.0.0.1';\n const { verifyAuth } = options;\n\n const httpServer = createServer();\n\n // Register our auth listener BEFORE chii.start() so it fires first.\n // Node's http.Server emits 'upgrade' to all listeners in registration order;\n // the first to destroy() the socket wins. Valid requests return without\n // side-effect so chii's own upgrade handler takes over normally.\n //\n // We only register when verifyAuth is provided so the no-auth path is\n // zero-overhead for tests and local-only dev sessions.\n if (verifyAuth) {\n httpServer.on('upgrade', (req: IncomingMessage, socket: Duplex) => {\n if (!verifyAuth(req)) {\n // Reject: send a minimal HTTP 401 response and close the socket.\n // We do NOT log req.url or any auth param here to avoid leaking codes.\n socket.write('HTTP/1.1 401 Unauthorized\\r\\nContent-Length: 0\\r\\n\\r\\n');\n socket.destroy();\n // Early return — chii's handler is NOT called for this socket.\n return;\n }\n // Auth passed: no-op. Chii's upgrade listener (registered below by\n // chii.start) will handle the rest.\n });\n }\n\n const chii = loadChiiServer();\n // Passing an existing `server` makes chii attach its Koa handler + WS upgrade\n // to our HTTP server rather than creating its own listener.\n // Note: port/domain here are display-only inside chii — the TCP bind is ours.\n await chii.start({ server: httpServer, domain: `${host}:${requestedPort}`, port: requestedPort });\n\n const actualPort = await new Promise<number>((resolve, reject) => {\n httpServer.once('error', reject);\n httpServer.listen(requestedPort, host, () => {\n httpServer.off('error', reject);\n // httpServer.address() is non-null immediately after the listen callback.\n const addr = httpServer.address() as AddressInfo;\n resolve(addr.port);\n });\n });\n\n return {\n port: actualPort,\n baseUrl: `http://${host}:${actualPort}`,\n close: () =>\n new Promise<void>((resolve) => {\n httpServer.close(() => resolve());\n }),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAMA,aAAAA,0BAAAA,eAAAA,QAAAA,MAAAA,CAAAA,cAAAA,WAAAA,CAAAA,KAAwC;AAa9C,SAAS,iBAAmC;CAE1C,MAAM,MAAeA,UAAQ,OAAO;AACpC,KACE,OAAO,QAAQ,YACf,QAAQ,QACR,WAAW,OACX,OAAQ,IAA2B,UAAU,WAE7C,QAAO;AAET,OAAM,IAAI,MAAM,4CAA4C;;;;;;;;;;;;;;;;;;AA0D9D,eAAsB,eAAe,UAAiC,EAAE,EAAsB;CAC5F,MAAM,gBAAgB,QAAQ,QAAQ;CACtC,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,EAAE,eAAe;CAEvB,MAAM,cAAA,GAAA,UAAA,eAA2B;AASjC,KAAI,WACF,YAAW,GAAG,YAAY,KAAsB,WAAmB;AACjE,MAAI,CAAC,WAAW,IAAI,EAAE;AAGpB,UAAO,MAAM,yDAAyD;AACtE,UAAO,SAAS;AAEhB;;GAIF;AAOJ,OAJa,gBAAgB,CAIlB,MAAM;EAAE,QAAQ;EAAY,QAAQ,GAAG,KAAK,GAAG;EAAiB,MAAM;EAAe,CAAC;CAEjG,MAAM,aAAa,MAAM,IAAI,SAAiB,SAAS,WAAW;AAChE,aAAW,KAAK,SAAS,OAAO;AAChC,aAAW,OAAO,eAAe,YAAY;AAC3C,cAAW,IAAI,SAAS,OAAO;AAG/B,WADa,WAAW,SAAS,CACpB,KAAK;IAClB;GACF;AAEF,QAAO;EACL,MAAM;EACN,SAAS,UAAU,KAAK,GAAG;EAC3B,aACE,IAAI,SAAe,YAAY;AAC7B,cAAW,YAAY,SAAS,CAAC;IACjC;EACL"}
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
import { createRequire } from "node:module";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
//#region src/mcp/chii-relay.ts
|
|
4
|
+
/**
|
|
5
|
+
* Boots the local Chii relay server.
|
|
6
|
+
*
|
|
7
|
+
* Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome
|
|
8
|
+
* WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.
|
|
9
|
+
* The relay accepts a `target` websocket from the phone's injected `target.js`
|
|
10
|
+
* and `client` websockets from CDP frontends (our MCP connection).
|
|
11
|
+
*
|
|
12
|
+
* Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app
|
|
13
|
+
* entries.
|
|
14
|
+
*
|
|
15
|
+
* TOTP auth (relay-side, authoritative gate):
|
|
16
|
+
* When `verifyAuth` is provided, this module registers an HTTP upgrade
|
|
17
|
+
* listener on the server BEFORE calling `chii.start({server})`. Node's
|
|
18
|
+
* `http.Server` allows multiple 'upgrade' listeners; the first to call
|
|
19
|
+
* `socket.destroy()` wins. Invalid auth → 401 + destroy (chii never sees
|
|
20
|
+
* the connection). Valid auth → return without side-effect (chii handles it).
|
|
21
|
+
*
|
|
22
|
+
* Threat model: "URL leak" — someone obtains the tunnel URL (Slack paste, QR
|
|
23
|
+
* screenshot, shoulder-surfing) but does not have the shared TOTP secret.
|
|
24
|
+
* Rotating 6-digit code makes the URL stale after 30 s.
|
|
25
|
+
* A determined attacker who extracts the secret from the dogfood bundle can
|
|
26
|
+
* still compute valid codes; that is out of scope (see umbrella CLAUDE.md §4).
|
|
27
|
+
*
|
|
28
|
+
* SECRET-HANDLING: The secret value and computed TOTP codes MUST NOT appear
|
|
29
|
+
* in any log, error message, or process output. `verifyAuth` is a black-box
|
|
30
|
+
* predicate from the caller's perspective; this module only forwards pass/fail.
|
|
31
|
+
*/
|
|
32
|
+
const require = createRequire(import.meta.url);
|
|
33
|
+
function loadChiiServer() {
|
|
34
|
+
const mod = require("chii");
|
|
35
|
+
if (typeof mod === "object" && mod !== null && "start" in mod && typeof mod.start === "function") return mod;
|
|
36
|
+
throw new Error("chii server module did not expose start()");
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* Starts the Chii relay and resolves once listening.
|
|
40
|
+
*
|
|
41
|
+
* Default port is 0 (OS-assigned). With port 0 the OS picks a free ephemeral
|
|
42
|
+
* port on every start, so a stale cloudflared orphan holding any particular
|
|
43
|
+
* port cannot cause EADDRINUSE. The resolved `ChiiRelay.port` and `baseUrl`
|
|
44
|
+
* always reflect the actual bound port.
|
|
45
|
+
*
|
|
46
|
+
* chii.start() is called with `server` (our pre-created httpServer) BEFORE
|
|
47
|
+
* httpServer.listen(). This is intentional: chii attaches its Koa handler and
|
|
48
|
+
* WS upgrade listener to the server object, but the actual TCP bind is
|
|
49
|
+
* performed by our httpServer.listen() call below. The `port`/`domain` values
|
|
50
|
+
* passed to chii.start() are used for display/banner purposes inside chii and
|
|
51
|
+
* do not affect which port the server binds. The connection path (clients
|
|
52
|
+
* connecting to `relay.baseUrl`) always uses the post-listen confirmed port.
|
|
53
|
+
*/
|
|
54
|
+
async function startChiiRelay(options = {}) {
|
|
55
|
+
const requestedPort = options.port ?? 0;
|
|
56
|
+
const host = options.host ?? "127.0.0.1";
|
|
57
|
+
const { verifyAuth } = options;
|
|
58
|
+
const httpServer = createServer();
|
|
59
|
+
if (verifyAuth) httpServer.on("upgrade", (req, socket) => {
|
|
60
|
+
if (!verifyAuth(req)) {
|
|
61
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n");
|
|
62
|
+
socket.destroy();
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
});
|
|
66
|
+
await loadChiiServer().start({
|
|
67
|
+
server: httpServer,
|
|
68
|
+
domain: `${host}:${requestedPort}`,
|
|
69
|
+
port: requestedPort
|
|
70
|
+
});
|
|
71
|
+
const actualPort = await new Promise((resolve, reject) => {
|
|
72
|
+
httpServer.once("error", reject);
|
|
73
|
+
httpServer.listen(requestedPort, host, () => {
|
|
74
|
+
httpServer.off("error", reject);
|
|
75
|
+
resolve(httpServer.address().port);
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
return {
|
|
79
|
+
port: actualPort,
|
|
80
|
+
baseUrl: `http://${host}:${actualPort}`,
|
|
81
|
+
close: () => new Promise((resolve) => {
|
|
82
|
+
httpServer.close(() => resolve());
|
|
83
|
+
})
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
//#endregion
|
|
87
|
+
export { startChiiRelay };
|
|
88
|
+
|
|
89
|
+
//# sourceMappingURL=chii-relay-itXOz7kS.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"chii-relay-itXOz7kS.js","names":[],"sources":["../src/mcp/chii-relay.ts"],"sourcesContent":["/**\n * Boots the local Chii relay server.\n *\n * Chii (liriliri/chii) is a chobitsu-based CDP relay that lets non-Chrome\n * WebViews (iOS WKWebView / Android WebView — i.e. the Toss app) expose CDP.\n * The relay accepts a `target` websocket from the phone's injected `target.js`\n * and `client` websockets from CDP frontends (our MCP connection).\n *\n * Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app\n * entries.\n *\n * TOTP auth (relay-side, authoritative gate):\n * When `verifyAuth` is provided, this module registers an HTTP upgrade\n * listener on the server BEFORE calling `chii.start({server})`. Node's\n * `http.Server` allows multiple 'upgrade' listeners; the first to call\n * `socket.destroy()` wins. Invalid auth → 401 + destroy (chii never sees\n * the connection). Valid auth → return without side-effect (chii handles it).\n *\n * Threat model: \"URL leak\" — someone obtains the tunnel URL (Slack paste, QR\n * screenshot, shoulder-surfing) but does not have the shared TOTP secret.\n * Rotating 6-digit code makes the URL stale after 30 s.\n * A determined attacker who extracts the secret from the dogfood bundle can\n * still compute valid codes; that is out of scope (see umbrella CLAUDE.md §4).\n *\n * SECRET-HANDLING: The secret value and computed TOTP codes MUST NOT appear\n * in any log, error message, or process output. `verifyAuth` is a black-box\n * predicate from the caller's perspective; this module only forwards pass/fail.\n */\n\nimport { createServer, type IncomingMessage, type Server } from 'node:http';\nimport { createRequire } from 'node:module';\nimport type { AddressInfo } from 'node:net';\nimport type { Duplex } from 'node:stream';\n\nconst require = createRequire(import.meta.url);\n\n/** `chii/server` is CommonJS and shipped without TypeScript types. */\ninterface ChiiServerModule {\n start(options: {\n port?: number;\n host?: string;\n domain?: string;\n server?: Server;\n basePath?: string;\n }): Promise<void>;\n}\n\nfunction loadChiiServer(): ChiiServerModule {\n // `chii`'s package `main` is `./server/index.js`, exposing `{ start }`.\n const mod: unknown = require('chii');\n if (\n typeof mod === 'object' &&\n mod !== null &&\n 'start' in mod &&\n typeof (mod as { start: unknown }).start === 'function'\n ) {\n return mod as ChiiServerModule;\n }\n throw new Error('chii server module did not expose start()');\n}\n\nexport interface ChiiRelay {\n port: number;\n /** Base URL for the relay HTTP/WS server, e.g. `http://127.0.0.1:54321`. */\n baseUrl: string;\n close(): Promise<void>;\n}\n\nexport interface StartChiiRelayOptions {\n /**\n * Local port for the relay. Default 0 (OS-assigned ephemeral port).\n *\n * Using 0 means the OS picks a free port — this is the safe default because\n * a stale cloudflared child process (PPID 1, orphaned after SIGKILL) may still\n * be holding a fixed port. A fixed port causes EADDRINUSE on the next startup,\n * which makes the MCP handshake fail with -32000. With port 0 the new relay\n * always gets a fresh port, making any orphaned process harmless.\n *\n * Pass an explicit number to restore fixed-port behaviour (backwards-compatible).\n */\n port?: number;\n /** Bind host. Default 127.0.0.1 (tunnel reaches it locally). */\n host?: string;\n /**\n * Optional auth predicate for WebSocket upgrade requests.\n *\n * When provided, every inbound WebSocket upgrade is checked by calling\n * `verifyAuth(req)` before Chii processes it. Return `true` to allow the\n * upgrade; return `false` to reject with HTTP 401 and destroy the socket.\n *\n * The predicate MUST NOT log the secret or any TOTP code — it is a black-box\n * from this module's perspective.\n *\n * @param req - The raw HTTP `IncomingMessage` from the upgrade handshake.\n * Inspect `req.url` for query parameters (e.g. `at=<code>`).\n * @returns `true` if the upgrade is authorised, `false` to reject.\n */\n verifyAuth?: (req: IncomingMessage) => boolean;\n}\n\n/**\n * Starts the Chii relay and resolves once listening.\n *\n * Default port is 0 (OS-assigned). With port 0 the OS picks a free ephemeral\n * port on every start, so a stale cloudflared orphan holding any particular\n * port cannot cause EADDRINUSE. The resolved `ChiiRelay.port` and `baseUrl`\n * always reflect the actual bound port.\n *\n * chii.start() is called with `server` (our pre-created httpServer) BEFORE\n * httpServer.listen(). This is intentional: chii attaches its Koa handler and\n * WS upgrade listener to the server object, but the actual TCP bind is\n * performed by our httpServer.listen() call below. The `port`/`domain` values\n * passed to chii.start() are used for display/banner purposes inside chii and\n * do not affect which port the server binds. The connection path (clients\n * connecting to `relay.baseUrl`) always uses the post-listen confirmed port.\n */\nexport async function startChiiRelay(options: StartChiiRelayOptions = {}): Promise<ChiiRelay> {\n const requestedPort = options.port ?? 0;\n const host = options.host ?? '127.0.0.1';\n const { verifyAuth } = options;\n\n const httpServer = createServer();\n\n // Register our auth listener BEFORE chii.start() so it fires first.\n // Node's http.Server emits 'upgrade' to all listeners in registration order;\n // the first to destroy() the socket wins. Valid requests return without\n // side-effect so chii's own upgrade handler takes over normally.\n //\n // We only register when verifyAuth is provided so the no-auth path is\n // zero-overhead for tests and local-only dev sessions.\n if (verifyAuth) {\n httpServer.on('upgrade', (req: IncomingMessage, socket: Duplex) => {\n if (!verifyAuth(req)) {\n // Reject: send a minimal HTTP 401 response and close the socket.\n // We do NOT log req.url or any auth param here to avoid leaking codes.\n socket.write('HTTP/1.1 401 Unauthorized\\r\\nContent-Length: 0\\r\\n\\r\\n');\n socket.destroy();\n // Early return — chii's handler is NOT called for this socket.\n return;\n }\n // Auth passed: no-op. Chii's upgrade listener (registered below by\n // chii.start) will handle the rest.\n });\n }\n\n const chii = loadChiiServer();\n // Passing an existing `server` makes chii attach its Koa handler + WS upgrade\n // to our HTTP server rather than creating its own listener.\n // Note: port/domain here are display-only inside chii — the TCP bind is ours.\n await chii.start({ server: httpServer, domain: `${host}:${requestedPort}`, port: requestedPort });\n\n const actualPort = await new Promise<number>((resolve, reject) => {\n httpServer.once('error', reject);\n httpServer.listen(requestedPort, host, () => {\n httpServer.off('error', reject);\n // httpServer.address() is non-null immediately after the listen callback.\n const addr = httpServer.address() as AddressInfo;\n resolve(addr.port);\n });\n });\n\n return {\n port: actualPort,\n baseUrl: `http://${host}:${actualPort}`,\n close: () =>\n new Promise<void>((resolve) => {\n httpServer.close(() => resolve());\n }),\n };\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAkCA,MAAM,UAAU,cAAc,OAAO,KAAK,IAAI;AAa9C,SAAS,iBAAmC;CAE1C,MAAM,MAAe,QAAQ,OAAO;AACpC,KACE,OAAO,QAAQ,YACf,QAAQ,QACR,WAAW,OACX,OAAQ,IAA2B,UAAU,WAE7C,QAAO;AAET,OAAM,IAAI,MAAM,4CAA4C;;;;;;;;;;;;;;;;;;AA0D9D,eAAsB,eAAe,UAAiC,EAAE,EAAsB;CAC5F,MAAM,gBAAgB,QAAQ,QAAQ;CACtC,MAAM,OAAO,QAAQ,QAAQ;CAC7B,MAAM,EAAE,eAAe;CAEvB,MAAM,aAAa,cAAc;AASjC,KAAI,WACF,YAAW,GAAG,YAAY,KAAsB,WAAmB;AACjE,MAAI,CAAC,WAAW,IAAI,EAAE;AAGpB,UAAO,MAAM,yDAAyD;AACtE,UAAO,SAAS;AAEhB;;GAIF;AAOJ,OAJa,gBAAgB,CAIlB,MAAM;EAAE,QAAQ;EAAY,QAAQ,GAAG,KAAK,GAAG;EAAiB,MAAM;EAAe,CAAC;CAEjG,MAAM,aAAa,MAAM,IAAI,SAAiB,SAAS,WAAW;AAChE,aAAW,KAAK,SAAS,OAAO;AAChC,aAAW,OAAO,eAAe,YAAY;AAC3C,cAAW,IAAI,SAAS,OAAO;AAG/B,WADa,WAAW,SAAS,CACpB,KAAK;IAClB;GACF;AAEF,QAAO;EACL,MAAM;EACN,SAAS,UAAU,KAAK,GAAG;EAC3B,aACE,IAAI,SAAe,YAAY;AAC7B,cAAW,YAAY,SAAS,CAAC;IACjC;EACL"}
|