@ait-co/devtools 0.1.49 → 0.1.51
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/in-app/index.d.ts +7 -0
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +16 -0
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.js +117 -23
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +5 -11
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +2 -2
- package/package.json +1 -1
package/dist/in-app/index.d.ts
CHANGED
|
@@ -205,6 +205,13 @@ declare function deriveTargetScriptUrl(relayUrl: string): string;
|
|
|
205
205
|
* Safe to call even if `document` is somehow unavailable (defensive boundary
|
|
206
206
|
* guard — in practice this always runs in a real WebView).
|
|
207
207
|
*
|
|
208
|
+
* **keepAwake side effect**: on a successful attach, `setScreenAwakeMode({
|
|
209
|
+
* enabled: true })` is called so the phone screen stays awake during the debug
|
|
210
|
+
* session. A `beforeunload` handler restores normal sleep on page unload.
|
|
211
|
+
* Opt out by adding `noKeepAwake=1` to the page URL query string — the check
|
|
212
|
+
* reads `window.location.search` directly, consistent with other guards in
|
|
213
|
+
* this file.
|
|
214
|
+
*
|
|
208
215
|
* @param gateResult - Optional pre-evaluated gate result for testability.
|
|
209
216
|
* Defaults to `checkDebugGate()` which reads the current page URL. Passing a
|
|
210
217
|
* custom value avoids the need to manipulate `window.location` in tests.
|
|
@@ -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":";;AA+DA;;;;;;;;;AASA;;;;;AAmBA;;;;;AAQA;;;;;;;;;;;AAwEA;;;;;AAyBA;;;;;;;;;;;;
|
|
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":";;AA+DA;;;;;;;;;AASA;;;;;AAmBA;;;;;AAQA;;;;;;;;;;;AAwEA;;;;;AAyBA;;;;;;;;;;;;ACtKA;;;;;AAmCA;;;;;;;;ACrBA;AAAA,UFmBiB,gBAAA;EAAA,SACN,MAAA;EEpBuB;EAAA,SFsBvB,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;;;;;;;;;;;;iBAyBb,iBAAA,CAAkB,QAAA;;;;;;;;;;;;;;;;;;;;;;iBAyBlB,iBAAA,CAAkB,KAAA,EAAO,SAAA,GAAY,UAAA;;;;;;AAzGrD;;;;;AAQA;;;;;;;iBCrEgB,qBAAA,CAAsB,QAAA;;;;AD6ItC;;;;;AAyBA;;;;;;;;;;;;ACtKA;;;iBAmCgB,WAAA,CAAY,UAAA,GAAY,UAAA;;;;;;;AD0GxC;;;;;AAyBA;;;;;iBExJgB,cAAA,CAAA,GAAkB,UAAA"}
|
package/dist/in-app/index.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { setScreenAwakeMode } from "@apps-in-toss/web-framework";
|
|
1
2
|
//#region src/in-app/gate.ts
|
|
2
3
|
/**
|
|
3
4
|
* The host suffix the Toss app uses to serve dogfood / private mini-apps.
|
|
@@ -139,6 +140,13 @@ let attached = false;
|
|
|
139
140
|
* Safe to call even if `document` is somehow unavailable (defensive boundary
|
|
140
141
|
* guard — in practice this always runs in a real WebView).
|
|
141
142
|
*
|
|
143
|
+
* **keepAwake side effect**: on a successful attach, `setScreenAwakeMode({
|
|
144
|
+
* enabled: true })` is called so the phone screen stays awake during the debug
|
|
145
|
+
* session. A `beforeunload` handler restores normal sleep on page unload.
|
|
146
|
+
* Opt out by adding `noKeepAwake=1` to the page URL query string — the check
|
|
147
|
+
* reads `window.location.search` directly, consistent with other guards in
|
|
148
|
+
* this file.
|
|
149
|
+
*
|
|
142
150
|
* @param gateResult - Optional pre-evaluated gate result for testability.
|
|
143
151
|
* Defaults to `checkDebugGate()` which reads the current page URL. Passing a
|
|
144
152
|
* custom value avoids the need to manipulate `window.location` in tests.
|
|
@@ -160,6 +168,14 @@ function maybeAttach(gateResult = checkDebugGate()) {
|
|
|
160
168
|
script.async = true;
|
|
161
169
|
(document.head ?? document.documentElement).appendChild(script);
|
|
162
170
|
attached = true;
|
|
171
|
+
if (typeof window !== "undefined" && new URLSearchParams(window.location.search).get("noKeepAwake") === "1") return;
|
|
172
|
+
setScreenAwakeMode({ enabled: true }).then(() => {
|
|
173
|
+
window.addEventListener("beforeunload", () => {
|
|
174
|
+
setScreenAwakeMode({ enabled: false }).catch(() => {});
|
|
175
|
+
}, { once: true });
|
|
176
|
+
}).catch((err) => {
|
|
177
|
+
console.debug("[@ait-co/devtools] setScreenAwakeMode failed:", err);
|
|
178
|
+
});
|
|
163
179
|
}
|
|
164
180
|
//#endregion
|
|
165
181
|
//#region src/in-app/index.ts
|
package/dist/in-app/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/in-app/gate.ts","../../src/in-app/attach.ts","../../src/in-app/index.ts"],"sourcesContent":["/**\n * Runtime activation gate for the in-app debug surface.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n * \"3-layer activation gate\". This is the pure gate decision; the Chii client,\n * WebSocket transport, MCP server, and CLI that consume it live in src/mcp/.\n *\n * This function evaluates the two RUNTIME layers, B and C. Layer A — the\n * build-time gate — is NOT evaluated here, and deliberately so: it is enforced\n * entirely by the consumer's `if (__DEBUG_BUILD__) { … }` guard around the\n * import site (see sdk-example `src/main.tsx`). `__DEBUG_BUILD__` is a\n * consumer-build-time constant; a release consumer build folds it to `false`\n * and dead-code-eliminates the whole import of `@ait-co/devtools/in-app`, so\n * this code is simply absent from release bundles. A pre-built npm package\n * cannot re-check that flag — it was already baked at devtools' own publish\n * time — so any `isDebugBuild` check inside this function would be permanently\n * `false` and could never pass. Layer A is the consumer guard; B and C are\n * here.\n *\n * Layer B has two parts. Both must pass:\n * B1 — host allowlist: `hostname` must be a `*.private-apps.tossmini.com`\n * subdomain. The Toss app serves dogfood / private mini-apps from a\n * separate `private-apps` host; a production (`intoss://`) entry is\n * served from `*.apps.tossmini.com` WITHOUT the `private-apps` segment.\n * This is the security gate against a dogfood build that somehow lands\n * on a production entry — see the comment on {@link isPrivateAppsHost}.\n * B2 — entry query: `_deploymentId` must be present and non-empty.\n *\n * Layer C — opt-in + relay + optional TOTP auth:\n * C1 — opt-in: `debug=1` must be present.\n * C2 — relay URL: `relay=<wss-url>` must be a valid `wss:` URL.\n * C3 — TOTP auth: When `verifyTotpCode` is provided (consumer injected the\n * baked secret at build time via `__DEBUG_TOTP_SECRET__`),\n * `at=<code>` is checked. Invalid or absent code → BLOCKED.\n * When no verifier is provided (TOTP disabled), `at` is\n * ignored (backward compatible).\n *\n * Security note on baked secrets:\n * The TOTP secret baked in via `__DEBUG_TOTP_SECRET__` is present in the\n * dogfood bundle and is extractable by a determined reverse engineer.\n * The practical bar raised is: \"URL leak\" (Slack paste, QR screenshot) →\n * blocked; \"URL + bundle extraction + live TOTP code\" → not blocked.\n * This is the intended threat model. Do not overpromise on this guarantee.\n *\n * SECRET-HANDLING: `verifyTotpCode` is a black-box predicate. This module\n * does NOT log the secret, any code value, or pass/fail details beyond the\n * `'auth'` reason enum.\n *\n * Decision matrix (gate only runs in a debug build — Layer A already passed):\n *\n * host ok | _deploymentId | debug=1 | relay ok | TOTP ok* | result\n * no | (any) | (any) | (any) | (any) | BLOCKED (host)\n * yes | absent | (any) | (any) | (any) | BLOCKED (entry)\n * yes | present | absent | (any) | (any) | BLOCKED (opt-in)\n * yes | present | present | invalid | (any) | BLOCKED (invalid-relay)\n * yes | present | present | valid | fail* | BLOCKED (auth)\n * yes | present | present | valid | pass/n/a | ATTACH\n *\n * * \"TOTP ok\" column only applies when `verifyTotpCode` is provided.\n * When no verifier is injected, TOTP check is skipped entirely.\n */\n\n/** Shape returned when the gate allows attachment. */\nexport interface GateResultAttach {\n readonly attach: true;\n /** The validated `wss:` relay URL from the `relay` query param. */\n readonly relayUrl: string;\n /** The deployment ID extracted from the `_deploymentId` query param. */\n readonly deploymentId: string;\n}\n\n/** Shape returned when the gate blocks attachment, with a reason code. */\nexport interface GateResultBlocked {\n readonly attach: false;\n /**\n * - `'host'` Layer B1: `hostname` is not a `*.private-apps.tossmini.com` host.\n * - `'entry'` Layer B2: `_deploymentId` param is absent or empty.\n * - `'opt-in'` Layer C1: `debug=1` param is absent.\n * - `'invalid-relay'` Layer C2: `relay` param is absent, empty, or not a `wss:` URL.\n * - `'auth'` Layer C3: TOTP `at=` code is absent, invalid, or expired\n * (only when a `verifyTotpCode` predicate is injected).\n *\n * There is no `'build'` reason: Layer A is enforced by the consumer's\n * `if (__DEBUG_BUILD__)` guard, not by this function.\n *\n * SECRET-HANDLING: `'auth'` is the only value surfaced for auth failures —\n * no code value, expected value, or secret fragment is ever exposed.\n */\n readonly reason: 'host' | 'entry' | 'opt-in' | 'invalid-relay' | 'auth';\n}\n\nexport type GateResult = GateResultAttach | GateResultBlocked;\n\n/**\n * Input for {@link evaluateDebugGate}.\n *\n * All fields are explicit so the function is trivially testable without\n * touching `window`.\n */\nexport interface GateInput {\n /**\n * The host the page is served from — `window.location.hostname`.\n *\n * This is the Layer B1 security signal. Why hostname and not the entry\n * scheme: the Toss SDK normalises `intoss-private://` to `intoss://` in\n * `getSchemeUri()`, and `getOperationalEnvironment()` / `getWebViewType()`\n * return the same value (`\"toss\"` / `\"partner\"`) for both dogfood and\n * production entries — none of them distinguish a dogfood entry. The host\n * does: a dogfood / private-apps entry is served from\n * `*.private-apps.tossmini.com`, a production entry is not. This was\n * confirmed live over CDP against mini-app 31146 (see spec open question 2).\n */\n readonly hostname: string;\n\n /**\n * The URL search params to inspect for gate signals (Layers B2 and C).\n *\n * Prefer `URLSearchParams` so callers can pass `new URLSearchParams(location.search)`\n * without coupling the pure function to `window`.\n */\n readonly searchParams: URLSearchParams;\n\n /**\n * Optional TOTP code verifier for Layer C3 auth gate.\n *\n * When provided, `evaluateDebugGate` reads the `at` query param and passes\n * it to this predicate. Return `true` to allow, `false` to block with\n * `reason: 'auth'`.\n *\n * Inject via the consumer's build define, e.g.:\n * ```ts\n * // dogfood build entry — consumer's build injects __DEBUG_TOTP_SECRET__\n * declare const __DEBUG_TOTP_SECRET__: string | undefined;\n * const verifyTotpCode = typeof __DEBUG_TOTP_SECRET__ !== 'undefined'\n * ? (code: string) => verifyTotp(__DEBUG_TOTP_SECRET__, code)\n * : undefined;\n * maybeAttach(evaluateDebugGate({ ...params, verifyTotpCode }));\n * ```\n *\n * Security note: this predicate is a black-box from the gate's perspective.\n * The gate only surfaces pass/fail and the `'auth'` reason code — no code\n * value or secret fragment is ever logged or returned.\n *\n * When `undefined` (TOTP disabled), `at=` is silently ignored and the gate\n * proceeds to ATTACH if all other layers pass.\n */\n readonly verifyTotpCode?: (code: string) => boolean;\n}\n\n/**\n * The host suffix the Toss app uses to serve dogfood / private mini-apps.\n *\n * A `intoss-private://` (dogfood) entry maps to a host such as\n * `aitc-sdk-example.private-apps.tossmini.com`. A production `intoss://`\n * entry is served from `*.apps.tossmini.com` — the `.private-apps.` segment\n * is absent. Confirmed live over CDP for mini-app 31146; the exact production\n * host is to be re-confirmed once 31146 passes review (spec open question 2).\n */\nconst PRIVATE_APPS_HOST_SUFFIX = '.private-apps.tossmini.com';\n\n/**\n * Returns whether `hostname` is a `*.private-apps.tossmini.com` subdomain —\n * the host the Toss app reserves for dogfood / private mini-app entries.\n *\n * The match is an exact suffix check, not a substring `.includes()`: a\n * substring test would also accept an attacker-controlled host like\n * `private-apps.tossmini.com.evil.example`, which ends in `.example`, not in\n * `.tossmini.com`. Requiring the string to END with the suffix closes that.\n * The leading `.` in the suffix also forces a real subdomain label, so a\n * bare `private-apps.tossmini.com` (no mini-app subdomain) does not match.\n */\nexport function isPrivateAppsHost(hostname: string): boolean {\n return hostname.endsWith(PRIVATE_APPS_HOST_SUFFIX);\n}\n\n/**\n * Pure function that evaluates the runtime debug activation layers (B and C).\n *\n * Has no side effects. The input is explicit. Returns a discriminated union\n * so callers can pattern-match on `result.attach`.\n *\n * Layer A (build-time) is intentionally not evaluated here — see the file-level\n * comment. By the time this function runs, the consumer's `if (__DEBUG_BUILD__)`\n * guard has already passed; this function only decides B and C.\n *\n * @example\n * ```ts\n * const result = evaluateDebugGate({\n * hostname: window.location.hostname,\n * searchParams: new URLSearchParams(window.location.search),\n * });\n * if (result.attach) {\n * // Proceed to load Chii client\n * }\n * ```\n */\nexport function evaluateDebugGate(input: GateInput): GateResult {\n // Layer B1 — host allowlist (the security gate).\n // The page must be served from a `*.private-apps.tossmini.com` host. A\n // production `intoss://` entry is served from `*.apps.tossmini.com` and is\n // rejected here. This is what stops a dogfood build that somehow reaches a\n // production entry from attaching: Layer A keeps debug code out of release\n // bundles, and this layer keeps a dogfood bundle that lands on a production\n // host from attaching even though its code is present.\n if (!isPrivateAppsHost(input.hostname)) {\n return { attach: false, reason: 'host' };\n }\n\n // Layer B2 — runtime entry query gate.\n // `_deploymentId` must be present and non-empty. The `intoss-private://`\n // scheme used for dogfood entries includes this param; general user entry\n // paths do not.\n const deploymentId = input.searchParams.get('_deploymentId') ?? '';\n if (deploymentId === '') {\n return { attach: false, reason: 'entry' };\n }\n\n // Layer C — explicit opt-in gate.\n // Require `debug=1` so that an operator who opens a dogfood URL by accident\n // does not inadvertently trigger the debug surface.\n const debugParam = input.searchParams.get('debug');\n if (debugParam !== '1') {\n return { attach: false, reason: 'opt-in' };\n }\n\n // Layer C continued — relay URL validation.\n // `relay=<wss-url>` must be present and must use the `wss:` scheme.\n // Plain `ws:` is rejected (no TLS). `http:`/`https:` are rejected.\n const relayRaw = input.searchParams.get('relay') ?? '';\n if (relayRaw === '') {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n let relayUrl: URL;\n try {\n relayUrl = new URL(relayRaw);\n } catch {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n if (relayUrl.protocol !== 'wss:') {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n // Layer C3 — TOTP auth gate (fail-fast, only when a verifier is injected).\n // The `at` query param carries the current TOTP code. Absent or invalid code\n // → BLOCKED. When no verifier is provided (TOTP disabled), this check is\n // skipped entirely for backward compatibility.\n //\n // SECRET-HANDLING: we do NOT log `code`, the verifier's result, or anything\n // derived from the secret. Only the `'auth'` enum is surfaced on failure.\n if (input.verifyTotpCode !== undefined) {\n const code = input.searchParams.get('at') ?? '';\n if (!input.verifyTotpCode(code)) {\n return { attach: false, reason: 'auth' };\n }\n }\n\n return { attach: true, relayUrl: relayUrl.href, deploymentId };\n}\n","/**\n * In-app Chii target injection for the debug attach flow.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n * \"MCP attach\" topology section — Phase 1 browser-side implementation.\n *\n * This module bridges the 3-layer gate result to a Chii `target.js` script\n * injection. The Chii npm package is the relay SERVER — the in-app side is\n * a plain `<script src=\"…/target.js\">` pointing at the relay host. No chii\n * npm dependency is needed here.\n */\n\nimport { checkDebugGate, type GateResult } from './index.js';\n\n/**\n * Converts a validated `wss:` relay URL into the Chii `target.js` script URL.\n *\n * Scheme is mapped `wss:` → `https:`. Host and port are preserved.\n * Pathname is set to `/target.js` regardless of the relay path.\n * Query params and hash from the relay URL are dropped — the target script\n * URL is a static asset path on the same host.\n *\n * @example\n * deriveTargetScriptUrl('wss://abc.trycloudflare.com/relay')\n * // → 'https://abc.trycloudflare.com/target.js'\n *\n * deriveTargetScriptUrl('wss://h.example.com:9100/')\n * // → 'https://h.example.com:9100/target.js'\n */\nexport function deriveTargetScriptUrl(relayUrl: string): string {\n const u = new URL(relayUrl);\n u.protocol = 'https:';\n u.pathname = '/target.js';\n u.search = '';\n u.hash = '';\n return u.toString();\n}\n\n/** Module-level guard against double-injection within a page lifecycle. */\nlet attached = false;\n\n/**\n * Evaluates the 3-layer debug gate and, if the gate passes, injects the Chii\n * `target.js` script into `document.head`.\n *\n * Idempotent — calling more than once is safe. The second call is a no-op if\n * a script with the same `src` is already present in the document, and the\n * module-level `attached` flag prevents redundant DOM queries after the first\n * successful injection.\n *\n * Safe to call even if `document` is somehow unavailable (defensive boundary\n * guard — in practice this always runs in a real WebView).\n *\n * @param gateResult - Optional pre-evaluated gate result for testability.\n * Defaults to `checkDebugGate()` which reads the current page URL. Passing a\n * custom value avoids the need to manipulate `window.location` in tests.\n */\nexport function maybeAttach(gateResult: GateResult = checkDebugGate()): void {\n if (!gateResult.attach) {\n console.debug(\n `[@ait-co/devtools] debug attach skipped — gate blocked (reason: ${gateResult.reason})`,\n );\n return;\n }\n\n // Guard against double-injection across repeated calls.\n if (attached) {\n return;\n }\n\n // Defensive: if document is not available (unusual, but possible in some\n // SSR-adjacent edge cases), bail silently rather than throwing.\n if (typeof document === 'undefined') {\n return;\n }\n\n const src = deriveTargetScriptUrl(gateResult.relayUrl);\n\n // Also guard against a script with the same src already in the DOM\n // (e.g. injected by a different code path or a page reload within SPA).\n const existing = document.querySelector<HTMLScriptElement>(`script[src=\"${src}\"]`);\n if (existing !== null) {\n attached = true;\n return;\n }\n\n const script = document.createElement('script');\n script.src = src;\n script.async = true;\n (document.head ?? document.documentElement).appendChild(script);\n\n attached = true;\n}\n","/**\n * @ait-co/devtools/in-app entry point.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n *\n * Phase 1 — gate + browser-side Chii target injection.\n * WebSocket relay, QR/paste UI, and AI-host MCP bin are later phases that\n * require real-device validation and are not included here.\n *\n * This thin entry reads `window.location` and calls the pure\n * {@link evaluateDebugGate} function. All testable logic lives in `./gate.ts`\n * and `./attach.ts`, not here.\n *\n * Layer A of the activation gate (build-time) is NOT enforced in this module.\n * It is the consumer's responsibility: the consumer wraps its\n * `import('@ait-co/devtools/in-app')` call site in `if (__DEBUG_BUILD__) { … }`\n * (see sdk-example `src/main.tsx`), where `__DEBUG_BUILD__` is a\n * consumer-build-time constant. A release consumer build folds that constant\n * to `false` and dead-code-eliminates this whole module. This package is\n * pre-built and ships with `__DEBUG_BUILD__` already resolved at devtools'\n * publish time, so it could never re-evaluate the consumer's build channel —\n * which is exactly why Layer A lives at the consumer guard, not here.\n */\n\nimport { evaluateDebugGate, type GateResult } from './gate.js';\n\nexport { deriveTargetScriptUrl, maybeAttach } from './attach.js';\nexport type { GateInput, GateResult, GateResultAttach, GateResultBlocked } from './gate.js';\nexport { evaluateDebugGate, isPrivateAppsHost } from './gate.js';\n\n/**\n * Evaluates the runtime debug activation layers (B and C) against the current\n * page URL.\n *\n * Returns the gate result. Callers can check `result.attach` to decide whether\n * to proceed with debug surface attachment.\n *\n * This function reads `window.location` only — both the hostname (Layer B1\n * host allowlist) and the search params (Layers B2 and C). Layer A\n * (build-time) is enforced by the consumer's `if (__DEBUG_BUILD__)` guard\n * around the import site, not here — see the file-level comment. Consumers\n * call this with no arguments, so the Layer B1 host check is picked up with\n * no change at the call site.\n */\nexport function checkDebugGate(): GateResult {\n return evaluateDebugGate({\n hostname: window.location.hostname,\n searchParams: new URLSearchParams(window.location.search),\n });\n}\n"],"mappings":";;;;;;;;;;AA8JA,MAAM,2BAA2B;;;;;;;;;;;;AAajC,SAAgB,kBAAkB,UAA2B;AAC3D,QAAO,SAAS,SAAS,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;AAwBpD,SAAgB,kBAAkB,OAA8B;AAQ9D,KAAI,CAAC,kBAAkB,MAAM,SAAS,CACpC,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAQ;CAO1C,MAAM,eAAe,MAAM,aAAa,IAAI,gBAAgB,IAAI;AAChE,KAAI,iBAAiB,GACnB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAS;AAO3C,KADmB,MAAM,aAAa,IAAI,QAAQ,KAC/B,IACjB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAU;CAM5C,MAAM,WAAW,MAAM,aAAa,IAAI,QAAQ,IAAI;AACpD,KAAI,aAAa,GACf,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAiB;CAGnD,IAAI;AACJ,KAAI;AACF,aAAW,IAAI,IAAI,SAAS;SACtB;AACN,SAAO;GAAE,QAAQ;GAAO,QAAQ;GAAiB;;AAGnD,KAAI,SAAS,aAAa,OACxB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAiB;AAUnD,KAAI,MAAM,mBAAmB,KAAA,GAAW;EACtC,MAAM,OAAO,MAAM,aAAa,IAAI,KAAK,IAAI;AAC7C,MAAI,CAAC,MAAM,eAAe,KAAK,CAC7B,QAAO;GAAE,QAAQ;GAAO,QAAQ;GAAQ;;AAI5C,QAAO;EAAE,QAAQ;EAAM,UAAU,SAAS;EAAM;EAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACrOhE,SAAgB,sBAAsB,UAA0B;CAC9D,MAAM,IAAI,IAAI,IAAI,SAAS;AAC3B,GAAE,WAAW;AACb,GAAE,WAAW;AACb,GAAE,SAAS;AACX,GAAE,OAAO;AACT,QAAO,EAAE,UAAU;;;AAIrB,IAAI,WAAW;;;;;;;;;;;;;;;;;AAkBf,SAAgB,YAAY,aAAyB,gBAAgB,EAAQ;AAC3E,KAAI,CAAC,WAAW,QAAQ;AACtB,UAAQ,MACN,mEAAmE,WAAW,OAAO,GACtF;AACD;;AAIF,KAAI,SACF;AAKF,KAAI,OAAO,aAAa,YACtB;CAGF,MAAM,MAAM,sBAAsB,WAAW,SAAS;AAKtD,KADiB,SAAS,cAAiC,eAAe,IAAI,IAAI,KACjE,MAAM;AACrB,aAAW;AACX;;CAGF,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,QAAO,MAAM;AACb,QAAO,QAAQ;AACf,EAAC,SAAS,QAAQ,SAAS,iBAAiB,YAAY,OAAO;AAE/D,YAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC/Cb,SAAgB,iBAA6B;AAC3C,QAAO,kBAAkB;EACvB,UAAU,OAAO,SAAS;EAC1B,cAAc,IAAI,gBAAgB,OAAO,SAAS,OAAO;EAC1D,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/in-app/gate.ts","../../src/in-app/attach.ts","../../src/in-app/index.ts"],"sourcesContent":["/**\n * Runtime activation gate for the in-app debug surface.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n * \"3-layer activation gate\". This is the pure gate decision; the Chii client,\n * WebSocket transport, MCP server, and CLI that consume it live in src/mcp/.\n *\n * This function evaluates the two RUNTIME layers, B and C. Layer A — the\n * build-time gate — is NOT evaluated here, and deliberately so: it is enforced\n * entirely by the consumer's `if (__DEBUG_BUILD__) { … }` guard around the\n * import site (see sdk-example `src/main.tsx`). `__DEBUG_BUILD__` is a\n * consumer-build-time constant; a release consumer build folds it to `false`\n * and dead-code-eliminates the whole import of `@ait-co/devtools/in-app`, so\n * this code is simply absent from release bundles. A pre-built npm package\n * cannot re-check that flag — it was already baked at devtools' own publish\n * time — so any `isDebugBuild` check inside this function would be permanently\n * `false` and could never pass. Layer A is the consumer guard; B and C are\n * here.\n *\n * Layer B has two parts. Both must pass:\n * B1 — host allowlist: `hostname` must be a `*.private-apps.tossmini.com`\n * subdomain. The Toss app serves dogfood / private mini-apps from a\n * separate `private-apps` host; a production (`intoss://`) entry is\n * served from `*.apps.tossmini.com` WITHOUT the `private-apps` segment.\n * This is the security gate against a dogfood build that somehow lands\n * on a production entry — see the comment on {@link isPrivateAppsHost}.\n * B2 — entry query: `_deploymentId` must be present and non-empty.\n *\n * Layer C — opt-in + relay + optional TOTP auth:\n * C1 — opt-in: `debug=1` must be present.\n * C2 — relay URL: `relay=<wss-url>` must be a valid `wss:` URL.\n * C3 — TOTP auth: When `verifyTotpCode` is provided (consumer injected the\n * baked secret at build time via `__DEBUG_TOTP_SECRET__`),\n * `at=<code>` is checked. Invalid or absent code → BLOCKED.\n * When no verifier is provided (TOTP disabled), `at` is\n * ignored (backward compatible).\n *\n * Security note on baked secrets:\n * The TOTP secret baked in via `__DEBUG_TOTP_SECRET__` is present in the\n * dogfood bundle and is extractable by a determined reverse engineer.\n * The practical bar raised is: \"URL leak\" (Slack paste, QR screenshot) →\n * blocked; \"URL + bundle extraction + live TOTP code\" → not blocked.\n * This is the intended threat model. Do not overpromise on this guarantee.\n *\n * SECRET-HANDLING: `verifyTotpCode` is a black-box predicate. This module\n * does NOT log the secret, any code value, or pass/fail details beyond the\n * `'auth'` reason enum.\n *\n * Decision matrix (gate only runs in a debug build — Layer A already passed):\n *\n * host ok | _deploymentId | debug=1 | relay ok | TOTP ok* | result\n * no | (any) | (any) | (any) | (any) | BLOCKED (host)\n * yes | absent | (any) | (any) | (any) | BLOCKED (entry)\n * yes | present | absent | (any) | (any) | BLOCKED (opt-in)\n * yes | present | present | invalid | (any) | BLOCKED (invalid-relay)\n * yes | present | present | valid | fail* | BLOCKED (auth)\n * yes | present | present | valid | pass/n/a | ATTACH\n *\n * * \"TOTP ok\" column only applies when `verifyTotpCode` is provided.\n * When no verifier is injected, TOTP check is skipped entirely.\n */\n\n/** Shape returned when the gate allows attachment. */\nexport interface GateResultAttach {\n readonly attach: true;\n /** The validated `wss:` relay URL from the `relay` query param. */\n readonly relayUrl: string;\n /** The deployment ID extracted from the `_deploymentId` query param. */\n readonly deploymentId: string;\n}\n\n/** Shape returned when the gate blocks attachment, with a reason code. */\nexport interface GateResultBlocked {\n readonly attach: false;\n /**\n * - `'host'` Layer B1: `hostname` is not a `*.private-apps.tossmini.com` host.\n * - `'entry'` Layer B2: `_deploymentId` param is absent or empty.\n * - `'opt-in'` Layer C1: `debug=1` param is absent.\n * - `'invalid-relay'` Layer C2: `relay` param is absent, empty, or not a `wss:` URL.\n * - `'auth'` Layer C3: TOTP `at=` code is absent, invalid, or expired\n * (only when a `verifyTotpCode` predicate is injected).\n *\n * There is no `'build'` reason: Layer A is enforced by the consumer's\n * `if (__DEBUG_BUILD__)` guard, not by this function.\n *\n * SECRET-HANDLING: `'auth'` is the only value surfaced for auth failures —\n * no code value, expected value, or secret fragment is ever exposed.\n */\n readonly reason: 'host' | 'entry' | 'opt-in' | 'invalid-relay' | 'auth';\n}\n\nexport type GateResult = GateResultAttach | GateResultBlocked;\n\n/**\n * Input for {@link evaluateDebugGate}.\n *\n * All fields are explicit so the function is trivially testable without\n * touching `window`.\n */\nexport interface GateInput {\n /**\n * The host the page is served from — `window.location.hostname`.\n *\n * This is the Layer B1 security signal. Why hostname and not the entry\n * scheme: the Toss SDK normalises `intoss-private://` to `intoss://` in\n * `getSchemeUri()`, and `getOperationalEnvironment()` / `getWebViewType()`\n * return the same value (`\"toss\"` / `\"partner\"`) for both dogfood and\n * production entries — none of them distinguish a dogfood entry. The host\n * does: a dogfood / private-apps entry is served from\n * `*.private-apps.tossmini.com`, a production entry is not. This was\n * confirmed live over CDP against mini-app 31146 (see spec open question 2).\n */\n readonly hostname: string;\n\n /**\n * The URL search params to inspect for gate signals (Layers B2 and C).\n *\n * Prefer `URLSearchParams` so callers can pass `new URLSearchParams(location.search)`\n * without coupling the pure function to `window`.\n */\n readonly searchParams: URLSearchParams;\n\n /**\n * Optional TOTP code verifier for Layer C3 auth gate.\n *\n * When provided, `evaluateDebugGate` reads the `at` query param and passes\n * it to this predicate. Return `true` to allow, `false` to block with\n * `reason: 'auth'`.\n *\n * Inject via the consumer's build define, e.g.:\n * ```ts\n * // dogfood build entry — consumer's build injects __DEBUG_TOTP_SECRET__\n * declare const __DEBUG_TOTP_SECRET__: string | undefined;\n * const verifyTotpCode = typeof __DEBUG_TOTP_SECRET__ !== 'undefined'\n * ? (code: string) => verifyTotp(__DEBUG_TOTP_SECRET__, code)\n * : undefined;\n * maybeAttach(evaluateDebugGate({ ...params, verifyTotpCode }));\n * ```\n *\n * Security note: this predicate is a black-box from the gate's perspective.\n * The gate only surfaces pass/fail and the `'auth'` reason code — no code\n * value or secret fragment is ever logged or returned.\n *\n * When `undefined` (TOTP disabled), `at=` is silently ignored and the gate\n * proceeds to ATTACH if all other layers pass.\n */\n readonly verifyTotpCode?: (code: string) => boolean;\n}\n\n/**\n * The host suffix the Toss app uses to serve dogfood / private mini-apps.\n *\n * A `intoss-private://` (dogfood) entry maps to a host such as\n * `aitc-sdk-example.private-apps.tossmini.com`. A production `intoss://`\n * entry is served from `*.apps.tossmini.com` — the `.private-apps.` segment\n * is absent. Confirmed live over CDP for mini-app 31146; the exact production\n * host is to be re-confirmed once 31146 passes review (spec open question 2).\n */\nconst PRIVATE_APPS_HOST_SUFFIX = '.private-apps.tossmini.com';\n\n/**\n * Returns whether `hostname` is a `*.private-apps.tossmini.com` subdomain —\n * the host the Toss app reserves for dogfood / private mini-app entries.\n *\n * The match is an exact suffix check, not a substring `.includes()`: a\n * substring test would also accept an attacker-controlled host like\n * `private-apps.tossmini.com.evil.example`, which ends in `.example`, not in\n * `.tossmini.com`. Requiring the string to END with the suffix closes that.\n * The leading `.` in the suffix also forces a real subdomain label, so a\n * bare `private-apps.tossmini.com` (no mini-app subdomain) does not match.\n */\nexport function isPrivateAppsHost(hostname: string): boolean {\n return hostname.endsWith(PRIVATE_APPS_HOST_SUFFIX);\n}\n\n/**\n * Pure function that evaluates the runtime debug activation layers (B and C).\n *\n * Has no side effects. The input is explicit. Returns a discriminated union\n * so callers can pattern-match on `result.attach`.\n *\n * Layer A (build-time) is intentionally not evaluated here — see the file-level\n * comment. By the time this function runs, the consumer's `if (__DEBUG_BUILD__)`\n * guard has already passed; this function only decides B and C.\n *\n * @example\n * ```ts\n * const result = evaluateDebugGate({\n * hostname: window.location.hostname,\n * searchParams: new URLSearchParams(window.location.search),\n * });\n * if (result.attach) {\n * // Proceed to load Chii client\n * }\n * ```\n */\nexport function evaluateDebugGate(input: GateInput): GateResult {\n // Layer B1 — host allowlist (the security gate).\n // The page must be served from a `*.private-apps.tossmini.com` host. A\n // production `intoss://` entry is served from `*.apps.tossmini.com` and is\n // rejected here. This is what stops a dogfood build that somehow reaches a\n // production entry from attaching: Layer A keeps debug code out of release\n // bundles, and this layer keeps a dogfood bundle that lands on a production\n // host from attaching even though its code is present.\n if (!isPrivateAppsHost(input.hostname)) {\n return { attach: false, reason: 'host' };\n }\n\n // Layer B2 — runtime entry query gate.\n // `_deploymentId` must be present and non-empty. The `intoss-private://`\n // scheme used for dogfood entries includes this param; general user entry\n // paths do not.\n const deploymentId = input.searchParams.get('_deploymentId') ?? '';\n if (deploymentId === '') {\n return { attach: false, reason: 'entry' };\n }\n\n // Layer C — explicit opt-in gate.\n // Require `debug=1` so that an operator who opens a dogfood URL by accident\n // does not inadvertently trigger the debug surface.\n const debugParam = input.searchParams.get('debug');\n if (debugParam !== '1') {\n return { attach: false, reason: 'opt-in' };\n }\n\n // Layer C continued — relay URL validation.\n // `relay=<wss-url>` must be present and must use the `wss:` scheme.\n // Plain `ws:` is rejected (no TLS). `http:`/`https:` are rejected.\n const relayRaw = input.searchParams.get('relay') ?? '';\n if (relayRaw === '') {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n let relayUrl: URL;\n try {\n relayUrl = new URL(relayRaw);\n } catch {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n if (relayUrl.protocol !== 'wss:') {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n // Layer C3 — TOTP auth gate (fail-fast, only when a verifier is injected).\n // The `at` query param carries the current TOTP code. Absent or invalid code\n // → BLOCKED. When no verifier is provided (TOTP disabled), this check is\n // skipped entirely for backward compatibility.\n //\n // SECRET-HANDLING: we do NOT log `code`, the verifier's result, or anything\n // derived from the secret. Only the `'auth'` enum is surfaced on failure.\n if (input.verifyTotpCode !== undefined) {\n const code = input.searchParams.get('at') ?? '';\n if (!input.verifyTotpCode(code)) {\n return { attach: false, reason: 'auth' };\n }\n }\n\n return { attach: true, relayUrl: relayUrl.href, deploymentId };\n}\n","/**\n * In-app Chii target injection for the debug attach flow.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n * \"MCP attach\" topology section — Phase 1 browser-side implementation.\n *\n * This module bridges the 3-layer gate result to a Chii `target.js` script\n * injection. The Chii npm package is the relay SERVER — the in-app side is\n * a plain `<script src=\"…/target.js\">` pointing at the relay host. No chii\n * npm dependency is needed here.\n */\n\nimport { setScreenAwakeMode } from '@apps-in-toss/web-framework';\nimport { checkDebugGate, type GateResult } from './index.js';\n\n/**\n * Converts a validated `wss:` relay URL into the Chii `target.js` script URL.\n *\n * Scheme is mapped `wss:` → `https:`. Host and port are preserved.\n * Pathname is set to `/target.js` regardless of the relay path.\n * Query params and hash from the relay URL are dropped — the target script\n * URL is a static asset path on the same host.\n *\n * @example\n * deriveTargetScriptUrl('wss://abc.trycloudflare.com/relay')\n * // → 'https://abc.trycloudflare.com/target.js'\n *\n * deriveTargetScriptUrl('wss://h.example.com:9100/')\n * // → 'https://h.example.com:9100/target.js'\n */\nexport function deriveTargetScriptUrl(relayUrl: string): string {\n const u = new URL(relayUrl);\n u.protocol = 'https:';\n u.pathname = '/target.js';\n u.search = '';\n u.hash = '';\n return u.toString();\n}\n\n/** Module-level guard against double-injection within a page lifecycle. */\nlet attached = false;\n\n/**\n * Evaluates the 3-layer debug gate and, if the gate passes, injects the Chii\n * `target.js` script into `document.head`.\n *\n * Idempotent — calling more than once is safe. The second call is a no-op if\n * a script with the same `src` is already present in the document, and the\n * module-level `attached` flag prevents redundant DOM queries after the first\n * successful injection.\n *\n * Safe to call even if `document` is somehow unavailable (defensive boundary\n * guard — in practice this always runs in a real WebView).\n *\n * **keepAwake side effect**: on a successful attach, `setScreenAwakeMode({\n * enabled: true })` is called so the phone screen stays awake during the debug\n * session. A `beforeunload` handler restores normal sleep on page unload.\n * Opt out by adding `noKeepAwake=1` to the page URL query string — the check\n * reads `window.location.search` directly, consistent with other guards in\n * this file.\n *\n * @param gateResult - Optional pre-evaluated gate result for testability.\n * Defaults to `checkDebugGate()` which reads the current page URL. Passing a\n * custom value avoids the need to manipulate `window.location` in tests.\n */\nexport function maybeAttach(gateResult: GateResult = checkDebugGate()): void {\n if (!gateResult.attach) {\n console.debug(\n `[@ait-co/devtools] debug attach skipped — gate blocked (reason: ${gateResult.reason})`,\n );\n return;\n }\n\n // Guard against double-injection across repeated calls.\n if (attached) {\n return;\n }\n\n // Defensive: if document is not available (unusual, but possible in some\n // SSR-adjacent edge cases), bail silently rather than throwing.\n if (typeof document === 'undefined') {\n return;\n }\n\n const src = deriveTargetScriptUrl(gateResult.relayUrl);\n\n // Also guard against a script with the same src already in the DOM\n // (e.g. injected by a different code path or a page reload within SPA).\n const existing = document.querySelector<HTMLScriptElement>(`script[src=\"${src}\"]`);\n if (existing !== null) {\n attached = true;\n return;\n }\n\n const script = document.createElement('script');\n script.src = src;\n script.async = true;\n (document.head ?? document.documentElement).appendChild(script);\n\n attached = true;\n\n // keepAwake — keep phone screen on during the debug session.\n // Opt out via noKeepAwake=1 in the URL (consistent with direct window reads\n // used throughout this file).\n if (\n typeof window !== 'undefined' &&\n new URLSearchParams(window.location.search).get('noKeepAwake') === '1'\n ) {\n return;\n }\n\n setScreenAwakeMode({ enabled: true })\n .then(() => {\n // Restore normal sleep on page unload — only if the enable call succeeded\n // (nothing to restore if it failed).\n window.addEventListener(\n 'beforeunload',\n () => {\n setScreenAwakeMode({ enabled: false }).catch(() => {});\n },\n { once: true },\n );\n })\n .catch((err) => {\n // Swallow rejection so attach never breaks — some platforms/mock reject.\n console.debug('[@ait-co/devtools] setScreenAwakeMode failed:', err);\n });\n}\n","/**\n * @ait-co/devtools/in-app entry point.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n *\n * Phase 1 — gate + browser-side Chii target injection.\n * WebSocket relay, QR/paste UI, and AI-host MCP bin are later phases that\n * require real-device validation and are not included here.\n *\n * This thin entry reads `window.location` and calls the pure\n * {@link evaluateDebugGate} function. All testable logic lives in `./gate.ts`\n * and `./attach.ts`, not here.\n *\n * Layer A of the activation gate (build-time) is NOT enforced in this module.\n * It is the consumer's responsibility: the consumer wraps its\n * `import('@ait-co/devtools/in-app')` call site in `if (__DEBUG_BUILD__) { … }`\n * (see sdk-example `src/main.tsx`), where `__DEBUG_BUILD__` is a\n * consumer-build-time constant. A release consumer build folds that constant\n * to `false` and dead-code-eliminates this whole module. This package is\n * pre-built and ships with `__DEBUG_BUILD__` already resolved at devtools'\n * publish time, so it could never re-evaluate the consumer's build channel —\n * which is exactly why Layer A lives at the consumer guard, not here.\n */\n\nimport { evaluateDebugGate, type GateResult } from './gate.js';\n\nexport { deriveTargetScriptUrl, maybeAttach } from './attach.js';\nexport type { GateInput, GateResult, GateResultAttach, GateResultBlocked } from './gate.js';\nexport { evaluateDebugGate, isPrivateAppsHost } from './gate.js';\n\n/**\n * Evaluates the runtime debug activation layers (B and C) against the current\n * page URL.\n *\n * Returns the gate result. Callers can check `result.attach` to decide whether\n * to proceed with debug surface attachment.\n *\n * This function reads `window.location` only — both the hostname (Layer B1\n * host allowlist) and the search params (Layers B2 and C). Layer A\n * (build-time) is enforced by the consumer's `if (__DEBUG_BUILD__)` guard\n * around the import site, not here — see the file-level comment. Consumers\n * call this with no arguments, so the Layer B1 host check is picked up with\n * no change at the call site.\n */\nexport function checkDebugGate(): GateResult {\n return evaluateDebugGate({\n hostname: window.location.hostname,\n searchParams: new URLSearchParams(window.location.search),\n });\n}\n"],"mappings":";;;;;;;;;;;AA8JA,MAAM,2BAA2B;;;;;;;;;;;;AAajC,SAAgB,kBAAkB,UAA2B;AAC3D,QAAO,SAAS,SAAS,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;AAwBpD,SAAgB,kBAAkB,OAA8B;AAQ9D,KAAI,CAAC,kBAAkB,MAAM,SAAS,CACpC,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAQ;CAO1C,MAAM,eAAe,MAAM,aAAa,IAAI,gBAAgB,IAAI;AAChE,KAAI,iBAAiB,GACnB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAS;AAO3C,KADmB,MAAM,aAAa,IAAI,QAAQ,KAC/B,IACjB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAU;CAM5C,MAAM,WAAW,MAAM,aAAa,IAAI,QAAQ,IAAI;AACpD,KAAI,aAAa,GACf,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAiB;CAGnD,IAAI;AACJ,KAAI;AACF,aAAW,IAAI,IAAI,SAAS;SACtB;AACN,SAAO;GAAE,QAAQ;GAAO,QAAQ;GAAiB;;AAGnD,KAAI,SAAS,aAAa,OACxB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAiB;AAUnD,KAAI,MAAM,mBAAmB,KAAA,GAAW;EACtC,MAAM,OAAO,MAAM,aAAa,IAAI,KAAK,IAAI;AAC7C,MAAI,CAAC,MAAM,eAAe,KAAK,CAC7B,QAAO;GAAE,QAAQ;GAAO,QAAQ;GAAQ;;AAI5C,QAAO;EAAE,QAAQ;EAAM,UAAU,SAAS;EAAM;EAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACpOhE,SAAgB,sBAAsB,UAA0B;CAC9D,MAAM,IAAI,IAAI,IAAI,SAAS;AAC3B,GAAE,WAAW;AACb,GAAE,WAAW;AACb,GAAE,SAAS;AACX,GAAE,OAAO;AACT,QAAO,EAAE,UAAU;;;AAIrB,IAAI,WAAW;;;;;;;;;;;;;;;;;;;;;;;;AAyBf,SAAgB,YAAY,aAAyB,gBAAgB,EAAQ;AAC3E,KAAI,CAAC,WAAW,QAAQ;AACtB,UAAQ,MACN,mEAAmE,WAAW,OAAO,GACtF;AACD;;AAIF,KAAI,SACF;AAKF,KAAI,OAAO,aAAa,YACtB;CAGF,MAAM,MAAM,sBAAsB,WAAW,SAAS;AAKtD,KADiB,SAAS,cAAiC,eAAe,IAAI,IAAI,KACjE,MAAM;AACrB,aAAW;AACX;;CAGF,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,QAAO,MAAM;AACb,QAAO,QAAQ;AACf,EAAC,SAAS,QAAQ,SAAS,iBAAiB,YAAY,OAAO;AAE/D,YAAW;AAKX,KACE,OAAO,WAAW,eAClB,IAAI,gBAAgB,OAAO,SAAS,OAAO,CAAC,IAAI,cAAc,KAAK,IAEnE;AAGF,oBAAmB,EAAE,SAAS,MAAM,CAAC,CAClC,WAAW;AAGV,SAAO,iBACL,sBACM;AACJ,sBAAmB,EAAE,SAAS,OAAO,CAAC,CAAC,YAAY,GAAG;KAExD,EAAE,MAAM,MAAM,CACf;GACD,CACD,OAAO,QAAQ;AAEd,UAAQ,MAAM,iDAAiD,IAAI;GACnE;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AClFN,SAAgB,iBAA6B;AAC3C,QAAO,kBAAkB;EACvB,UAAU,OAAO,SAAS;EAC1B,cAAc,IAAI,gBAAgB,OAAO,SAAS,OAAO;EAC1D,CAAC"}
|
package/dist/mcp/cli.js
CHANGED
|
@@ -3148,6 +3148,7 @@ function readDevtoolsVersion() {
|
|
|
3148
3148
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
3149
3149
|
*
|
|
3150
3150
|
* Branch rules (evaluated in priority order):
|
|
3151
|
+
* 0. tunnel.droppedAt non-null → restart (permanent tunnel drop — highest priority)
|
|
3151
3152
|
* 1. tunnel.up === false AND env is relay → restart (relay needs a live tunnel)
|
|
3152
3153
|
* 1b. tunnel.up === false AND env is mock → wait_for_page (local target: tunnel-less is normal)
|
|
3153
3154
|
* 2. tunnel.up, pages empty, env === relay → build_attach_url (start attach)
|
|
@@ -3157,6 +3158,10 @@ function readDevtoolsVersion() {
|
|
|
3157
3158
|
* Pure — does not throw; receives the final assembled snapshot fields.
|
|
3158
3159
|
*/
|
|
3159
3160
|
function computeNextRecommendedAction(tunnel, pages, env) {
|
|
3161
|
+
if (tunnel.droppedAt != null) return {
|
|
3162
|
+
tool: "restart",
|
|
3163
|
+
reason: `tunnel permanently dropped at ${tunnel.droppedAt} after ${tunnel.reissueAttempts} reissue attempt(s) — restart the MCP server (npx @ait-co/devtools devtools-mcp)`
|
|
3164
|
+
};
|
|
3160
3165
|
if (!tunnel.up) if (!isRelayEnv(env)) {
|
|
3161
3166
|
if (pages !== null && pages.pages.length === 0 && !pages.crashDetectedAt) return {
|
|
3162
3167
|
tool: "wait_for_page",
|
|
@@ -3187,7 +3192,7 @@ function computeNextRecommendedAction(tunnel, pages, env) {
|
|
|
3187
3192
|
* - Lock file data contains only pid + startedAt + wssUrl — no secrets.
|
|
3188
3193
|
*/
|
|
3189
3194
|
async function getDiagnostics(input) {
|
|
3190
|
-
const { tunnel, connection, env, envReason, collector, readLock: readLockFn, recentErrorsLimit = 10, getMcpVersion = readMcpSdkVersion } = input;
|
|
3195
|
+
const { tunnel, connection, env, envReason, collector, readLock: readLockFn, recentErrorsLimit = 10, getMcpVersion = readMcpSdkVersion, checkParentAlive = () => isPidAlive(process.ppid) } = input;
|
|
3191
3196
|
const [mcpVersion, devtoolsVersion] = await Promise.all([getMcpVersion(), Promise.resolve(readDevtoolsVersion())]);
|
|
3192
3197
|
const lockData = readLockFn();
|
|
3193
3198
|
const serverLockHolder = lockData ? {
|
|
@@ -3199,7 +3204,9 @@ async function getDiagnostics(input) {
|
|
|
3199
3204
|
up: tunnel.up,
|
|
3200
3205
|
wssUrl: tunnel.wssUrl,
|
|
3201
3206
|
pid: lockData?.pid ?? null,
|
|
3202
|
-
startedAt: lockData?.startedAt ?? null
|
|
3207
|
+
startedAt: lockData?.startedAt ?? null,
|
|
3208
|
+
droppedAt: tunnel.droppedAt ?? null,
|
|
3209
|
+
reissueAttempts: tunnel.reissueAttempts ?? 0
|
|
3203
3210
|
};
|
|
3204
3211
|
let pages = null;
|
|
3205
3212
|
if (connection !== void 0) try {
|
|
@@ -3223,6 +3230,11 @@ async function getDiagnostics(input) {
|
|
|
3223
3230
|
liveGuardActive: isLiveRelayEnv(env)
|
|
3224
3231
|
},
|
|
3225
3232
|
serverLockHolder,
|
|
3233
|
+
process: {
|
|
3234
|
+
pid: process.pid,
|
|
3235
|
+
ppid: process.ppid,
|
|
3236
|
+
parentAlive: checkParentAlive()
|
|
3237
|
+
},
|
|
3226
3238
|
nextRecommendedAction
|
|
3227
3239
|
};
|
|
3228
3240
|
}
|
|
@@ -3549,8 +3561,10 @@ function extractDeploymentId(schemeUrl) {
|
|
|
3549
3561
|
}
|
|
3550
3562
|
/**
|
|
3551
3563
|
* Waits for the first target matching `filterFn` to attach, using the
|
|
3552
|
-
* event-driven `waitForFirstTarget()`
|
|
3553
|
-
*
|
|
3564
|
+
* event-driven `waitForFirstTarget()` when the connection supports it
|
|
3565
|
+
* (interface-optional member, present on `ChiiCdpConnection`), or falling
|
|
3566
|
+
* back to a polling loop for connections that don't implement it (test fakes,
|
|
3567
|
+
* `LocalCdpConnection`).
|
|
3554
3568
|
*
|
|
3555
3569
|
* This eliminates the polling-only race that previously caused `wait_for_attach`
|
|
3556
3570
|
* to resolve before the relay had observed the first inbound CDP message from
|
|
@@ -3559,10 +3573,10 @@ function extractDeploymentId(schemeUrl) {
|
|
|
3559
3573
|
* @param connection - The CDP connection (production or fake).
|
|
3560
3574
|
* @param filterFn - Resolves when this predicate is satisfied.
|
|
3561
3575
|
* @param timeoutMs - Maximum wait time in ms.
|
|
3562
|
-
* @param pollIntervalMs - Fallback poll interval for
|
|
3576
|
+
* @param pollIntervalMs - Fallback poll interval for connections without waitForFirstTarget.
|
|
3563
3577
|
*/
|
|
3564
3578
|
function waitForAttachWithEvents(connection, filterFn, timeoutMs, pollIntervalMs = 1e3) {
|
|
3565
|
-
if (connection
|
|
3579
|
+
if (connection.waitForFirstTarget) return connection.waitForFirstTarget(filterFn, timeoutMs, pollIntervalMs);
|
|
3566
3580
|
return new Promise((resolve, reject) => {
|
|
3567
3581
|
const deadline = Date.now() + timeoutMs;
|
|
3568
3582
|
let settled = false;
|
|
@@ -3611,7 +3625,7 @@ function createDebugServer(deps) {
|
|
|
3611
3625
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
3612
3626
|
const server = new Server({
|
|
3613
3627
|
name: "ait-debug",
|
|
3614
|
-
version: "0.1.
|
|
3628
|
+
version: "0.1.51"
|
|
3615
3629
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
3616
3630
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
3617
3631
|
const env = resolveEnvironment();
|
|
@@ -3840,8 +3854,8 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3840
3854
|
await connection.enableDomains();
|
|
3841
3855
|
} catch (err) {
|
|
3842
3856
|
if (name === "list_pages") {
|
|
3843
|
-
|
|
3844
|
-
await connection.refreshTargets();
|
|
3857
|
+
try {
|
|
3858
|
+
await connection.refreshTargets?.();
|
|
3845
3859
|
} catch {}
|
|
3846
3860
|
const pagesData = listPages(connection, getTunnelStatus());
|
|
3847
3861
|
const attached = connection.listTargets().length > 0;
|
|
@@ -3858,8 +3872,8 @@ ${browserResult.httpUrl}\n또는 PNG로 받기: ${browserResult.pngUrl}` + stder
|
|
|
3858
3872
|
}
|
|
3859
3873
|
case "list_network_requests": return jsonResult$1(listNetworkRequests(connection));
|
|
3860
3874
|
case "list_pages": {
|
|
3861
|
-
|
|
3862
|
-
await connection.refreshTargets();
|
|
3875
|
+
try {
|
|
3876
|
+
await connection.refreshTargets?.();
|
|
3863
3877
|
} catch {}
|
|
3864
3878
|
const listPagesData = listPages(connection, getTunnelStatus());
|
|
3865
3879
|
const listPagesAttached = connection.listTargets().length > 0;
|
|
@@ -3991,6 +4005,45 @@ function startAttachWatcher(connection, server, intervalMs = 1e3, onFirstAttach)
|
|
|
3991
4005
|
} };
|
|
3992
4006
|
}
|
|
3993
4007
|
/**
|
|
4008
|
+
* Starts a periodic watcher that detects when the parent process (e.g. Claude
|
|
4009
|
+
* Code) has died without sending SIGTERM/SIGHUP, and calls `onOrphaned` so the
|
|
4010
|
+
* daemon can self-terminate rather than running as a zombie.
|
|
4011
|
+
*
|
|
4012
|
+
* Mirrors the `startAttachWatcher` pattern: `setInterval`-based, returns
|
|
4013
|
+
* `{ stop(): void }`, injectable deps for testability.
|
|
4014
|
+
*
|
|
4015
|
+
* @param onOrphaned - Called once when the parent is gone.
|
|
4016
|
+
* @param opts.intervalMs - Poll interval in milliseconds (default 5 000).
|
|
4017
|
+
* @param opts.initialPpid - Parent PID to watch (default `process.ppid`).
|
|
4018
|
+
* @param opts.isAlive - Predicate to test if a PID is running (default `isPidAlive`).
|
|
4019
|
+
* @param opts.getPpid - Supplier of current ppid (default `() => process.ppid`).
|
|
4020
|
+
* Detects ppid changes as well as death.
|
|
4021
|
+
* @param opts.log - Logger (default `process.stderr.write`).
|
|
4022
|
+
*
|
|
4023
|
+
* @returns `stop` — call during shutdown to clear the interval.
|
|
4024
|
+
*/
|
|
4025
|
+
function startParentWatcher(onOrphaned, opts) {
|
|
4026
|
+
const { intervalMs = 5e3, initialPpid = process.ppid, isAlive = isPidAlive, getPpid = () => process.ppid, log = (msg) => process.stderr.write(msg) } = opts ?? {};
|
|
4027
|
+
if (initialPpid <= 1) {
|
|
4028
|
+
log("[ait-debug] parent-pid watcher: no parent to watch (ppid<=1), skipping\n");
|
|
4029
|
+
return { stop() {} };
|
|
4030
|
+
}
|
|
4031
|
+
let fired = false;
|
|
4032
|
+
const handle = setInterval(() => {
|
|
4033
|
+
if (fired) return;
|
|
4034
|
+
const currentPpid = getPpid();
|
|
4035
|
+
if (currentPpid !== initialPpid || !isAlive(initialPpid)) {
|
|
4036
|
+
fired = true;
|
|
4037
|
+
clearInterval(handle);
|
|
4038
|
+
log(`[ait-debug] parent-pid watcher: parent PID ${initialPpid} is gone (currentPpid=${currentPpid}) — shutting down\n`);
|
|
4039
|
+
onOrphaned();
|
|
4040
|
+
}
|
|
4041
|
+
}, intervalMs);
|
|
4042
|
+
return { stop() {
|
|
4043
|
+
clearInterval(handle);
|
|
4044
|
+
} };
|
|
4045
|
+
}
|
|
4046
|
+
/**
|
|
3994
4047
|
* Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a
|
|
3995
4048
|
* `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.
|
|
3996
4049
|
*
|
|
@@ -4014,6 +4067,19 @@ function buildRelayVerifyAuth() {
|
|
|
4014
4067
|
};
|
|
4015
4068
|
}
|
|
4016
4069
|
/**
|
|
4070
|
+
* Factory that constructs a `ChiiCdpConnection` for the given relay base URL.
|
|
4071
|
+
*
|
|
4072
|
+
* Introduced as a named seam so PR-2 (dual-connection, #348) can defer
|
|
4073
|
+
* construction to first-activation time by moving or replacing this call —
|
|
4074
|
+
* without changing the current eager construction order at startup.
|
|
4075
|
+
*
|
|
4076
|
+
* The relay base URL is only available after `startChiiRelay()` resolves, so
|
|
4077
|
+
* the factory is called right after that point (same as before this refactor).
|
|
4078
|
+
*/
|
|
4079
|
+
function createRelayConnection(relayBaseUrl) {
|
|
4080
|
+
return new ChiiCdpConnection({ relayBaseUrl });
|
|
4081
|
+
}
|
|
4082
|
+
/**
|
|
4017
4083
|
* Boots the live debug stack and serves it over stdio:
|
|
4018
4084
|
* 1. start the Chii relay on an OS-assigned port (with TOTP auth if
|
|
4019
4085
|
* AIT_DEBUG_TOTP_SECRET is set),
|
|
@@ -4070,7 +4136,7 @@ async function runDebugServer(options = {}) {
|
|
|
4070
4136
|
}, (err) => {
|
|
4071
4137
|
logError("tunnel.down", { msg: `Failed to open cloudflared quick tunnel: ${err instanceof Error ? err.message : String(err)}. The relay is up locally; attach over the public URL is unavailable until the tunnel starts.` });
|
|
4072
4138
|
});
|
|
4073
|
-
const connection =
|
|
4139
|
+
const connection = createRelayConnection(relay.baseUrl);
|
|
4074
4140
|
const aitSource = new ChiiAitSource(connection);
|
|
4075
4141
|
let qrServer;
|
|
4076
4142
|
try {
|
|
@@ -4094,9 +4160,11 @@ async function runDebugServer(options = {}) {
|
|
|
4094
4160
|
const transport = new StdioServerTransport();
|
|
4095
4161
|
let closed = false;
|
|
4096
4162
|
let attachWatcher = null;
|
|
4163
|
+
let parentWatcher = null;
|
|
4097
4164
|
const shutdown = () => {
|
|
4098
4165
|
if (closed) return;
|
|
4099
4166
|
closed = true;
|
|
4167
|
+
parentWatcher?.stop();
|
|
4100
4168
|
attachWatcher?.stop();
|
|
4101
4169
|
tunnelProbe?.stop();
|
|
4102
4170
|
connection.close();
|
|
@@ -4112,6 +4180,7 @@ async function runDebugServer(options = {}) {
|
|
|
4112
4180
|
process.on("exit", () => {
|
|
4113
4181
|
if (!closed) {
|
|
4114
4182
|
closed = true;
|
|
4183
|
+
parentWatcher?.stop();
|
|
4115
4184
|
attachWatcher?.stop();
|
|
4116
4185
|
tunnelProbe?.stop();
|
|
4117
4186
|
tunnel?.stop();
|
|
@@ -4142,6 +4211,20 @@ async function runDebugServer(options = {}) {
|
|
|
4142
4211
|
defaultEnv: "relay-dev"
|
|
4143
4212
|
}));
|
|
4144
4213
|
});
|
|
4214
|
+
if (process.env.AIT_DEBUG_NO_PARENT_WATCH !== "1") {
|
|
4215
|
+
parentWatcher = startParentWatcher(() => {
|
|
4216
|
+
shutdown();
|
|
4217
|
+
process.exit(0);
|
|
4218
|
+
}, { intervalMs: 5e3 });
|
|
4219
|
+
process.stdin.once("end", () => {
|
|
4220
|
+
shutdown();
|
|
4221
|
+
process.exit(0);
|
|
4222
|
+
});
|
|
4223
|
+
process.stdin.once("close", () => {
|
|
4224
|
+
shutdown();
|
|
4225
|
+
process.exit(0);
|
|
4226
|
+
});
|
|
4227
|
+
}
|
|
4145
4228
|
}
|
|
4146
4229
|
/**
|
|
4147
4230
|
* Boots the local-browser debug stack and serves it over stdio:
|
|
@@ -4183,9 +4266,11 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4183
4266
|
const transport = new StdioServerTransport();
|
|
4184
4267
|
let closed = false;
|
|
4185
4268
|
let attachWatcher = null;
|
|
4269
|
+
let parentWatcher = null;
|
|
4186
4270
|
const shutdown = () => {
|
|
4187
4271
|
if (closed) return;
|
|
4188
4272
|
closed = true;
|
|
4273
|
+
parentWatcher?.stop();
|
|
4189
4274
|
attachWatcher?.stop();
|
|
4190
4275
|
connection.close();
|
|
4191
4276
|
chromium.stop();
|
|
@@ -4198,6 +4283,7 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4198
4283
|
process.on("exit", () => {
|
|
4199
4284
|
if (!closed) {
|
|
4200
4285
|
closed = true;
|
|
4286
|
+
parentWatcher?.stop();
|
|
4201
4287
|
attachWatcher?.stop();
|
|
4202
4288
|
chromium.stop();
|
|
4203
4289
|
lockHandle.release();
|
|
@@ -4223,6 +4309,20 @@ async function runLocalDebugServer(options = {}) {
|
|
|
4223
4309
|
});
|
|
4224
4310
|
await server.connect(transport);
|
|
4225
4311
|
attachWatcher = startAttachWatcher(connection, server);
|
|
4312
|
+
if (process.env.AIT_DEBUG_NO_PARENT_WATCH !== "1") {
|
|
4313
|
+
parentWatcher = startParentWatcher(() => {
|
|
4314
|
+
shutdown();
|
|
4315
|
+
process.exit(0);
|
|
4316
|
+
}, { intervalMs: 5e3 });
|
|
4317
|
+
process.stdin.once("end", () => {
|
|
4318
|
+
shutdown();
|
|
4319
|
+
process.exit(0);
|
|
4320
|
+
});
|
|
4321
|
+
process.stdin.once("close", () => {
|
|
4322
|
+
shutdown();
|
|
4323
|
+
process.exit(0);
|
|
4324
|
+
});
|
|
4325
|
+
}
|
|
4226
4326
|
}
|
|
4227
4327
|
//#endregion
|
|
4228
4328
|
//#region src/mcp/ait-http-source.ts
|
|
@@ -4635,16 +4735,10 @@ async function buildDevMeasureSafeArea(aitSource) {
|
|
|
4635
4735
|
*/
|
|
4636
4736
|
async function buildDevCallSdk(methodName, aitSource) {
|
|
4637
4737
|
switch (methodName) {
|
|
4638
|
-
case "getOperationalEnvironment": {
|
|
4639
|
-
|
|
4640
|
-
|
|
4641
|
-
|
|
4642
|
-
value: {
|
|
4643
|
-
environment: env.environment,
|
|
4644
|
-
sdkVersion: env.sdkVersion
|
|
4645
|
-
}
|
|
4646
|
-
};
|
|
4647
|
-
}
|
|
4738
|
+
case "getOperationalEnvironment": return {
|
|
4739
|
+
ok: true,
|
|
4740
|
+
value: (await aitSource.get("AIT.getOperationalEnvironment")).environment
|
|
4741
|
+
};
|
|
4648
4742
|
default: return {
|
|
4649
4743
|
ok: false,
|
|
4650
4744
|
error: `dev-mode-unsupported: "${methodName}"은 dev-mode에서 직접 호출할 수 없습니다. CDP bridge(window.__sdkCall)가 없으므로 실제 SDK 호출은 \`--mode=local\` 또는 debug 모드에서만 가능합니다. 지원 메서드: getOperationalEnvironment (mock state에서 읽음).`
|
|
@@ -4658,7 +4752,7 @@ function createDevServer(deps = {}) {
|
|
|
4658
4752
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
4659
4753
|
const server = new Server({
|
|
4660
4754
|
name: "ait-devtools",
|
|
4661
|
-
version: "0.1.
|
|
4755
|
+
version: "0.1.51"
|
|
4662
4756
|
}, { capabilities: { tools: {} } });
|
|
4663
4757
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
4664
4758
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|