@ait-co/devtools 0.1.45 → 0.1.46
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 +4 -0
- package/README.md +4 -0
- package/dist/mcp/cli.js +205 -97
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +2 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +2 -2
- package/package.json +1 -1
package/README.en.md
CHANGED
|
@@ -101,6 +101,10 @@ The page on the phone died (OOM, JS exception, or native bridge crash). Relaunch
|
|
|
101
101
|
|
|
102
102
|
When `call_sdk` returns `ok: false, error: "window.__sdkCall is not available"`, a non-dogfood bundle is loaded. Redeploy through the dogfood channel with the `__DEBUG_BUILD__` flag enabled and try again. This error is the expected result in environment 2 (PWA). (Related: [#285](https://github.com/apps-in-toss-community/devtools/issues/285))
|
|
103
103
|
|
|
104
|
+
**"QR scanned but auth rejected" — TOTP code expired**
|
|
105
|
+
|
|
106
|
+
When `AIT_DEBUG_TOTP_SECRET` is set, `build_attach_url` automatically splices the current one-time TOTP code (`at=`) into the returned `attachUrl`. The code is valid for a 30-second step. Scanning after `totp.expiresAt` causes the relay to reject the request. Fix: call `build_attach_url` again to get a fresh URL and QR, then scan within 30 seconds.
|
|
107
|
+
|
|
104
108
|
---
|
|
105
109
|
|
|
106
110
|
## Install
|
package/README.md
CHANGED
|
@@ -101,6 +101,10 @@ cloudflared quick tunnel은 수 시간 후 drop될 수 있습니다. `devtools-m
|
|
|
101
101
|
|
|
102
102
|
`call_sdk` 호출 시 `ok: false, error: "window.__sdkCall is not available"` 에러가 뜨면 dogfood 빌드가 아닌 일반 번들이 로드된 것입니다. `__DEBUG_BUILD__` 플래그가 켜진 dogfood 채널로 재배포 후 다시 시도하세요. 환경 2(PWA)에서는 이 에러가 예상 결과입니다. (관련: [#285](https://github.com/apps-in-toss-community/devtools/issues/285))
|
|
103
103
|
|
|
104
|
+
**"QR 스캔했는데 인증 실패" — TOTP 만료**
|
|
105
|
+
|
|
106
|
+
`AIT_DEBUG_TOTP_SECRET` 설정 시 `build_attach_url`이 반환하는 attachUrl에는 30초 유효 TOTP 코드(`at=`)가 자동으로 포함됩니다. 응답의 `totp.expiresAt` 이후 스캔하면 relay가 인증을 거부합니다. 해결: `build_attach_url`을 재호출해 새 URL과 QR을 발급받은 뒤 30초 이내에 스캔하세요.
|
|
107
|
+
|
|
104
108
|
---
|
|
105
109
|
|
|
106
110
|
## 설치
|
package/dist/mcp/cli.js
CHANGED
|
@@ -848,6 +848,51 @@ var AutoDevtoolsOpener = class {
|
|
|
848
848
|
}
|
|
849
849
|
};
|
|
850
850
|
//#endregion
|
|
851
|
+
//#region src/mcp/envelope.ts
|
|
852
|
+
/**
|
|
853
|
+
* Returns `true` when `AIT_MCP_COMPAT=chrome-devtools` is set, which bypasses
|
|
854
|
+
* envelope wrapping and returns raw payloads (0.1.x back-compat).
|
|
855
|
+
*/
|
|
856
|
+
function isCompatMode() {
|
|
857
|
+
return process.env.AIT_MCP_COMPAT === "chrome-devtools";
|
|
858
|
+
}
|
|
859
|
+
/**
|
|
860
|
+
* Maps `McpEnvironment` to `EnvelopeEnv`. After #307 these are the same
|
|
861
|
+
* union (`mock | relay-dev | relay-live`), so this is identity — kept as a
|
|
862
|
+
* named export for surface stability if envelope env diverges in the future.
|
|
863
|
+
*/
|
|
864
|
+
function toEnvelopeEnv(env) {
|
|
865
|
+
return env;
|
|
866
|
+
}
|
|
867
|
+
/**
|
|
868
|
+
* Wraps `data` in a `ToolEnvelope<T>` **unless** compat mode is active, in
|
|
869
|
+
* which case `data` is returned as-is.
|
|
870
|
+
*
|
|
871
|
+
* Use this at every tool call-site in `debug-server.ts` and `server.ts`.
|
|
872
|
+
*
|
|
873
|
+
* @example
|
|
874
|
+
* ```ts
|
|
875
|
+
* return jsonResult(wrapEnvelope(listPages(connection, tunnel), {
|
|
876
|
+
* tool: 'list_pages',
|
|
877
|
+
* env: resolveEnvironment(),
|
|
878
|
+
* attached: connection.listTargets().length > 0,
|
|
879
|
+
* }));
|
|
880
|
+
* ```
|
|
881
|
+
*/
|
|
882
|
+
function wrapEnvelope(data, ctx) {
|
|
883
|
+
if (isCompatMode()) return data;
|
|
884
|
+
return {
|
|
885
|
+
ok: true,
|
|
886
|
+
data,
|
|
887
|
+
meta: {
|
|
888
|
+
tool: ctx.tool,
|
|
889
|
+
env: toEnvelopeEnv(ctx.env),
|
|
890
|
+
attached: ctx.attached,
|
|
891
|
+
contentType: ctx.contentType ?? "json"
|
|
892
|
+
}
|
|
893
|
+
};
|
|
894
|
+
}
|
|
895
|
+
//#endregion
|
|
851
896
|
//#region src/mcp/environment.ts
|
|
852
897
|
/**
|
|
853
898
|
* Returns `true` when the environment is any relay variant (`relay-dev` or
|
|
@@ -2013,6 +2058,83 @@ function warnPassthrough(name) {
|
|
|
2013
2058
|
}
|
|
2014
2059
|
SIGNATURES.map((s) => s.name);
|
|
2015
2060
|
//#endregion
|
|
2061
|
+
//#region src/mcp/totp.ts
|
|
2062
|
+
/**
|
|
2063
|
+
* RFC 6238 TOTP implementation (Node.js, node:crypto only).
|
|
2064
|
+
*
|
|
2065
|
+
* External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used
|
|
2066
|
+
* to keep the dependency surface minimal. This hand-roll is ~30 lines and
|
|
2067
|
+
* covers exactly what relay-side auth needs.
|
|
2068
|
+
*
|
|
2069
|
+
* Algorithm summary (RFC 6238 + RFC 4226):
|
|
2070
|
+
* T = floor(now / 30) — 30-second time step counter
|
|
2071
|
+
* K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)
|
|
2072
|
+
* MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)
|
|
2073
|
+
* offset = MAC[19] & 0x0f
|
|
2074
|
+
* code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits
|
|
2075
|
+
*
|
|
2076
|
+
* Security note (keep this comment accurate):
|
|
2077
|
+
* The baked-in secret in a dogfood build is extractable from the bundle by a
|
|
2078
|
+
* determined reverse engineer. This mechanism raises the bar from
|
|
2079
|
+
* "anyone with the URL" to "URL + bundle extraction + live TOTP calculation".
|
|
2080
|
+
* Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are
|
|
2081
|
+
* blocked; deliberate reverse engineering is not. See threat model in
|
|
2082
|
+
* src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.
|
|
2083
|
+
*
|
|
2084
|
+
* SECRET-HANDLING: secret values and computed codes MUST NOT appear in any
|
|
2085
|
+
* log, error message, or string visible outside this module. Only boolean
|
|
2086
|
+
* pass/fail and reason enum values are safe to surface.
|
|
2087
|
+
*/
|
|
2088
|
+
/** Time step window in seconds (RFC 6238 default). */
|
|
2089
|
+
const TIME_STEP = 30;
|
|
2090
|
+
/** Number of digits in the generated code. */
|
|
2091
|
+
const DIGITS = 6;
|
|
2092
|
+
/**
|
|
2093
|
+
* Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-
|
|
2094
|
+
* clock time.
|
|
2095
|
+
*
|
|
2096
|
+
* @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32
|
|
2097
|
+
* bytes). Must be the output of `generateAttachToken()` or compatible.
|
|
2098
|
+
* @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
|
|
2099
|
+
* @returns A zero-padded 6-digit decimal string, e.g. `"042193"`.
|
|
2100
|
+
*/
|
|
2101
|
+
function generateTotp(secret, when = Date.now()) {
|
|
2102
|
+
const key = Buffer.from(secret, "hex");
|
|
2103
|
+
const counter = Math.max(0, Math.floor(when / 1e3 / TIME_STEP));
|
|
2104
|
+
const counterBuf = Buffer.alloc(8);
|
|
2105
|
+
const hi = Math.floor(counter / 4294967296);
|
|
2106
|
+
const lo = counter >>> 0;
|
|
2107
|
+
counterBuf.writeUInt32BE(hi, 0);
|
|
2108
|
+
counterBuf.writeUInt32BE(lo, 4);
|
|
2109
|
+
const mac = createHmac("sha1", key).update(counterBuf).digest();
|
|
2110
|
+
const offset = mac[19] & 15;
|
|
2111
|
+
return (((mac[offset] & 127) << 24 | (mac[offset + 1] & 255) << 16 | (mac[offset + 2] & 255) << 8 | mac[offset + 3] & 255) % 10 ** DIGITS).toString().padStart(DIGITS, "0");
|
|
2112
|
+
}
|
|
2113
|
+
/**
|
|
2114
|
+
* Verifies a TOTP code against the secret, accepting ±`skew` time steps to
|
|
2115
|
+
* tolerate clock drift between the relay host and the client device.
|
|
2116
|
+
*
|
|
2117
|
+
* Uses `timingSafeEqual` for constant-time comparison to prevent timing
|
|
2118
|
+
* side-channel attacks.
|
|
2119
|
+
*
|
|
2120
|
+
* @param secret - Hex-encoded shared secret.
|
|
2121
|
+
* @param code - The 6-digit code to verify (string or numeric).
|
|
2122
|
+
* @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
|
|
2123
|
+
* @param skew - Number of adjacent steps to accept on either side. Default 1
|
|
2124
|
+
* (accepts T-1, T, T+1 — a 90-second acceptance window).
|
|
2125
|
+
* @returns `true` if the code matches any accepted step, `false` otherwise.
|
|
2126
|
+
*/
|
|
2127
|
+
function verifyTotp(secret, code, when = Date.now(), skew = 1) {
|
|
2128
|
+
const normalised = String(code).padStart(DIGITS, "0");
|
|
2129
|
+
if (normalised.length !== DIGITS || !/^\d{6}$/.test(normalised)) return false;
|
|
2130
|
+
const candidateBuf = Buffer.from(normalised, "utf8");
|
|
2131
|
+
for (let delta = -skew; delta <= skew; delta++) {
|
|
2132
|
+
const expected = generateTotp(secret, when + delta * TIME_STEP * 1e3);
|
|
2133
|
+
if (timingSafeEqual(Buffer.from(expected, "utf8"), candidateBuf)) return true;
|
|
2134
|
+
}
|
|
2135
|
+
return false;
|
|
2136
|
+
}
|
|
2137
|
+
//#endregion
|
|
2016
2138
|
//#region src/mcp/tools.ts
|
|
2017
2139
|
/** Static MCP tool descriptors (name + JSONSchema) for the full debug tool surface. */
|
|
2018
2140
|
const DEBUG_TOOL_DEFINITIONS = [
|
|
@@ -2048,7 +2170,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
2048
2170
|
},
|
|
2049
2171
|
{
|
|
2050
2172
|
name: "build_attach_url",
|
|
2051
|
-
description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Turns an `ait deploy --scheme-only` URL (intoss-private://…?_deploymentId=<uuid>) into a self-attaching deep link by splicing in debug=1 and the live relay URL for this session. Returns the deep link JSON and a unicode QR of that deep link. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Requires the tunnel to be up — call list_pages first. If the tunnel is not up, restart the MCP server: `npx @ait-co/devtools devtools-mcp`. Set wait_for_attach=true to block until the phone scans and a page attaches (polls listTargets up to 30 s by default), then returns the attached page info too. On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). Requires MCP_ENV=relay (set automatically when
|
|
2173
|
+
description: "The tool result already shows the QR to the user directly (Claude Code renders MCP tool output to the user's screen; they press Ctrl+O to expand if it's collapsed). Do NOT re-print or re-render the QR in your reply — that just wastes output tokens. Simply tell the user to scan the QR shown in this tool's output with their phone camera. Turns an `ait deploy --scheme-only` URL (intoss-private://…?_deploymentId=<uuid>) into a self-attaching deep link by splicing in debug=1 and the live relay URL for this session. Returns the deep link JSON and a unicode QR of that deep link. Scan the QR with the phone camera to open the mini-app and attach it to this debug session (QR is the single entry path — no USB cable or platform CLI needed). Requires the tunnel to be up — call list_pages first. If the tunnel is not up, restart the MCP server: `npx @ait-co/devtools devtools-mcp`. Set wait_for_attach=true to block until the phone scans and a page attaches (polls listTargets up to 30 s by default), then returns the attached page info too. On timeout, call build_attach_url again to resume polling. When open_in_browser=true (default), saves the QR as a PNG and opens it in the OS default browser — only works when the MCP server runs on a local GUI machine (not headless/remote containers). Requires MCP_ENV=relay-dev or relay-live (set automatically in debug-mode default).\n\nTOTP auth: when AIT_DEBUG_TOTP_SECRET is set on the MCP server, the returned attachUrl automatically includes the current one-time code (at=<code>) — the URL is single-use for that 30-second step. The response includes a `totp` field with `expiresAt` (ISO timestamp). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh one-time URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.",
|
|
2052
2174
|
inputSchema: {
|
|
2053
2175
|
type: "object",
|
|
2054
2176
|
properties: {
|
|
@@ -2364,20 +2486,50 @@ function listPages(connection, tunnel) {
|
|
|
2364
2486
|
* URL plus this session's live relay. Throws if the tunnel is not up yet (no
|
|
2365
2487
|
* relay URL to splice in) — the caller surfaces that as a tool error.
|
|
2366
2488
|
*
|
|
2489
|
+
* When `AIT_DEBUG_TOTP_SECRET` is set, generates the current TOTP code and
|
|
2490
|
+
* splices it as `at=<code>` into the attach URL. The code is valid for one
|
|
2491
|
+
* 30-second time step (±1 skew accepted by the relay, so the effective window
|
|
2492
|
+
* is up to 90 s). If the scan happens after `totp.expiresAt`, call
|
|
2493
|
+
* `build_attach_url` again to get a fresh code.
|
|
2494
|
+
*
|
|
2367
2495
|
* Also validates the scheme URL's authority. A suspicious authority (empty,
|
|
2368
2496
|
* "web", "localhost", etc.) is surfaced as a non-fatal `authorityWarning` on
|
|
2369
2497
|
* the result so the caller can show a helpful hint without blocking the link
|
|
2370
2498
|
* generation (the warning is consistent with how other validation in
|
|
2371
2499
|
* `buildDeepLinkAttachUrl` works — hard errors for relay, soft warning for
|
|
2372
2500
|
* the scheme authority which is in the caller's input, not ours to own).
|
|
2501
|
+
*
|
|
2502
|
+
* SECRET-HANDLING: `totpSecret` (if provided) is used only to compute a code
|
|
2503
|
+
* and must never appear in any log, error message, or output outside of the
|
|
2504
|
+
* spliced `at=` param in `attachUrl`.
|
|
2505
|
+
*
|
|
2506
|
+
* @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL.
|
|
2507
|
+
* @param tunnel - Current tunnel status from the running debug server.
|
|
2508
|
+
* @param totpSecret - Optional hex-encoded TOTP secret (from
|
|
2509
|
+
* `AIT_DEBUG_TOTP_SECRET`). When provided, the current code is spliced into
|
|
2510
|
+
* the attach URL as `at=<code>`.
|
|
2373
2511
|
*/
|
|
2374
|
-
function buildAttachUrl(schemeUrl, tunnel) {
|
|
2512
|
+
function buildAttachUrl(schemeUrl, tunnel, totpSecret) {
|
|
2375
2513
|
if (!tunnel.up || tunnel.wssUrl === null) throw new Error("tunnel-down: cloudflared 터널이 안 떠 있습니다. MCP 서버를 재시작하거나 잠시 후 list_pages로 터널 상태를 다시 확인하세요.");
|
|
2376
2514
|
const authorityWarning = validateSchemeAuthority(schemeUrl) ?? void 0;
|
|
2515
|
+
let totpCode;
|
|
2516
|
+
let totpMeta;
|
|
2517
|
+
if (totpSecret !== void 0 && totpSecret !== "") {
|
|
2518
|
+
const now = Date.now();
|
|
2519
|
+
totpCode = generateTotp(totpSecret, now);
|
|
2520
|
+
const STEP_SECONDS = 30;
|
|
2521
|
+
const expiresAtMs = (Math.floor(now / 1e3 / STEP_SECONDS) + 1) * STEP_SECONDS * 1e3;
|
|
2522
|
+
totpMeta = {
|
|
2523
|
+
enabled: true,
|
|
2524
|
+
ttlSeconds: STEP_SECONDS,
|
|
2525
|
+
expiresAt: new Date(expiresAtMs).toISOString()
|
|
2526
|
+
};
|
|
2527
|
+
}
|
|
2377
2528
|
return {
|
|
2378
|
-
attachUrl: buildDeepLinkAttachUrl(schemeUrl, tunnel.wssUrl),
|
|
2529
|
+
attachUrl: buildDeepLinkAttachUrl(schemeUrl, tunnel.wssUrl, totpCode),
|
|
2379
2530
|
relayUrl: tunnel.wssUrl,
|
|
2380
|
-
...authorityWarning !== void 0 ? { authorityWarning } : {}
|
|
2531
|
+
...authorityWarning !== void 0 ? { authorityWarning } : {},
|
|
2532
|
+
...totpMeta !== void 0 ? { totp: totpMeta } : {}
|
|
2381
2533
|
};
|
|
2382
2534
|
}
|
|
2383
2535
|
/**
|
|
@@ -3069,83 +3221,6 @@ async function getDiagnostics(input) {
|
|
|
3069
3221
|
};
|
|
3070
3222
|
}
|
|
3071
3223
|
//#endregion
|
|
3072
|
-
//#region src/mcp/totp.ts
|
|
3073
|
-
/**
|
|
3074
|
-
* RFC 6238 TOTP implementation (Node.js, node:crypto only).
|
|
3075
|
-
*
|
|
3076
|
-
* External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used
|
|
3077
|
-
* to keep the dependency surface minimal. This hand-roll is ~30 lines and
|
|
3078
|
-
* covers exactly what relay-side auth needs.
|
|
3079
|
-
*
|
|
3080
|
-
* Algorithm summary (RFC 6238 + RFC 4226):
|
|
3081
|
-
* T = floor(now / 30) — 30-second time step counter
|
|
3082
|
-
* K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)
|
|
3083
|
-
* MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)
|
|
3084
|
-
* offset = MAC[19] & 0x0f
|
|
3085
|
-
* code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits
|
|
3086
|
-
*
|
|
3087
|
-
* Security note (keep this comment accurate):
|
|
3088
|
-
* The baked-in secret in a dogfood build is extractable from the bundle by a
|
|
3089
|
-
* determined reverse engineer. This mechanism raises the bar from
|
|
3090
|
-
* "anyone with the URL" to "URL + bundle extraction + live TOTP calculation".
|
|
3091
|
-
* Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are
|
|
3092
|
-
* blocked; deliberate reverse engineering is not. See threat model in
|
|
3093
|
-
* src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.
|
|
3094
|
-
*
|
|
3095
|
-
* SECRET-HANDLING: secret values and computed codes MUST NOT appear in any
|
|
3096
|
-
* log, error message, or string visible outside this module. Only boolean
|
|
3097
|
-
* pass/fail and reason enum values are safe to surface.
|
|
3098
|
-
*/
|
|
3099
|
-
/** Time step window in seconds (RFC 6238 default). */
|
|
3100
|
-
const TIME_STEP = 30;
|
|
3101
|
-
/** Number of digits in the generated code. */
|
|
3102
|
-
const DIGITS = 6;
|
|
3103
|
-
/**
|
|
3104
|
-
* Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-
|
|
3105
|
-
* clock time.
|
|
3106
|
-
*
|
|
3107
|
-
* @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32
|
|
3108
|
-
* bytes). Must be the output of `generateAttachToken()` or compatible.
|
|
3109
|
-
* @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
|
|
3110
|
-
* @returns A zero-padded 6-digit decimal string, e.g. `"042193"`.
|
|
3111
|
-
*/
|
|
3112
|
-
function generateTotp(secret, when = Date.now()) {
|
|
3113
|
-
const key = Buffer.from(secret, "hex");
|
|
3114
|
-
const counter = Math.max(0, Math.floor(when / 1e3 / TIME_STEP));
|
|
3115
|
-
const counterBuf = Buffer.alloc(8);
|
|
3116
|
-
const hi = Math.floor(counter / 4294967296);
|
|
3117
|
-
const lo = counter >>> 0;
|
|
3118
|
-
counterBuf.writeUInt32BE(hi, 0);
|
|
3119
|
-
counterBuf.writeUInt32BE(lo, 4);
|
|
3120
|
-
const mac = createHmac("sha1", key).update(counterBuf).digest();
|
|
3121
|
-
const offset = mac[19] & 15;
|
|
3122
|
-
return (((mac[offset] & 127) << 24 | (mac[offset + 1] & 255) << 16 | (mac[offset + 2] & 255) << 8 | mac[offset + 3] & 255) % 10 ** DIGITS).toString().padStart(DIGITS, "0");
|
|
3123
|
-
}
|
|
3124
|
-
/**
|
|
3125
|
-
* Verifies a TOTP code against the secret, accepting ±`skew` time steps to
|
|
3126
|
-
* tolerate clock drift between the relay host and the client device.
|
|
3127
|
-
*
|
|
3128
|
-
* Uses `timingSafeEqual` for constant-time comparison to prevent timing
|
|
3129
|
-
* side-channel attacks.
|
|
3130
|
-
*
|
|
3131
|
-
* @param secret - Hex-encoded shared secret.
|
|
3132
|
-
* @param code - The 6-digit code to verify (string or numeric).
|
|
3133
|
-
* @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
|
|
3134
|
-
* @param skew - Number of adjacent steps to accept on either side. Default 1
|
|
3135
|
-
* (accepts T-1, T, T+1 — a 90-second acceptance window).
|
|
3136
|
-
* @returns `true` if the code matches any accepted step, `false` otherwise.
|
|
3137
|
-
*/
|
|
3138
|
-
function verifyTotp(secret, code, when = Date.now(), skew = 1) {
|
|
3139
|
-
const normalised = String(code).padStart(DIGITS, "0");
|
|
3140
|
-
if (normalised.length !== DIGITS || !/^\d{6}$/.test(normalised)) return false;
|
|
3141
|
-
const candidateBuf = Buffer.from(normalised, "utf8");
|
|
3142
|
-
for (let delta = -skew; delta <= skew; delta++) {
|
|
3143
|
-
const expected = generateTotp(secret, when + delta * TIME_STEP * 1e3);
|
|
3144
|
-
if (timingSafeEqual(Buffer.from(expected, "utf8"), candidateBuf)) return true;
|
|
3145
|
-
}
|
|
3146
|
-
return false;
|
|
3147
|
-
}
|
|
3148
|
-
//#endregion
|
|
3149
3224
|
//#region src/mcp/tunnel.ts
|
|
3150
3225
|
/**
|
|
3151
3226
|
* cloudflared quick tunnel + attach banner for the debug-mode MCP server.
|
|
@@ -3518,7 +3593,7 @@ function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs
|
|
|
3518
3593
|
* naturally via `enableDomains`). The tier only controls visibility.
|
|
3519
3594
|
*/
|
|
3520
3595
|
function createDebugServer(deps) {
|
|
3521
|
-
const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, defaultEnv } = deps;
|
|
3596
|
+
const { connection, aitSource, getTunnelStatus, waitForAttachTimeoutMs = 9e4, qrHttpServer, getEnvironment: getEnvDep, getEnvironmentReason: getEnvReasonDep, diagnosticsCollector: collectorDep, defaultEnv, totpSecret } = deps;
|
|
3522
3597
|
const resolveEnvironment = getEnvDep ?? (() => getEnvironment({
|
|
3523
3598
|
connection,
|
|
3524
3599
|
defaultEnv
|
|
@@ -3530,7 +3605,7 @@ function createDebugServer(deps) {
|
|
|
3530
3605
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
3531
3606
|
const server = new Server({
|
|
3532
3607
|
name: "ait-debug",
|
|
3533
|
-
version: "0.1.
|
|
3608
|
+
version: "0.1.46"
|
|
3534
3609
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
3535
3610
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
3536
3611
|
const env = resolveEnvironment();
|
|
@@ -3574,7 +3649,7 @@ function createDebugServer(deps) {
|
|
|
3574
3649
|
if (name === "get_diagnostics") try {
|
|
3575
3650
|
const rawLimit = request.params.arguments?.recent_errors_limit;
|
|
3576
3651
|
const recentErrorsLimit = typeof rawLimit === "number" && rawLimit > 0 ? rawLimit : 10;
|
|
3577
|
-
|
|
3652
|
+
const result = await getDiagnostics({
|
|
3578
3653
|
tunnel: getTunnelStatus(),
|
|
3579
3654
|
connection,
|
|
3580
3655
|
env: resolveEnvironment(),
|
|
@@ -3582,7 +3657,9 @@ function createDebugServer(deps) {
|
|
|
3582
3657
|
collector,
|
|
3583
3658
|
readLock: readServerLock,
|
|
3584
3659
|
recentErrorsLimit
|
|
3585
|
-
})
|
|
3660
|
+
});
|
|
3661
|
+
const attached = connection.listTargets().length > 0;
|
|
3662
|
+
return envelopeResult(result, name, resolveEnvironment(), attached);
|
|
3586
3663
|
} catch (err) {
|
|
3587
3664
|
return errorResult(err, name);
|
|
3588
3665
|
}
|
|
@@ -3609,7 +3686,7 @@ function createDebugServer(deps) {
|
|
|
3609
3686
|
return `${baseText}\n\nNo page${deploymentId ? ` matching deploymentId=${deploymentId}` : ""} attached within ${timeoutSec}s${observedNote} — call list_pages to retry.`;
|
|
3610
3687
|
};
|
|
3611
3688
|
try {
|
|
3612
|
-
const { attachUrl, relayUrl, authorityWarning } = buildAttachUrl(schemeUrl, getTunnelStatus());
|
|
3689
|
+
const { attachUrl, relayUrl, authorityWarning, totp } = buildAttachUrl(schemeUrl, getTunnelStatus(), totpSecret);
|
|
3613
3690
|
const warningPrefix = authorityWarning ? `⚠️ scheme_url 경고: ${authorityWarning}\n\n` : "";
|
|
3614
3691
|
const header = "This tool result is shown to the user directly — do NOT re-print the QR below in your reply (it wastes output tokens). Just tell the user to scan the QR in this output (Ctrl+O to expand if collapsed).";
|
|
3615
3692
|
const guiAvailable = canOpenBrowser();
|
|
@@ -3618,7 +3695,8 @@ function createDebugServer(deps) {
|
|
|
3618
3695
|
const qrHeadless = await renderQr(attachUrl);
|
|
3619
3696
|
const headlessText = `${warningPrefix}${headlessNote}${header}\n${JSON.stringify({
|
|
3620
3697
|
attachUrl,
|
|
3621
|
-
relayUrl
|
|
3698
|
+
relayUrl,
|
|
3699
|
+
...totp ? { totp } : {}
|
|
3622
3700
|
}, null, 2)}\n\n${qrHeadless}`;
|
|
3623
3701
|
if (!waitForAttach) return { content: [{
|
|
3624
3702
|
type: "text",
|
|
@@ -3654,7 +3732,8 @@ function createDebugServer(deps) {
|
|
|
3654
3732
|
};
|
|
3655
3733
|
const shortText = `${warningPrefix}${header}\n${JSON.stringify({
|
|
3656
3734
|
relayUrl,
|
|
3657
|
-
openResult
|
|
3735
|
+
openResult,
|
|
3736
|
+
...totp ? { totp } : {}
|
|
3658
3737
|
}, null, 2)}\n\n브라우저에서 QR을 열었습니다${retriedNote}. 폰 카메라로 스캔하세요.\nURL: ${browserResult.httpUrl}`;
|
|
3659
3738
|
if (!waitForAttach) return { content: [{
|
|
3660
3739
|
type: "text",
|
|
@@ -3693,7 +3772,8 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3693
3772
|
const baseText = `${warningPrefix}${fallbackNote}${header}\n${JSON.stringify({
|
|
3694
3773
|
attachUrl,
|
|
3695
3774
|
relayUrl,
|
|
3696
|
-
openResult
|
|
3775
|
+
openResult,
|
|
3776
|
+
...totp ? { totp } : {}
|
|
3697
3777
|
}, null, 2)}\n\n${qr}`;
|
|
3698
3778
|
if (!waitForAttach) return { content: [{
|
|
3699
3779
|
type: "text",
|
|
@@ -3721,7 +3801,8 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3721
3801
|
const qr = await renderQr(attachUrl);
|
|
3722
3802
|
const baseText = `${warningPrefix}${header}\n${JSON.stringify({
|
|
3723
3803
|
attachUrl,
|
|
3724
|
-
relayUrl
|
|
3804
|
+
relayUrl,
|
|
3805
|
+
...totp ? { totp } : {}
|
|
3725
3806
|
}, null, 2)}\n\n${qr}`;
|
|
3726
3807
|
if (!waitForAttach) return { content: [{
|
|
3727
3808
|
type: "text",
|
|
@@ -3756,7 +3837,9 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3756
3837
|
if (connection instanceof ChiiCdpConnection) try {
|
|
3757
3838
|
await connection.refreshTargets();
|
|
3758
3839
|
} catch {}
|
|
3759
|
-
|
|
3840
|
+
const pagesData = listPages(connection, getTunnelStatus());
|
|
3841
|
+
const attached = connection.listTargets().length > 0;
|
|
3842
|
+
return envelopeResult(pagesData, name, resolveEnvironment(), attached);
|
|
3760
3843
|
}
|
|
3761
3844
|
return classifyEnableDomainError(err, name);
|
|
3762
3845
|
}
|
|
@@ -3768,11 +3851,14 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3768
3851
|
return jsonResult$1({ exceptions: listExceptions(connection, typeof rawLimit === "number" && rawLimit > 0 ? rawLimit : 50) });
|
|
3769
3852
|
}
|
|
3770
3853
|
case "list_network_requests": return jsonResult$1(listNetworkRequests(connection));
|
|
3771
|
-
case "list_pages":
|
|
3854
|
+
case "list_pages": {
|
|
3772
3855
|
if (connection instanceof ChiiCdpConnection) try {
|
|
3773
3856
|
await connection.refreshTargets();
|
|
3774
3857
|
} catch {}
|
|
3775
|
-
|
|
3858
|
+
const listPagesData = listPages(connection, getTunnelStatus());
|
|
3859
|
+
const listPagesAttached = connection.listTargets().length > 0;
|
|
3860
|
+
return envelopeResult(listPagesData, name, resolveEnvironment(), listPagesAttached);
|
|
3861
|
+
}
|
|
3776
3862
|
case "get_dom_document": return jsonResult$1(await getDomDocument(connection));
|
|
3777
3863
|
case "take_snapshot": return jsonResult$1(await takeSnapshot(connection));
|
|
3778
3864
|
case "take_screenshot": {
|
|
@@ -3783,7 +3869,11 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3783
3869
|
mimeType: shot.mimeType
|
|
3784
3870
|
}] };
|
|
3785
3871
|
}
|
|
3786
|
-
case "measure_safe_area":
|
|
3872
|
+
case "measure_safe_area": {
|
|
3873
|
+
const safeAreaData = await measureSafeArea(connection, resolveEnvironment());
|
|
3874
|
+
const safeAreaAttached = connection.listTargets().length > 0;
|
|
3875
|
+
return envelopeResult(safeAreaData, name, resolveEnvironment(), safeAreaAttached);
|
|
3876
|
+
}
|
|
3787
3877
|
case "evaluate": {
|
|
3788
3878
|
const expression = request.params.arguments?.expression;
|
|
3789
3879
|
if (typeof expression !== "string" || expression === "") return mcpError("evaluate: expression 인자가 비어 있습니다. 평가할 JavaScript 표현식을 전달하세요.");
|
|
@@ -3798,7 +3888,8 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3798
3888
|
if (isLiveRelayEnv(env) && request.params.arguments?.confirm !== true) return liveGuardError("call_sdk");
|
|
3799
3889
|
const sdkResult = await callSdk(connection, sdkName, sdkArgs);
|
|
3800
3890
|
if (!sdkResult.ok && typeof sdkResult.error === "string" && sdkResult.error.startsWith("sdk-absent:")) return sdkAbsentError("call_sdk");
|
|
3801
|
-
|
|
3891
|
+
const callSdkAttached = connection.listTargets().length > 0;
|
|
3892
|
+
return envelopeResult(sdkResult, name, resolveEnvironment(), callSdkAttached);
|
|
3802
3893
|
}
|
|
3803
3894
|
default: return unknownTool(name);
|
|
3804
3895
|
}
|
|
@@ -3814,6 +3905,22 @@ function jsonResult$1(value) {
|
|
|
3814
3905
|
text: JSON.stringify(value, null, 2)
|
|
3815
3906
|
}] };
|
|
3816
3907
|
}
|
|
3908
|
+
/**
|
|
3909
|
+
* Wraps `value` in a `ToolEnvelope` (when compat mode is off) and returns it
|
|
3910
|
+
* as a text content block. When `AIT_MCP_COMPAT=chrome-devtools` is set the
|
|
3911
|
+
* envelope is skipped and the raw value is returned — identical to `jsonResult`.
|
|
3912
|
+
*/
|
|
3913
|
+
function envelopeResult(value, tool, env, attached) {
|
|
3914
|
+
const wrapped = wrapEnvelope(value, {
|
|
3915
|
+
tool,
|
|
3916
|
+
env,
|
|
3917
|
+
attached
|
|
3918
|
+
});
|
|
3919
|
+
return { content: [{
|
|
3920
|
+
type: "text",
|
|
3921
|
+
text: JSON.stringify(wrapped, null, 2)
|
|
3922
|
+
}] };
|
|
3923
|
+
}
|
|
3817
3924
|
function unknownTool(name) {
|
|
3818
3925
|
return mcpError(`알 수 없는 tool: ${name}`);
|
|
3819
3926
|
}
|
|
@@ -3975,7 +4082,8 @@ async function runDebugServer(options = {}) {
|
|
|
3975
4082
|
return qrServer;
|
|
3976
4083
|
},
|
|
3977
4084
|
diagnosticsCollector,
|
|
3978
|
-
defaultEnv: "relay-dev"
|
|
4085
|
+
defaultEnv: "relay-dev",
|
|
4086
|
+
...process.env.AIT_DEBUG_TOTP_SECRET ? { totpSecret: process.env.AIT_DEBUG_TOTP_SECRET } : {}
|
|
3979
4087
|
});
|
|
3980
4088
|
const transport = new StdioServerTransport();
|
|
3981
4089
|
let closed = false;
|
|
@@ -4515,7 +4623,7 @@ function createDevServer(deps = {}) {
|
|
|
4515
4623
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
4516
4624
|
const server = new Server({
|
|
4517
4625
|
name: "ait-devtools",
|
|
4518
|
-
version: "0.1.
|
|
4626
|
+
version: "0.1.46"
|
|
4519
4627
|
}, { capabilities: { tools: {} } });
|
|
4520
4628
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
4521
4629
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|