@ait-co/devtools 0.1.55 → 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 +62 -11
- package/README.md +61 -10
- 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/panel/index.js +2 -2
- 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
|
@@ -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
|
|
|
@@ -248,7 +252,7 @@ module.exports = {
|
|
|
248
252
|
| `forceEnable` | `boolean` | `false` | Enable devtools even in production |
|
|
249
253
|
| `mock` | `boolean` | `true` (dev) / `false` (prod+forceEnable) | Enable mock alias |
|
|
250
254
|
| `mcp` | `boolean` | `false` | Add an MCP state endpoint to the Vite dev server (Vite only — see [MCP Server](#mcp-server)) |
|
|
251
|
-
| `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** |
|
|
252
256
|
|
|
253
257
|
```ts
|
|
254
258
|
aitDevtools.vite({ panel: false }); // mock only, no panel
|
|
@@ -256,6 +260,7 @@ aitDevtools.vite({ forceEnable: true }); // enable in production (mock OFF by de
|
|
|
256
260
|
aitDevtools.vite({ forceEnable: true, mock: true }); // enable mock in production too
|
|
257
261
|
aitDevtools.vite({ mcp: true }); // enable MCP endpoint for AI agents
|
|
258
262
|
aitDevtools.vite({ tunnel: true }); // expose dev server at *.trycloudflare.com
|
|
263
|
+
aitDevtools.vite({ tunnel: { cdp: true } }); // real-device preview + on-device CDP debugging
|
|
259
264
|
```
|
|
260
265
|
|
|
261
266
|
## Production builds
|
|
@@ -324,6 +329,8 @@ export default defineConfig({
|
|
|
324
329
|
|
|
325
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).
|
|
326
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
|
+
|
|
327
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:
|
|
328
335
|
|
|
329
336
|
```json
|
|
@@ -938,14 +945,58 @@ AI coding agents (Claude Code, Cursor, etc.) can observe a running mini-app dire
|
|
|
938
945
|
|
|
939
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`).
|
|
940
947
|
|
|
941
|
-
| Mode + target | Invocation | Env
|
|
948
|
+
| Mode + target | Invocation | Env vars | Target | Tools |
|
|
942
949
|
|---|---|---|---|---|
|
|
943
|
-
| `--
|
|
944
|
-
| `--mode=debug --target=relay`
|
|
945
|
-
| `--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 |
|
|
946
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) |
|
|
947
955
|
|
|
948
|
-
`--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.
|
|
949
1000
|
|
|
950
1001
|
### Debug mode (CDP via Chii)
|
|
951
1002
|
|
|
@@ -955,7 +1006,7 @@ Read-only tools only. Tools are registered in two tiers based on attach state
|
|
|
955
1006
|
|
|
956
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.
|
|
957
1008
|
|
|
958
|
-
Environment 3 (dogfood relay):
|
|
1009
|
+
Environment 3 (dogfood relay) — `start_debug({mode: 'staging'})` is the primary in-session entry:
|
|
959
1010
|
|
|
960
1011
|
```json
|
|
961
1012
|
{
|
|
@@ -971,7 +1022,7 @@ Environment 3 (dogfood relay):
|
|
|
971
1022
|
}
|
|
972
1023
|
```
|
|
973
1024
|
|
|
974
|
-
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:
|
|
975
1026
|
|
|
976
1027
|
```json
|
|
977
1028
|
{
|
|
@@ -987,7 +1038,7 @@ Environment 4 (LIVE relay, LIVE guard enabled):
|
|
|
987
1038
|
}
|
|
988
1039
|
```
|
|
989
1040
|
|
|
990
|
-
|
|
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**.
|
|
991
1042
|
|
|
992
1043
|
| Tool | CDP / AIT backing | Description |
|
|
993
1044
|
|---|---|---|
|
package/README.md
CHANGED
|
@@ -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
|
|
|
@@ -248,7 +252,7 @@ module.exports = {
|
|
|
248
252
|
| `forceEnable` | `boolean` | `false` | production에서도 devtools 활성화 |
|
|
249
253
|
| `mock` | `boolean` | `true` (dev) / `false` (prod+forceEnable) | mock alias 활성화 여부 |
|
|
250
254
|
| `mcp` | `boolean` | `false` | Vite dev server에 MCP state endpoint 추가 (Vite 전용, [MCP 섹션](#mcp-server) 참조) |
|
|
251
|
-
| `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 모드 전용** |
|
|
252
256
|
|
|
253
257
|
```ts
|
|
254
258
|
aitDevtools.vite({ panel: false }); // Panel 없이 mock만 사용
|
|
@@ -256,6 +260,7 @@ aitDevtools.vite({ forceEnable: true }); // production에서도 활성화 (mock
|
|
|
256
260
|
aitDevtools.vite({ forceEnable: true, mock: true }); // production에서 mock도 활성화
|
|
257
261
|
aitDevtools.vite({ mcp: true }); // AI 에이전트용 MCP endpoint 활성화
|
|
258
262
|
aitDevtools.vite({ tunnel: true }); // dev 서버를 *.trycloudflare.com으로 노출
|
|
263
|
+
aitDevtools.vite({ tunnel: { cdp: true } }); // 실기기 미리보기 + on-device CDP 디버깅
|
|
259
264
|
```
|
|
260
265
|
|
|
261
266
|
## Production 빌드
|
|
@@ -324,6 +329,8 @@ export default defineConfig({
|
|
|
324
329
|
|
|
325
330
|
> `process.env.AIT_TUNNEL`은 `vite.config.ts`를 로드하는 시점(= vite 프로세스 기동 시)에 평가됩니다. 따라서 env 변수는 **vite를 띄우기 전에** 설정되어 있어야 합니다 (아래 (c)의 `dev:phone` 스크립트가 이를 자동으로 해결합니다).
|
|
326
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
|
+
|
|
327
334
|
(b) **`package.json`에 pnpm 10+ 빌드 스크립트 허용** — pnpm은 보안상 dependency의 postinstall을 기본 차단합니다. `cloudflared`는 postinstall에서 바이너리(~38 MB)를 받으므로 명시 허용 필요:
|
|
328
335
|
|
|
329
336
|
```json
|
|
@@ -970,12 +977,56 @@ AI 코딩 에이전트(Claude Code, Cursor 등)가 [MCP(Model Context Protocol)]
|
|
|
970
977
|
|
|
971
978
|
| 모드 + 타깃 | 호출 | 환경 변수 | 대상 | tool |
|
|
972
979
|
|---|---|---|---|---|
|
|
973
|
-
| `--
|
|
974
|
-
| `--mode=debug --target=relay`
|
|
975
|
-
| `--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) | 동일 |
|
|
976
984
|
| `--mode=dev` | `devtools-mcp --mode=dev` | `MCP_ENV=mock` (자동) | 실행 중인 Vite dev server의 mock state (AIT.* 전용, CDP 없음) | `AIT.*` (+ `devtools_get_mock_state` alias) |
|
|
977
985
|
|
|
978
|
-
`--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 호스트)에서 완성됩니다.
|
|
979
1030
|
|
|
980
1031
|
### Debug 모드 (CDP via Chii)
|
|
981
1032
|
|
|
@@ -993,7 +1044,7 @@ tunnel로 공개 `wss://*.trycloudflare.com` URL을 발급한 뒤 QR을 터미
|
|
|
993
1044
|
에이전트가 `chrome-devtools-mcp` 호환 tool로 console/network/page 상태를 read합니다. 사람이 폰을
|
|
994
1045
|
지켜볼 필요 없이 회귀를 단독 진단하는 것이 목표입니다.
|
|
995
1046
|
|
|
996
|
-
환경 3 (dogfood relay):
|
|
1047
|
+
환경 3 (dogfood relay) — `start_debug({mode: 'staging'})`이 세션 내 1차 진입:
|
|
997
1048
|
|
|
998
1049
|
```json
|
|
999
1050
|
{
|
|
@@ -1009,7 +1060,7 @@ tunnel로 공개 `wss://*.trycloudflare.com` URL을 발급한 뒤 QR을 터미
|
|
|
1009
1060
|
}
|
|
1010
1061
|
```
|
|
1011
1062
|
|
|
1012
|
-
환경 4 (LIVE relay, LIVE guard 활성화):
|
|
1063
|
+
환경 4 (LIVE relay, LIVE guard 활성화) — `start_debug({mode: 'live', confirm: true})`이 세션 내 1차 진입:
|
|
1013
1064
|
|
|
1014
1065
|
```json
|
|
1015
1066
|
{
|
|
@@ -1025,7 +1076,7 @@ tunnel로 공개 `wss://*.trycloudflare.com` URL을 발급한 뒤 QR을 터미
|
|
|
1025
1076
|
}
|
|
1026
1077
|
```
|
|
1027
1078
|
|
|
1028
|
-
`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)`)을 명시해야 합니다**.
|
|
1029
1080
|
|
|
1030
1081
|
| Tool | CDP / AIT 백킹 | 설명 |
|
|
1031
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"}
|
package/dist/in-app/index.d.ts
CHANGED
|
@@ -18,14 +18,20 @@
|
|
|
18
18
|
* `false` and could never pass. Layer A is the consumer guard; B and C are
|
|
19
19
|
* here.
|
|
20
20
|
*
|
|
21
|
-
* Layer B has two parts
|
|
21
|
+
* Layer B has two parts:
|
|
22
22
|
* B1 — host allowlist: `hostname` must be a `*.private-apps.tossmini.com`
|
|
23
|
-
* subdomain
|
|
24
|
-
*
|
|
23
|
+
* subdomain (Toss dogfood entry) OR a `*.trycloudflare.com` host (env 2
|
|
24
|
+
* PWA dev tunnel). The Toss app serves dogfood / private mini-apps from
|
|
25
|
+
* a separate `private-apps` host; a production (`intoss://`) entry is
|
|
25
26
|
* served from `*.apps.tossmini.com` WITHOUT the `private-apps` segment.
|
|
26
27
|
* This is the security gate against a dogfood build that somehow lands
|
|
27
28
|
* on a production entry — see the comment on {@link isPrivateAppsHost}.
|
|
28
|
-
*
|
|
29
|
+
* The env 2 tunnel host is allowed because it has no production runtime
|
|
30
|
+
* (mock SDK, the developer's own dev server) — see {@link
|
|
31
|
+
* isTrycloudflareHost}.
|
|
32
|
+
* B2 — entry query: `_deploymentId` must be present and non-empty. Applies to
|
|
33
|
+
* the Toss path only; the env 2 tunnel has no deployed bundle, so B2 is
|
|
34
|
+
* skipped for `*.trycloudflare.com` hosts.
|
|
29
35
|
*
|
|
30
36
|
* Layer C — opt-in + relay + optional TOTP auth:
|
|
31
37
|
* C1 — opt-in: `debug=1` must be present.
|
|
@@ -49,16 +55,23 @@
|
|
|
49
55
|
*
|
|
50
56
|
* Decision matrix (gate only runs in a debug build — Layer A already passed):
|
|
51
57
|
*
|
|
52
|
-
* host
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
*
|
|
56
|
-
*
|
|
57
|
-
*
|
|
58
|
-
*
|
|
58
|
+
* host | _deploymentId | debug=1 | relay ok | TOTP ok* | result
|
|
59
|
+
* neither | (any) | (any) | (any) | (any) | BLOCKED (host)
|
|
60
|
+
* private-apps| absent | (any) | (any) | (any) | BLOCKED (entry)
|
|
61
|
+
* private-apps| present | absent | (any) | (any) | BLOCKED (opt-in)
|
|
62
|
+
* private-apps| present | present | invalid | (any) | BLOCKED (invalid-relay)
|
|
63
|
+
* private-apps| present | present | valid | fail* | BLOCKED (auth)
|
|
64
|
+
* private-apps| present | present | valid | pass/n/a | ATTACH
|
|
65
|
+
* trycloudflare| (skipped) | absent | (any) | (any) | BLOCKED (opt-in)
|
|
66
|
+
* trycloudflare| (skipped) | present | invalid | (any) | BLOCKED (invalid-relay)
|
|
67
|
+
* trycloudflare| (skipped) | present | valid | fail* | BLOCKED (auth)
|
|
68
|
+
* trycloudflare| (skipped) | present | valid | pass/n/a | ATTACH
|
|
59
69
|
*
|
|
60
70
|
* * "TOTP ok" column only applies when `verifyTotpCode` is provided.
|
|
61
71
|
* When no verifier is injected, TOTP check is skipped entirely.
|
|
72
|
+
* For trycloudflare (env 2 tunnel) hosts B1 is bypassed and B2 is skipped;
|
|
73
|
+
* C1/C2/C3 still apply identically. The ATTACH result carries
|
|
74
|
+
* `deploymentId: ''` for tunnel hosts.
|
|
62
75
|
*/
|
|
63
76
|
/** Shape returned when the gate allows attachment. */
|
|
64
77
|
interface GateResultAttach {
|
|
@@ -153,6 +166,26 @@ interface GateInput {
|
|
|
153
166
|
* bare `private-apps.tossmini.com` (no mini-app subdomain) does not match.
|
|
154
167
|
*/
|
|
155
168
|
declare function isPrivateAppsHost(hostname: string): boolean;
|
|
169
|
+
/**
|
|
170
|
+
* The host suffix Cloudflare quick-tunnels use — the env 2 (PWA) entry.
|
|
171
|
+
*
|
|
172
|
+
* Env 2 serves the local Vite dev server through a `*.trycloudflare.com` quick
|
|
173
|
+
* tunnel (`src/unplugin/tunnel.ts`). It has no Toss app, no `intoss-private://`
|
|
174
|
+
* scheme, and — critically — no production runtime: the SDK is the devtools
|
|
175
|
+
* mock, and the page is the developer's own dev build. The Layer B1 safety net
|
|
176
|
+
* (which stops a dogfood build that lands on a Toss *production* host from
|
|
177
|
+
* attaching) has nothing to protect against here, because env 2 has no
|
|
178
|
+
* production host. So a trycloudflare host is allowed past B1 — but ONLY past
|
|
179
|
+
* B1: the remaining layers (C1 opt-in, C2 relay, C3 TOTP) still apply, so a
|
|
180
|
+
* leaked tunnel URL is still blocked by TOTP exactly as on the Toss path.
|
|
181
|
+
*
|
|
182
|
+
* The match is the same exact-suffix `endsWith` check as
|
|
183
|
+
* {@link isPrivateAppsHost} — never a substring `.includes()`, which would
|
|
184
|
+
* accept an attacker-controlled `evil.trycloudflare.com.example.com`. The
|
|
185
|
+
* leading `.` forces a real subdomain label, so a bare `trycloudflare.com`
|
|
186
|
+
* (no tunnel subdomain) does not match.
|
|
187
|
+
*/
|
|
188
|
+
declare function isTrycloudflareHost(hostname: string): boolean;
|
|
156
189
|
/**
|
|
157
190
|
* Pure function that evaluates the runtime debug activation layers (B and C).
|
|
158
191
|
*
|
|
@@ -235,5 +268,5 @@ declare function maybeAttach(gateResult?: GateResult): void;
|
|
|
235
268
|
*/
|
|
236
269
|
declare function checkDebugGate(): GateResult;
|
|
237
270
|
//#endregion
|
|
238
|
-
export { type GateInput, type GateResult, type GateResultAttach, type GateResultBlocked, checkDebugGate, deriveTargetScriptUrl, evaluateDebugGate, isPrivateAppsHost, maybeAttach };
|
|
271
|
+
export { type GateInput, type GateResult, type GateResultAttach, type GateResultBlocked, checkDebugGate, deriveTargetScriptUrl, evaluateDebugGate, isPrivateAppsHost, isTrycloudflareHost, maybeAttach };
|
|
239
272
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/in-app/gate.ts","../../src/in-app/attach.ts","../../src/in-app/index.ts"],"mappings":";;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/in-app/gate.ts","../../src/in-app/attach.ts","../../src/in-app/index.ts"],"mappings":";;AA4EA;;;;;;;;;AASA;;;;;AAmBA;;;;;AAQA;;;;;;;;;;;AA8EA;;;;;AAuBA;;;;;AAyBA;;;;;;;;;;;;AChNA;;;;;AAmCA;;;;;;;;ACrBA;;;;;;;;;UFgCiB,gBAAA;EAAA,SACN,MAAA;;WAEA,QAAA;;WAEA,YAAA;AAAA;;UAIM,iBAAA;EAAA,SACN,MAAA;;;;;;;;;;;;;;;WAeA,MAAA;AAAA;AAAA,KAGC,UAAA,GAAa,gBAAA,GAAmB,iBAAA;;;;;;;UAQ3B,SAAA;;;;;;;;;;;;;WAaN,QAAA;;;;;;;WAQA,YAAA,EAAc,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;WA0Bd,cAAA,IAAkB,IAAA;AAAA;;;;;;;;;;;;iBA+Bb,iBAAA,CAAkB,QAAA;;;;;;;;;;;;;;;;;;;;iBAuBlB,mBAAA,CAAoB,QAAA;;;;;;;;;;;;;;;;;;;;;;iBAyBpB,iBAAA,CAAkB,KAAA,EAAO,SAAA,GAAY,UAAA;;;;;;AAtIrD;;;;;AAQA;;;;;;;iBClFgB,qBAAA,CAAsB,QAAA;;;;ADgKtC;;;;;AAuBA;;;;;AAyBA;;;;;;;;;;iBC7KgB,WAAA,CAAY,UAAA,GAAY,UAAA;;;;;;;AD6HxC;;;;;AAuBA;;;;;iBEzKgB,cAAA,CAAA,GAAkB,UAAA"}
|