@ait-co/devtools 0.1.76 → 0.1.78
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 +2 -2
- package/README.md +6 -6
- package/dist/deeplink-B-94XmWA.js.map +1 -1
- package/dist/deeplink-BLU2_hg6.cjs.map +1 -1
- package/dist/deeplink-CU6opogq.cjs.map +1 -1
- package/dist/deeplink-CYqDwVYs.js.map +1 -1
- package/dist/in-app/auto.d.ts +46 -11
- package/dist/in-app/auto.d.ts.map +1 -1
- package/dist/in-app/auto.js +55 -12
- package/dist/in-app/auto.js.map +1 -1
- package/dist/mcp/cli.js +34 -34
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +6 -6
- package/dist/mcp/server.js.map +1 -1
- package/dist/panel/index.js +8 -8
- package/dist/panel/index.js.map +1 -1
- package/dist/{qr-http-server-CDO6o2nr.js → qr-http-server-BQhf6N3z.js} +9 -9
- package/dist/qr-http-server-BQhf6N3z.js.map +1 -0
- package/dist/{qr-http-server-jMC1nVqY.cjs → qr-http-server-CC2d-Tc3.cjs} +9 -9
- package/dist/qr-http-server-CC2d-Tc3.cjs.map +1 -0
- package/dist/{qr-http-server-DznDIcJF.js → qr-http-server-CRK_XlW8.js} +9 -9
- package/dist/qr-http-server-CRK_XlW8.js.map +1 -0
- package/dist/{qr-http-server-D0v9ooAD.cjs → qr-http-server-CTX8GtQV.cjs} +9 -9
- package/dist/qr-http-server-CTX8GtQV.cjs.map +1 -0
- package/dist/{relay-secret-store-D-W-WaSx.js → relay-secret-store-6pPzLkUO.js} +2 -2
- package/dist/{relay-secret-store-D-W-WaSx.js.map → relay-secret-store-6pPzLkUO.js.map} +1 -1
- package/dist/{relay-secret-store-BFOEhsLO.cjs → relay-secret-store-CsCOfpWt.cjs} +2 -2
- package/dist/{relay-secret-store-BFOEhsLO.cjs.map → relay-secret-store-CsCOfpWt.cjs.map} +1 -1
- package/dist/{relay-secret-store-C_LUxvAp.js → relay-secret-store-J0SUUXjH.js} +2 -2
- package/dist/{relay-secret-store-C_LUxvAp.js.map → relay-secret-store-J0SUUXjH.js.map} +1 -1
- package/dist/{relay-url-store-DjKJJZ0d.js → relay-url-store-B_wrNe5A.js} +2 -2
- package/dist/{relay-url-store-DjKJJZ0d.js.map → relay-url-store-B_wrNe5A.js.map} +1 -1
- package/dist/{relay-url-store-DAh5KiJi.js → relay-url-store-CV8nScsn.js} +2 -2
- package/dist/{relay-url-store-DAh5KiJi.js.map → relay-url-store-CV8nScsn.js.map} +1 -1
- package/dist/{relay-url-store-2sy_l2bf.cjs → relay-url-store-Cx_SqWtl.cjs} +2 -2
- package/dist/{relay-url-store-2sy_l2bf.cjs.map → relay-url-store-Cx_SqWtl.cjs.map} +1 -1
- package/dist/totp-BcBNRoDD.js +3 -0
- package/dist/{totp-BxtxuEt4.js → totp-BmKSPb5d.js} +2 -2
- package/dist/{totp-D9rndqg_.cjs.map → totp-BmKSPb5d.js.map} +1 -1
- package/dist/{totp-D9rndqg_.cjs → totp-BwDZ6dUT.cjs} +2 -2
- package/dist/totp-BwDZ6dUT.cjs.map +1 -0
- package/dist/{totp-D4iTMA9U.cjs → totp-CNw0w89F.cjs} +3 -3
- package/dist/totp-CNw0w89F.cjs.map +1 -0
- package/dist/{totp-DbEfKQRi.js → totp-DYdP9N3o.js} +3 -3
- package/dist/totp-DYdP9N3o.js.map +1 -0
- package/dist/{totp-BfVk8gQe.js → totp-Xq3ACwkm.js} +3 -3
- package/dist/totp-Xq3ACwkm.js.map +1 -0
- package/dist/{tunnel-D7f-0enB.cjs → tunnel-CbtLkErI.cjs} +3 -3
- package/dist/{tunnel-D7f-0enB.cjs.map → tunnel-CbtLkErI.cjs.map} +1 -1
- package/dist/{tunnel-km3KkZrF.js → tunnel-vIjpD9wn.js} +3 -3
- package/dist/{tunnel-km3KkZrF.js.map → tunnel-vIjpD9wn.js.map} +1 -1
- package/dist/unplugin/index.cjs +4 -4
- package/dist/unplugin/index.js +4 -4
- package/dist/unplugin/tunnel.cjs +2 -2
- package/dist/unplugin/tunnel.js +2 -2
- package/package.json +1 -1
- package/dist/qr-http-server-CDO6o2nr.js.map +0 -1
- package/dist/qr-http-server-D0v9ooAD.cjs.map +0 -1
- package/dist/qr-http-server-DznDIcJF.js.map +0 -1
- package/dist/qr-http-server-jMC1nVqY.cjs.map +0 -1
- package/dist/totp-BfVk8gQe.js.map +0 -1
- package/dist/totp-BxtxuEt4.js.map +0 -1
- package/dist/totp-D4iTMA9U.cjs.map +0 -1
- package/dist/totp-D8f6qAEu.js +0 -3
- package/dist/totp-DbEfKQRi.js.map +0 -1
package/dist/in-app/auto.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"auto.js","names":[],"sources":["../../src/shared/relay-auth-close.ts","../../src/in-app/gate.ts","../../src/in-app/index.ts","../../src/in-app/attach.ts","../../src/in-app/auto.ts"],"sourcesContent":["/**\n * Shared constants for the relay's named TOTP-auth rejection (issue #478).\n *\n * Before #478 the relay rejected an unauthenticated WebSocket upgrade with a\n * raw `HTTP/1.1 401` + `socket.destroy()`. A handshake aborted that way is\n * indistinguishable from a network failure on the browser side — the\n * WebSocket only ever sees close code 1006, so the phone (env-2 launcher PWA)\n * could not tell \"stale TOTP code\" apart from \"tunnel down\" and stayed\n * silent. The fix is accept-then-close: complete the handshake, then close\n * with an application close code that NAMES the rejection.\n *\n * Three parties share this contract:\n * - `src/mcp/chii-relay.ts` (Node) sends the close frame / HTTP error body;\n * - `src/in-app/attach.ts` (browser) observes relay-bound WebSockets and\n * surfaces the code to the launcher shell;\n * - `src/mcp/chii-connection.ts` (Node daemon client) recognises the code\n * as an auth failure on its own `/client` dial (defensive — #439's fresh\n * code mint means it should not normally hit this).\n *\n * This module is intentionally dependency-free (no Node, no DOM) so it is\n * safe to import from both the browser in-app bundle and the MCP daemon\n * bundle.\n *\n * SECRET-HANDLING: these are fixed enum values. The close reason / error body\n * must never grow to carry a secret, a TOTP code, or a host.\n */\n\n/**\n * WebSocket close code sent by the relay when TOTP auth is rejected.\n *\n * 4000–4999 is the application-reserved range (RFC 6455 §7.4.2); 4401 mirrors\n * HTTP 401 so it reads as \"unauthorized\" at a glance.\n */\nexport const RELAY_AUTH_REJECT_CLOSE_CODE = 4401;\n\n/**\n * Close reason string accompanying {@link RELAY_AUTH_REJECT_CLOSE_CODE}, and\n * the `error` value of the relay's HTTP 401 JSON body. Enum string only —\n * never interpolated with request data.\n */\nexport const RELAY_AUTH_REJECT_REASON = 'totp-rejected';\n","/**\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:\n * B1 — host allowlist: `hostname` must be a `*.private-apps.tossmini.com`\n * subdomain (Toss dogfood entry) OR a `*.trycloudflare.com` host (env 2\n * PWA dev tunnel). The Toss app serves dogfood / private mini-apps from\n * a 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 * The env 2 tunnel host is allowed because it has no production runtime\n * (mock SDK, the developer's own dev server) — see {@link\n * isTrycloudflareHost}.\n * B2 — entry query: `_deploymentId` must be present and non-empty. Applies to\n * the Toss path only; the env 2 tunnel has no deployed bundle, so B2 is\n * skipped for `*.trycloudflare.com` hosts.\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 | _deploymentId | debug=1 | relay ok | TOTP ok* | result\n * neither | (any) | (any) | (any) | (any) | BLOCKED (host)\n * private-apps| absent | (any) | (any) | (any) | BLOCKED (entry)\n * private-apps| present | absent | (any) | (any) | BLOCKED (opt-in)\n * private-apps| present | present | invalid | (any) | BLOCKED (invalid-relay)\n * private-apps| present | present | valid | fail* | BLOCKED (auth)\n * private-apps| present | present | valid | pass/n/a | ATTACH\n * trycloudflare| (skipped) | absent | (any) | (any) | BLOCKED (opt-in)\n * trycloudflare| (skipped) | present | invalid | (any) | BLOCKED (invalid-relay)\n * trycloudflare| (skipped) | present | valid | fail* | BLOCKED (auth)\n * trycloudflare| (skipped) | 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 * For trycloudflare (env 2 tunnel) hosts B1 is bypassed and B2 is skipped;\n * C1/C2/C3 still apply identically. The ATTACH result carries\n * `deploymentId: ''` for tunnel hosts.\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 * The host suffix Cloudflare quick-tunnels serve from — the env 2 (PWA) entry.\n * See {@link isTrycloudflareHost} for why this host kind bypasses Layer B1.\n */\nconst TRYCLOUDFLARE_HOST_SUFFIX = '.trycloudflare.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 * The host suffix Cloudflare quick-tunnels use — the env 2 (PWA) entry.\n *\n * Env 2 serves the local Vite dev server through a `*.trycloudflare.com` quick\n * tunnel (`src/unplugin/tunnel.ts`). It has no Toss app, no `intoss-private://`\n * scheme, and — critically — no production runtime: the SDK is the devtools\n * mock, and the page is the developer's own dev build. The Layer B1 safety net\n * (which stops a dogfood build that lands on a Toss *production* host from\n * attaching) has nothing to protect against here, because env 2 has no\n * production host. So a trycloudflare host is allowed past B1 — but ONLY past\n * B1: the remaining layers (C1 opt-in, C2 relay, C3 TOTP) still apply, so a\n * leaked tunnel URL is still blocked by TOTP exactly as on the Toss path.\n *\n * The match is the same exact-suffix `endsWith` check as\n * {@link isPrivateAppsHost} — never a substring `.includes()`, which would\n * accept an attacker-controlled `evil.trycloudflare.com.example.com`. The\n * leading `.` forces a real subdomain label, so a bare `trycloudflare.com`\n * (no tunnel subdomain) does not match.\n */\nexport function isTrycloudflareHost(hostname: string): boolean {\n return hostname.endsWith(TRYCLOUDFLARE_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 // Two host kinds are allowed past B1:\n // - Toss dogfood: `*.private-apps.tossmini.com`. A production `intoss://`\n // entry is served from `*.apps.tossmini.com` and is rejected here. This\n // is what stops a dogfood build that somehow reaches a production entry\n // from attaching: Layer A keeps debug code out of release bundles, and\n // this layer keeps a dogfood bundle that lands on a production host from\n // attaching even though its code is present.\n // - Env 2 PWA tunnel: `*.trycloudflare.com`. This is the developer's own\n // local dev server (mock SDK, no production runtime), so the\n // production-entry hazard B1 guards against cannot occur. It bypasses B1\n // but NOT the remaining layers — C1/C2/C3 (incl. TOTP) still apply, so a\n // leaked tunnel URL is blocked exactly as on the Toss path. See\n // {@link isTrycloudflareHost}.\n const isTunnel = isTrycloudflareHost(input.hostname);\n if (!isPrivateAppsHost(input.hostname) && !isTunnel) {\n return { attach: false, reason: 'host' };\n }\n\n // Layer B2 — runtime entry query gate (Toss path only).\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. The env 2 tunnel has no deployed bundle and therefore no\n // `_deploymentId` — B2 is skipped for it, and `deploymentId` is reported as\n // the empty string on a tunnel attach (no consumer reads it; see attach.ts).\n let deploymentId = '';\n if (!isTunnel) {\n deploymentId = input.searchParams.get('_deploymentId') ?? '';\n if (deploymentId === '') {\n return { attach: false, reason: 'entry' };\n }\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 * @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, isTrycloudflareHost } 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","/**\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 {\n RELAY_AUTH_REJECT_CLOSE_CODE,\n RELAY_AUTH_REJECT_REASON,\n} from '../shared/relay-auth-close.js';\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` (or `/at/<code>/target.js` when a TOTP code\n * is given) regardless of the relay path. Query params and hash from the\n * relay URL are dropped — the target script URL is a static asset path on the\n * same host.\n *\n * TOTP path-prefix transport (issue #466): chii's stock `target.js` derives\n * its WS endpoint from the script `src` (`scriptEl.src.replace('target.js',\n * '')`), so embedding the current TOTP code in the script URL *path* is the\n * only way the phone-side WS upgrade can carry it — both the script fetch and\n * the derived `wss://<host>/at/<code>/target/<id>` dial inherit the prefix,\n * and the relay verifies + strips it before chii parses the URL. The\n * `window.ChiiServerUrl` + query alternative does NOT work: chii appends\n * `target/<id>` to the serverUrl string, which would land after a `?`.\n *\n * SECRET-HANDLING: `atCode` rides only inside the returned URL (the intended\n * transport — same exposure grade as the daemon client's `at=` query). It is\n * never logged here.\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/', '123456')\n * // → 'https://h.example.com:9100/at/123456/target.js'\n *\n * @param relayUrl - Validated `wss:` relay URL from the gate result.\n * @param atCode - Current TOTP code from the page URL's `at` query param, or\n * `null`/`undefined`/`''` to keep the legacy un-prefixed URL.\n */\nexport function deriveTargetScriptUrl(relayUrl: string, atCode?: string | null): string {\n const u = new URL(relayUrl);\n u.protocol = 'https:';\n u.pathname =\n atCode !== undefined && atCode !== null && atCode !== ''\n ? `/at/${encodeURIComponent(atCode)}/target.js`\n : '/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// Relay-origin WebSocket observer (issue #478)\n//\n// After a successful attach, chii's target.js owns its own reconnect loop —\n// `maybeAttach()` never re-runs, so a much-later reconnect carrying the stale\n// `/at/<code>/` prefix is rejected by the relay with NO in-page signal. The\n// relay now names that rejection (accept-then-close, code 4401), and this\n// observer is the in-page half: it watches relay-bound WebSockets for the\n// 4401 close and tells the parent launcher shell once, then fail-fasts any\n// further relay dials so the retry loop stops generating network traffic.\n// ---------------------------------------------------------------------------\n\n/** One-shot guard for the parent notification (both observer + onerror probe). */\nlet authExpiredNotified = false;\n\n/** Set once a relay-bound socket closed with 4401 — flips dials to fail-fast. */\nlet relayAuthExpired = false;\n\n/** Guard against stacking multiple observer wrappers on window.WebSocket. */\nlet wsObserverInstalled = false;\n\n/**\n * Posts the `auth-expired` block signal to the parent launcher shell, once.\n *\n * Mirrors the existing `reason: 'auth'` postMessage in {@link maybeAttach}.\n * SECRET-HANDLING: the payload carries ONLY the reason enum — never the code,\n * secret, host, or relay URL.\n */\nfunction notifyAuthExpired(): void {\n if (authExpiredNotified) return;\n if (typeof window === 'undefined' || window.parent === window) return;\n authExpiredNotified = true;\n window.parent.postMessage({ type: 'ait:debug-attach-blocked', reason: 'auth-expired' }, '*');\n}\n\n/**\n * Normalises a URL into a comparable origin key, mapping the HTTP scheme pair\n * onto the WS pair (`https:`→`wss:`, `http:`→`ws:`) so the `wss:` relay URL\n * from the gate result matches the dials target.js derives from its\n * `https://…/target.js` script src. Returns `null` for unparsable URLs.\n */\nfunction wsOriginKey(rawUrl: string): string | null {\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n return null;\n }\n const protocol =\n parsed.protocol === 'https:' ? 'wss:' : parsed.protocol === 'http:' ? 'ws:' : parsed.protocol;\n return `${protocol}//${parsed.host}`;\n}\n\n/**\n * Builds a dummy WebSocket that never connects and closes immediately\n * (asynchronously, with the 4401 code) — returned for relay-bound dials after\n * auth expiry so chii's internal reconnect loop stops producing real network\n * traffic. We cannot stop the loop itself (it lives inside stock target.js);\n * we can only make each iteration free.\n *\n * Both `onclose`-style property handlers and `addEventListener` listeners are\n * fired — stock target.js uses property handlers, but we cannot know every\n * consumer. (A consumer wiring BOTH would see a double callback; acceptable\n * for a retry scheduler and irrelevant for chii.)\n */\nfunction createFailFastSocket(url: string): WebSocket {\n const eventTarget = new EventTarget();\n const sock = {\n url,\n readyState: 3, // CLOSED\n bufferedAmount: 0,\n extensions: '',\n protocol: '',\n binaryType: 'blob' as BinaryType,\n onopen: null as ((ev: Event) => unknown) | null,\n onmessage: null as ((ev: Event) => unknown) | null,\n onerror: null as ((ev: Event) => unknown) | null,\n onclose: null as ((ev: Event) => unknown) | null,\n close(): void {},\n send(): void {},\n addEventListener: eventTarget.addEventListener.bind(eventTarget),\n removeEventListener: eventTarget.removeEventListener.bind(eventTarget),\n dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),\n CONNECTING: 0,\n OPEN: 1,\n CLOSING: 2,\n CLOSED: 3,\n };\n setTimeout(() => {\n const errorEvent = new Event('error');\n sock.onerror?.(errorEvent);\n eventTarget.dispatchEvent(errorEvent);\n // CloseEvent exists in every real WebView; the Object.assign fallback\n // keeps the dummy environment-proof (consumers only read `.code`).\n let closeEvent: Event;\n try {\n closeEvent = new CloseEvent('close', {\n code: RELAY_AUTH_REJECT_CLOSE_CODE,\n reason: RELAY_AUTH_REJECT_REASON,\n wasClean: false,\n });\n } catch {\n closeEvent = Object.assign(new Event('close'), {\n code: RELAY_AUTH_REJECT_CLOSE_CODE,\n reason: RELAY_AUTH_REJECT_REASON,\n wasClean: false,\n });\n }\n sock.onclose?.(closeEvent);\n eventTarget.dispatchEvent(closeEvent);\n }, 0);\n return sock as unknown as WebSocket;\n}\n\n/**\n * Wraps `window.WebSocket` with a relay-origin-scoped observer (issue #478).\n *\n * - Connections whose URL origin does NOT match the relay origin pass through\n * to the native constructor untouched — app traffic is never observed.\n * - Relay-origin connections get a `close` listener: code 4401 (the relay's\n * named TOTP rejection) flips the module into the expired state and posts\n * `reason: 'auth-expired'` to the parent launcher shell (once).\n * - After 4401, further relay-origin dials return a fail-fast dummy socket so\n * target.js's autonomous reconnect loop stops hitting the network.\n *\n * Installed by {@link maybeAttach} BEFORE target.js is injected so the very\n * first dial is already observed. Idempotent per page lifecycle. Exported for\n * unit tests.\n */\nexport function installRelayWsObserver(relayUrl: string): void {\n if (wsObserverInstalled) return;\n if (typeof window === 'undefined' || typeof window.WebSocket !== 'function') return;\n const relayKey = wsOriginKey(relayUrl);\n if (relayKey === null) return;\n wsObserverInstalled = true;\n\n const NativeWebSocket = window.WebSocket;\n const observed = new Proxy(NativeWebSocket, {\n construct(target, args: unknown[]): object {\n const url = String(args[0]);\n if (wsOriginKey(url) !== relayKey) {\n // Not relay traffic — construct natively, no observation.\n return Reflect.construct(target, args);\n }\n if (relayAuthExpired) {\n // Retry-storm cutoff: the relay already named this session expired.\n return createFailFastSocket(url);\n }\n const ws = Reflect.construct(target, args) as WebSocket;\n ws.addEventListener('close', (event) => {\n if ((event as CloseEvent).code === RELAY_AUTH_REJECT_CLOSE_CODE) {\n relayAuthExpired = true;\n notifyAuthExpired();\n }\n });\n return ws;\n },\n });\n window.WebSocket = observed as typeof WebSocket;\n}\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 // Defect 2: a wrong/expired TOTP code is the ONLY block reason that is a\n // user-actionable failure inside a deliberate debug session — the operator\n // scanned a QR expecting an attach. Surface it to the parent launcher shell\n // so it can show a \"rescan the QR\" banner. Every other reason\n // ('host'/'entry'/'opt-in'/'invalid-relay') fires on ordinary non-debug page\n // loads and must stay silent to avoid a banner on every plain pageview.\n // SECRET-HANDLING: the message carries ONLY the 'auth' reason enum — never\n // the code, secret, host, or relay URL.\n if (gateResult.reason === 'auth' && typeof window !== 'undefined' && window.parent !== window) {\n window.parent.postMessage({ type: 'ait:debug-attach-blocked', reason: 'auth' }, '*');\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 // TOTP path-prefix transport (issue #466): forward the page URL's `at` code\n // (delivered by the dashboard QR → launcher deep-link) into the target\n // script URL so the WS upgrade derived from it passes the relay's TOTP\n // gate. Absent `at` → legacy un-prefixed URL (relay without TOTP, tests).\n // Read window.location.search directly, consistent with other guards in\n // this file. SECRET-HANDLING: the code is never logged; it rides only in\n // the script src (the intended transport).\n //\n // TTL note: the code is verified within the relay's ±1-step window (90 s),\n // so the initial attach always fits. A much-later automatic reconnect by\n // target.js reuses the stale prefix and is rejected (401) — by design under\n // the URL-leak threat model; recover by rescanning the QR (the relay-side\n // auth-reject counter from issue #467 makes this visible).\n const atCode =\n typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('at') : null;\n\n const src = deriveTargetScriptUrl(gateResult.relayUrl, atCode);\n\n // Issue #478: observe relay-bound WebSockets BEFORE target.js is injected so\n // even its very first dial — and every autonomous reconnect after a session\n // drop — is covered. The relay names a TOTP rejection with close code 4401;\n // the observer relays it to the launcher banner and cuts the retry storm.\n installRelayWsObserver(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 // Issue #478: a first-load stale code (QR scanned after expiry) fails the\n // target.js GET itself — no WebSocket is ever dialled, so the observer\n // above can't see it. Probe the same URL once with fetch(): the relay's\n // 401 now carries CORS headers, so the status is readable cross-origin.\n // 401 → surface auth-expired; anything else (tunnel down, transient\n // network) stays silent — same behaviour as before #478.\n script.onerror = () => {\n void fetch(src)\n .then((res) => {\n if (res.status === 401) notifyAuthExpired();\n })\n .catch(() => {\n // Network-level failure — not an auth signal; stay silent.\n });\n };\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/auto — self-gating side-effect entry.\n *\n * Consumers add a single line to their mini-app entry:\n *\n * import '@ait-co/devtools/in-app/auto';\n *\n * The entry self-gates: if none of the debug activation signals are present\n * (no `?debug=1`, no `?relay=`, and not a DEV build), it does nothing. The\n * imported chunk stays dormant and `window.__sdk` / `window.__sdkCall` are\n * never installed on a normal production load.\n *\n * When the gate passes it:\n * 1. Calls `maybeAttach()` — runs the full Layer B/C gate (host allowlist,\n * opt-in params, relay URL, TOTP) and injects the Chii `target.js` script.\n * Gate semantics are NOT changed — this is a thin self-gate wrapper.\n * 2. Installs the SDK bridge (`window.__sdk` / `window.__sdkCall`) so an AI\n * agent can drive any SDK API over the CDP relay without hand-synthesising\n * the Granite/ReactNative bridge envelope. SDK access uses a dynamic\n * import of `@apps-in-toss/web-framework` — the peer is optional, so if\n * the SDK is not installed the bridge install is silently skipped\n * (fail-silent). The namespace mirror pattern (iterate `Object.keys`) is\n * SDK version-neutral: 2.x and 3.x are both covered without any static\n * import that would couple the entry to a specific SDK line.\n *\n * SECRET-HANDLING: no secret, TOTP code, relay URL, or host value is ever\n * logged or surfaced beyond the reason enum in `maybeAttach()`.\n *\n * Layer A (build-time DCE) is NOT enforced here — this entry IS the\n * consumer-facing alternative to `if (__DEBUG_BUILD__) { … }`. The self-gate\n * below performs the same dormancy guarantee via a URL param check, which is\n * safe in a side-effect import context (the gate runs at module evaluation\n * time, before any React tree mounts). Consumers who already manage their own\n * `__DEBUG_BUILD__` guard can keep using `@ait-co/devtools/in-app` directly.\n *\n * DEV detection: `import.meta.env.DEV` is resolved by the consumer's bundler\n * at their build time (Vite/Webpack/Rspack inject the value), not at this\n * package's publish time — same pattern used by the polyfill's `auto` entry.\n * When the consumer is NOT running a bundler that injects `import.meta.env`\n * (e.g. bare Node or a test runner that leaves the raw identifiers in place),\n * the `typeof` guard makes it safe: a missing `import.meta.env.DEV` resolves\n * to `undefined`, which is falsy — the DEV path is simply skipped.\n */\n\nimport { maybeAttach } from './attach.js';\n\n// ---------------------------------------------------------------------------\n// Global type augmentation\n//\n// Consumers who import '@ait-co/devtools/in-app/auto' get these Window types\n// automatically — no separate globals.d.ts needed in their project.\n// ---------------------------------------------------------------------------\ndeclare global {\n interface Window {\n /**\n * Entire `@apps-in-toss/web-framework` export namespace mirrored onto a\n * plain writable object. Installed by the auto entry when `?debug=1` /\n * `?relay=` is present in the URL, or in DEV builds.\n *\n * Lets an AI agent call any SDK API over a CDP relay without\n * hand-synthesising the Granite/ReactNative bridge envelope:\n * `window.__sdk.setDeviceOrientation({ type: 'landscape' })`\n */\n __sdk?: Record<string, unknown>;\n\n /**\n * Safe call wrapper for `window.__sdk`. Returns a JSON-serialisable\n * `{ ok: true, value }` or `{ ok: false, error }` tuple even for\n * throwing/async SDK functions — ideal for `Runtime.evaluate` results.\n *\n * @example\n * window.__sdkCall('setDeviceOrientation', { type: 'landscape' })\n */\n __sdkCall?: (\n name: string,\n ...args: unknown[]\n ) => Promise<{ ok: boolean; value?: unknown; error?: string }>;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Self-gate\n//\n// Mirrors the gate in sdk-example/src/main.tsx:\n// - import.meta.env.DEV → env 1 (plain `pnpm dev`)\n// - ?debug=1 → on-device debug deep-link (env 3/4)\n// - ?relay= → on-device relay (env 2/3/4)\n//\n// A normal production load matches none of these and the module exits here,\n// keeping the SDK bridge chunk dormant.\n// ---------------------------------------------------------------------------\n\n/**\n * Pure predicate for the self-gate. Exported for unit tests.\n *\n * @param isDev - Whether the consumer's bundler folded `import.meta.env.DEV`\n * to `true`. Default: reads from `import.meta.env.DEV` at call time, which\n * is what the consumer's bundler replaces with a literal at build time.\n * Pass an explicit value in tests to control the DEV signal without\n * depending on the Vite/vitest build environment.\n * @param searchStr - URL search string to inspect. Defaults to\n * `window.location.search` when called in a browser context.\n */\nexport function shouldActivate(\n isDev: boolean = ((): boolean => {\n const metaEnv = (import.meta as unknown as Record<string, unknown>)?.env as\n | Record<string, unknown>\n | undefined;\n return metaEnv?.DEV === true;\n })(),\n searchStr: string = typeof window !== 'undefined' ? window.location.search : '',\n): boolean {\n if (isDev) return true;\n const params = new URLSearchParams(searchStr);\n return params.get('debug') === '1' || params.has('relay');\n}\n\nif (!shouldActivate()) {\n // Dormant — no-op. Normal production load exits here.\n} else {\n // ---------------------------------------------------------------------------\n // Step 1: attach (runs the full Layer B/C gate — zero semantics change).\n // ---------------------------------------------------------------------------\n maybeAttach();\n\n // ---------------------------------------------------------------------------\n // Step 2: SDK bridge — install window.__sdk / window.__sdkCall.\n //\n // Dynamic import keeps the SDK out of the top-level module graph so the\n // bridge chunk stays dormant when not needed. The namespace mirror pattern\n // (iterate Object.keys) works identically for SDK 2.x and 3.x without any\n // version-specific code path (version-agnostic, umbrella §5.1).\n //\n // `@apps-in-toss/web-framework` is an optional peer. If it is absent (e.g.\n // MCP-only consumers, test environments without the SDK), the dynamic import\n // rejects and we catch + swallow silently.\n //\n // SECRET-HANDLING: no host, relay URL, or auth code is logged here.\n // ---------------------------------------------------------------------------\n void import('@apps-in-toss/web-framework')\n .then((sdk) => {\n if (typeof window === 'undefined') return;\n\n // Enumerate all exports onto a plain writable object. A namespace import\n // is frozen/read-only, so callers need a plain enumerable surface.\n const bridge: Record<string, unknown> = {};\n for (const key of Object.keys(sdk)) {\n bridge[key] = (sdk as Record<string, unknown>)[key];\n }\n window.__sdk = bridge;\n\n // Convenience call helper: window.__sdkCall('apiName', arg1, arg2)\n // returns { ok: true, value } or { ok: false, error } — safe for any\n // CDP Runtime.evaluate result consumer.\n window.__sdkCall = async (name: string, ...args: unknown[]) => {\n const fn = bridge[name];\n if (typeof fn !== 'function') {\n return { ok: false, error: `__sdk.${name} is not a function` };\n }\n try {\n const value = await (fn as (...a: unknown[]) => unknown)(...args);\n return { ok: true, value };\n } catch (e) {\n return { ok: false, error: e instanceof Error ? e.message : String(e) };\n }\n };\n })\n .catch(() => {\n // Optional peer absent or failed to resolve — fail silently.\n // Do not log: a missing SDK on MCP-only consumers or test environments\n // is expected and should not produce console noise.\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"auto.js","names":[],"sources":["../../src/shared/relay-auth-close.ts","../../src/in-app/gate.ts","../../src/in-app/index.ts","../../src/in-app/attach.ts","../../src/in-app/auto.ts"],"sourcesContent":["/**\n * Shared constants for the relay's named TOTP-auth rejection (issue #478).\n *\n * Before #478 the relay rejected an unauthenticated WebSocket upgrade with a\n * raw `HTTP/1.1 401` + `socket.destroy()`. A handshake aborted that way is\n * indistinguishable from a network failure on the browser side — the\n * WebSocket only ever sees close code 1006, so the phone (env-2 launcher PWA)\n * could not tell \"stale TOTP code\" apart from \"tunnel down\" and stayed\n * silent. The fix is accept-then-close: complete the handshake, then close\n * with an application close code that NAMES the rejection.\n *\n * Three parties share this contract:\n * - `src/mcp/chii-relay.ts` (Node) sends the close frame / HTTP error body;\n * - `src/in-app/attach.ts` (browser) observes relay-bound WebSockets and\n * surfaces the code to the launcher shell;\n * - `src/mcp/chii-connection.ts` (Node daemon client) recognises the code\n * as an auth failure on its own `/client` dial (defensive — #439's fresh\n * code mint means it should not normally hit this).\n *\n * This module is intentionally dependency-free (no Node, no DOM) so it is\n * safe to import from both the browser in-app bundle and the MCP daemon\n * bundle.\n *\n * SECRET-HANDLING: these are fixed enum values. The close reason / error body\n * must never grow to carry a secret, a TOTP code, or a host.\n */\n\n/**\n * WebSocket close code sent by the relay when TOTP auth is rejected.\n *\n * 4000–4999 is the application-reserved range (RFC 6455 §7.4.2); 4401 mirrors\n * HTTP 401 so it reads as \"unauthorized\" at a glance.\n */\nexport const RELAY_AUTH_REJECT_CLOSE_CODE = 4401;\n\n/**\n * Close reason string accompanying {@link RELAY_AUTH_REJECT_CLOSE_CODE}, and\n * the `error` value of the relay's HTTP 401 JSON body. Enum string only —\n * never interpolated with request data.\n */\nexport const RELAY_AUTH_REJECT_REASON = 'totp-rejected';\n","/**\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:\n * B1 — host allowlist: `hostname` must be a `*.private-apps.tossmini.com`\n * subdomain (Toss dogfood entry) OR a `*.trycloudflare.com` host (env 2\n * PWA dev tunnel). The Toss app serves dogfood / private mini-apps from\n * a 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 * The env 2 tunnel host is allowed because it has no production runtime\n * (mock SDK, the developer's own dev server) — see {@link\n * isTrycloudflareHost}.\n * B2 — entry query: `_deploymentId` must be present and non-empty. Applies to\n * the Toss path only; the env 2 tunnel has no deployed bundle, so B2 is\n * skipped for `*.trycloudflare.com` hosts.\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 | _deploymentId | debug=1 | relay ok | TOTP ok* | result\n * neither | (any) | (any) | (any) | (any) | BLOCKED (host)\n * private-apps| absent | (any) | (any) | (any) | BLOCKED (entry)\n * private-apps| present | absent | (any) | (any) | BLOCKED (opt-in)\n * private-apps| present | present | invalid | (any) | BLOCKED (invalid-relay)\n * private-apps| present | present | valid | fail* | BLOCKED (auth)\n * private-apps| present | present | valid | pass/n/a | ATTACH\n * trycloudflare| (skipped) | absent | (any) | (any) | BLOCKED (opt-in)\n * trycloudflare| (skipped) | present | invalid | (any) | BLOCKED (invalid-relay)\n * trycloudflare| (skipped) | present | valid | fail* | BLOCKED (auth)\n * trycloudflare| (skipped) | 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 * For trycloudflare (env 2 tunnel) hosts B1 is bypassed and B2 is skipped;\n * C1/C2/C3 still apply identically. The ATTACH result carries\n * `deploymentId: ''` for tunnel hosts.\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 * The host suffix Cloudflare quick-tunnels serve from — the env 2 (PWA) entry.\n * See {@link isTrycloudflareHost} for why this host kind bypasses Layer B1.\n */\nconst TRYCLOUDFLARE_HOST_SUFFIX = '.trycloudflare.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 * The host suffix Cloudflare quick-tunnels use — the env 2 (PWA) entry.\n *\n * Env 2 serves the local Vite dev server through a `*.trycloudflare.com` quick\n * tunnel (`src/unplugin/tunnel.ts`). It has no Toss app, no `intoss-private://`\n * scheme, and — critically — no production runtime: the SDK is the devtools\n * mock, and the page is the developer's own dev build. The Layer B1 safety net\n * (which stops a dogfood build that lands on a Toss *production* host from\n * attaching) has nothing to protect against here, because env 2 has no\n * production host. So a trycloudflare host is allowed past B1 — but ONLY past\n * B1: the remaining layers (C1 opt-in, C2 relay, C3 TOTP) still apply, so a\n * leaked tunnel URL is still blocked by TOTP exactly as on the Toss path.\n *\n * The match is the same exact-suffix `endsWith` check as\n * {@link isPrivateAppsHost} — never a substring `.includes()`, which would\n * accept an attacker-controlled `evil.trycloudflare.com.example.com`. The\n * leading `.` forces a real subdomain label, so a bare `trycloudflare.com`\n * (no tunnel subdomain) does not match.\n */\nexport function isTrycloudflareHost(hostname: string): boolean {\n return hostname.endsWith(TRYCLOUDFLARE_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 // Two host kinds are allowed past B1:\n // - Toss dogfood: `*.private-apps.tossmini.com`. A production `intoss://`\n // entry is served from `*.apps.tossmini.com` and is rejected here. This\n // is what stops a dogfood build that somehow reaches a production entry\n // from attaching: Layer A keeps debug code out of release bundles, and\n // this layer keeps a dogfood bundle that lands on a production host from\n // attaching even though its code is present.\n // - Env 2 PWA tunnel: `*.trycloudflare.com`. This is the developer's own\n // local dev server (mock SDK, no production runtime), so the\n // production-entry hazard B1 guards against cannot occur. It bypasses B1\n // but NOT the remaining layers — C1/C2/C3 (incl. TOTP) still apply, so a\n // leaked tunnel URL is blocked exactly as on the Toss path. See\n // {@link isTrycloudflareHost}.\n const isTunnel = isTrycloudflareHost(input.hostname);\n if (!isPrivateAppsHost(input.hostname) && !isTunnel) {\n return { attach: false, reason: 'host' };\n }\n\n // Layer B2 — runtime entry query gate (Toss path only).\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. The env 2 tunnel has no deployed bundle and therefore no\n // `_deploymentId` — B2 is skipped for it, and `deploymentId` is reported as\n // the empty string on a tunnel attach (no consumer reads it; see attach.ts).\n let deploymentId = '';\n if (!isTunnel) {\n deploymentId = input.searchParams.get('_deploymentId') ?? '';\n if (deploymentId === '') {\n return { attach: false, reason: 'entry' };\n }\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 * @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, isTrycloudflareHost } 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","/**\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 {\n RELAY_AUTH_REJECT_CLOSE_CODE,\n RELAY_AUTH_REJECT_REASON,\n} from '../shared/relay-auth-close.js';\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` (or `/at/<code>/target.js` when a TOTP code\n * is given) regardless of the relay path. Query params and hash from the\n * relay URL are dropped — the target script URL is a static asset path on the\n * same host.\n *\n * TOTP path-prefix transport (issue #466): chii's stock `target.js` derives\n * its WS endpoint from the script `src` (`scriptEl.src.replace('target.js',\n * '')`), so embedding the current TOTP code in the script URL *path* is the\n * only way the phone-side WS upgrade can carry it — both the script fetch and\n * the derived `wss://<host>/at/<code>/target/<id>` dial inherit the prefix,\n * and the relay verifies + strips it before chii parses the URL. The\n * `window.ChiiServerUrl` + query alternative does NOT work: chii appends\n * `target/<id>` to the serverUrl string, which would land after a `?`.\n *\n * SECRET-HANDLING: `atCode` rides only inside the returned URL (the intended\n * transport — same exposure grade as the daemon client's `at=` query). It is\n * never logged here.\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/', '123456')\n * // → 'https://h.example.com:9100/at/123456/target.js'\n *\n * @param relayUrl - Validated `wss:` relay URL from the gate result.\n * @param atCode - Current TOTP code from the page URL's `at` query param, or\n * `null`/`undefined`/`''` to keep the legacy un-prefixed URL.\n */\nexport function deriveTargetScriptUrl(relayUrl: string, atCode?: string | null): string {\n const u = new URL(relayUrl);\n u.protocol = 'https:';\n u.pathname =\n atCode !== undefined && atCode !== null && atCode !== ''\n ? `/at/${encodeURIComponent(atCode)}/target.js`\n : '/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// Relay-origin WebSocket observer (issue #478)\n//\n// After a successful attach, chii's target.js owns its own reconnect loop —\n// `maybeAttach()` never re-runs, so a much-later reconnect carrying the stale\n// `/at/<code>/` prefix is rejected by the relay with NO in-page signal. The\n// relay now names that rejection (accept-then-close, code 4401), and this\n// observer is the in-page half: it watches relay-bound WebSockets for the\n// 4401 close and tells the parent launcher shell once, then fail-fasts any\n// further relay dials so the retry loop stops generating network traffic.\n// ---------------------------------------------------------------------------\n\n/** One-shot guard for the parent notification (both observer + onerror probe). */\nlet authExpiredNotified = false;\n\n/** Set once a relay-bound socket closed with 4401 — flips dials to fail-fast. */\nlet relayAuthExpired = false;\n\n/** Guard against stacking multiple observer wrappers on window.WebSocket. */\nlet wsObserverInstalled = false;\n\n/**\n * Posts the `auth-expired` block signal to the parent launcher shell, once.\n *\n * Mirrors the existing `reason: 'auth'` postMessage in {@link maybeAttach}.\n * SECRET-HANDLING: the payload carries ONLY the reason enum — never the code,\n * secret, host, or relay URL.\n */\nfunction notifyAuthExpired(): void {\n if (authExpiredNotified) return;\n if (typeof window === 'undefined' || window.parent === window) return;\n authExpiredNotified = true;\n window.parent.postMessage({ type: 'ait:debug-attach-blocked', reason: 'auth-expired' }, '*');\n}\n\n/**\n * Normalises a URL into a comparable origin key, mapping the HTTP scheme pair\n * onto the WS pair (`https:`→`wss:`, `http:`→`ws:`) so the `wss:` relay URL\n * from the gate result matches the dials target.js derives from its\n * `https://…/target.js` script src. Returns `null` for unparsable URLs.\n */\nfunction wsOriginKey(rawUrl: string): string | null {\n let parsed: URL;\n try {\n parsed = new URL(rawUrl);\n } catch {\n return null;\n }\n const protocol =\n parsed.protocol === 'https:' ? 'wss:' : parsed.protocol === 'http:' ? 'ws:' : parsed.protocol;\n return `${protocol}//${parsed.host}`;\n}\n\n/**\n * Builds a dummy WebSocket that never connects and closes immediately\n * (asynchronously, with the 4401 code) — returned for relay-bound dials after\n * auth expiry so chii's internal reconnect loop stops producing real network\n * traffic. We cannot stop the loop itself (it lives inside stock target.js);\n * we can only make each iteration free.\n *\n * Both `onclose`-style property handlers and `addEventListener` listeners are\n * fired — stock target.js uses property handlers, but we cannot know every\n * consumer. (A consumer wiring BOTH would see a double callback; acceptable\n * for a retry scheduler and irrelevant for chii.)\n */\nfunction createFailFastSocket(url: string): WebSocket {\n const eventTarget = new EventTarget();\n const sock = {\n url,\n readyState: 3, // CLOSED\n bufferedAmount: 0,\n extensions: '',\n protocol: '',\n binaryType: 'blob' as BinaryType,\n onopen: null as ((ev: Event) => unknown) | null,\n onmessage: null as ((ev: Event) => unknown) | null,\n onerror: null as ((ev: Event) => unknown) | null,\n onclose: null as ((ev: Event) => unknown) | null,\n close(): void {},\n send(): void {},\n addEventListener: eventTarget.addEventListener.bind(eventTarget),\n removeEventListener: eventTarget.removeEventListener.bind(eventTarget),\n dispatchEvent: eventTarget.dispatchEvent.bind(eventTarget),\n CONNECTING: 0,\n OPEN: 1,\n CLOSING: 2,\n CLOSED: 3,\n };\n setTimeout(() => {\n const errorEvent = new Event('error');\n sock.onerror?.(errorEvent);\n eventTarget.dispatchEvent(errorEvent);\n // CloseEvent exists in every real WebView; the Object.assign fallback\n // keeps the dummy environment-proof (consumers only read `.code`).\n let closeEvent: Event;\n try {\n closeEvent = new CloseEvent('close', {\n code: RELAY_AUTH_REJECT_CLOSE_CODE,\n reason: RELAY_AUTH_REJECT_REASON,\n wasClean: false,\n });\n } catch {\n closeEvent = Object.assign(new Event('close'), {\n code: RELAY_AUTH_REJECT_CLOSE_CODE,\n reason: RELAY_AUTH_REJECT_REASON,\n wasClean: false,\n });\n }\n sock.onclose?.(closeEvent);\n eventTarget.dispatchEvent(closeEvent);\n }, 0);\n return sock as unknown as WebSocket;\n}\n\n/**\n * Wraps `window.WebSocket` with a relay-origin-scoped observer (issue #478).\n *\n * - Connections whose URL origin does NOT match the relay origin pass through\n * to the native constructor untouched — app traffic is never observed.\n * - Relay-origin connections get a `close` listener: code 4401 (the relay's\n * named TOTP rejection) flips the module into the expired state and posts\n * `reason: 'auth-expired'` to the parent launcher shell (once).\n * - After 4401, further relay-origin dials return a fail-fast dummy socket so\n * target.js's autonomous reconnect loop stops hitting the network.\n *\n * Installed by {@link maybeAttach} BEFORE target.js is injected so the very\n * first dial is already observed. Idempotent per page lifecycle. Exported for\n * unit tests.\n */\nexport function installRelayWsObserver(relayUrl: string): void {\n if (wsObserverInstalled) return;\n if (typeof window === 'undefined' || typeof window.WebSocket !== 'function') return;\n const relayKey = wsOriginKey(relayUrl);\n if (relayKey === null) return;\n wsObserverInstalled = true;\n\n const NativeWebSocket = window.WebSocket;\n const observed = new Proxy(NativeWebSocket, {\n construct(target, args: unknown[]): object {\n const url = String(args[0]);\n if (wsOriginKey(url) !== relayKey) {\n // Not relay traffic — construct natively, no observation.\n return Reflect.construct(target, args);\n }\n if (relayAuthExpired) {\n // Retry-storm cutoff: the relay already named this session expired.\n return createFailFastSocket(url);\n }\n const ws = Reflect.construct(target, args) as WebSocket;\n ws.addEventListener('close', (event) => {\n if ((event as CloseEvent).code === RELAY_AUTH_REJECT_CLOSE_CODE) {\n relayAuthExpired = true;\n notifyAuthExpired();\n }\n });\n return ws;\n },\n });\n window.WebSocket = observed as typeof WebSocket;\n}\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 // Defect 2: a wrong/expired TOTP code is the ONLY block reason that is a\n // user-actionable failure inside a deliberate debug session — the operator\n // scanned a QR expecting an attach. Surface it to the parent launcher shell\n // so it can show a \"rescan the QR\" banner. Every other reason\n // ('host'/'entry'/'opt-in'/'invalid-relay') fires on ordinary non-debug page\n // loads and must stay silent to avoid a banner on every plain pageview.\n // SECRET-HANDLING: the message carries ONLY the 'auth' reason enum — never\n // the code, secret, host, or relay URL.\n if (gateResult.reason === 'auth' && typeof window !== 'undefined' && window.parent !== window) {\n window.parent.postMessage({ type: 'ait:debug-attach-blocked', reason: 'auth' }, '*');\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 // TOTP path-prefix transport (issue #466): forward the page URL's `at` code\n // (delivered by the dashboard QR → launcher deep-link) into the target\n // script URL so the WS upgrade derived from it passes the relay's TOTP\n // gate. Absent `at` → legacy un-prefixed URL (relay without TOTP, tests).\n // Read window.location.search directly, consistent with other guards in\n // this file. SECRET-HANDLING: the code is never logged; it rides only in\n // the script src (the intended transport).\n //\n // TTL note: the code is verified within the relay's ±1-step window (90 s),\n // so the initial attach always fits. A much-later automatic reconnect by\n // target.js reuses the stale prefix and is rejected (401) — by design under\n // the URL-leak threat model; recover by rescanning the QR (the relay-side\n // auth-reject counter from issue #467 makes this visible).\n const atCode =\n typeof window !== 'undefined' ? new URLSearchParams(window.location.search).get('at') : null;\n\n const src = deriveTargetScriptUrl(gateResult.relayUrl, atCode);\n\n // Issue #478: observe relay-bound WebSockets BEFORE target.js is injected so\n // even its very first dial — and every autonomous reconnect after a session\n // drop — is covered. The relay names a TOTP rejection with close code 4401;\n // the observer relays it to the launcher banner and cuts the retry storm.\n installRelayWsObserver(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 // Issue #478: a first-load stale code (QR scanned after expiry) fails the\n // target.js GET itself — no WebSocket is ever dialled, so the observer\n // above can't see it. Probe the same URL once with fetch(): the relay's\n // 401 now carries CORS headers, so the status is readable cross-origin.\n // 401 → surface auth-expired; anything else (tunnel down, transient\n // network) stays silent — same behaviour as before #478.\n script.onerror = () => {\n void fetch(src)\n .then((res) => {\n if (res.status === 401) notifyAuthExpired();\n })\n .catch(() => {\n // Network-level failure — not an auth signal; stay silent.\n });\n };\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/auto — self-gating side-effect entry.\n *\n * Consumers add a single line to their mini-app entry:\n *\n * import '@ait-co/devtools/in-app/auto';\n *\n * The entry self-gates: if none of the debug activation signals are present\n * (no `?debug=1`, no `?relay=`, and not a DEV build), it does nothing. The\n * imported chunk stays dormant and `window.__sdk` / `window.__sdkCall` are\n * never installed on a normal production load.\n *\n * When the gate passes it:\n * 1. Calls `maybeAttach()` — runs the full Layer B/C gate (host allowlist,\n * opt-in params, relay URL, TOTP) and injects the Chii `target.js` script.\n * Gate semantics are NOT changed — this is a thin self-gate wrapper.\n * 2. Installs the SDK bridge (`window.__sdk` / `window.__sdkCall`) so an AI\n * agent can drive any SDK API over the CDP relay without hand-synthesising\n * the Granite/ReactNative bridge envelope. SDK access uses a dynamic\n * import of `@apps-in-toss/web-framework` — the peer is optional, so if\n * the SDK is not installed the bridge install is silently skipped\n * (fail-silent). The namespace mirror pattern (iterate `Object.keys`) is\n * SDK version-neutral: 2.x and 3.x are both covered without any static\n * import that would couple the entry to a specific SDK line.\n *\n * SECRET-HANDLING: no secret, TOTP code, relay URL, or host value is ever\n * logged or surfaced beyond the reason enum in `maybeAttach()`.\n *\n * Layer A (build-time DCE) is NOT enforced here — this entry IS the\n * consumer-facing alternative to `if (__DEBUG_BUILD__) { … }`. The self-gate\n * below performs the same dormancy guarantee via a URL param check, which is\n * safe in a side-effect import context (the gate runs at module evaluation\n * time, before any React tree mounts). Consumers who already manage their own\n * `__DEBUG_BUILD__` guard can keep using `@ait-co/devtools/in-app` directly.\n *\n * DEV detection uses two complementary signals:\n * 1. `import.meta.env.DEV` — resolved by the consumer's bundler at their\n * build time (Vite/Webpack/Rspack inject the value via top-level source\n * transforms). Works when the consumer's source code (not node_modules)\n * is processed — same pattern used by the polyfill's `auto` entry.\n * 2. `process.env.NODE_ENV === 'development'` — resolved by the consumer's\n * bundler via esbuild `define` (Vite dep-prebundle) or DefinePlugin\n * (webpack/Rspack). This token IS substituted in dep code inside\n * node_modules (how React's own dev/prod branching works), fixing the\n * env-1 regression where signal (1) was never injected into dep code\n * (sdk-example#180 / issue #520).\n * IMPORTANT: the `process.env.NODE_ENV` token must be written verbatim\n * — bundler define substitution is a textual token match. A `typeof\n * process` guard would survive substitution as-is and always evaluate to\n * `false` in a browser, killing the comparison. Instead we rely on\n * try/catch: if `process` is not defined (raw ESM in a browser without\n * bundler substitution) a ReferenceError is caught → fail-closed (dormant).\n */\n\nimport { maybeAttach } from './attach.js';\n\n// ---------------------------------------------------------------------------\n// Global type augmentation\n//\n// Consumers who import '@ait-co/devtools/in-app/auto' get these Window types\n// automatically — no separate globals.d.ts needed in their project.\n// ---------------------------------------------------------------------------\ndeclare global {\n interface Window {\n /**\n * Entire `@apps-in-toss/web-framework` export namespace mirrored onto a\n * plain writable object. Installed by the auto entry when `?debug=1` /\n * `?relay=` is present in the URL, or in DEV builds.\n *\n * Lets an AI agent call any SDK API over a CDP relay without\n * hand-synthesising the Granite/ReactNative bridge envelope:\n * `window.__sdk.setDeviceOrientation({ type: 'landscape' })`\n */\n __sdk?: Record<string, unknown>;\n\n /**\n * Safe call wrapper for `window.__sdk`. Returns a JSON-serialisable\n * `{ ok: true, value }` or `{ ok: false, error }` tuple even for\n * throwing/async SDK functions — ideal for `Runtime.evaluate` results.\n *\n * @example\n * window.__sdkCall('setDeviceOrientation', { type: 'landscape' })\n */\n __sdkCall?: (\n name: string,\n ...args: unknown[]\n ) => Promise<{ ok: boolean; value?: unknown; error?: string }>;\n }\n}\n\n// ---------------------------------------------------------------------------\n// Self-gate\n//\n// Mirrors the gate in sdk-example/src/main.tsx:\n// - import.meta.env.DEV → env 1 (plain `pnpm dev`)\n// - ?debug=1 → on-device debug deep-link (env 3/4)\n// - ?relay= → on-device relay (env 2/3/4)\n//\n// A normal production load matches none of these and the module exits here,\n// keeping the SDK bridge chunk dormant.\n// ---------------------------------------------------------------------------\n\n/**\n * Detects whether the current build is a DEV build by consulting two signals.\n *\n * Signal A — `import.meta.env.DEV`:\n * Substituted by Vite/Webpack/Rspack in the consumer's own source files.\n * NOT substituted in node_modules dep code (esbuild prebundle does not\n * apply Vite's define pass to deps) — this was the root cause of #520.\n *\n * Signal B — `process.env.NODE_ENV === 'development'`:\n * Substituted by esbuild's dep-prebundle define pass (Vite) and by\n * DefinePlugin (webpack/Rspack) even inside node_modules. This is how\n * React itself gates its dev-only code paths. Writing the token verbatim\n * ensures textual substitution works; a `typeof process` guard would not\n * be substituted and would evaluate to `'undefined'` in the browser,\n * killing the comparison. A try/catch catches the ReferenceError when\n * `process` is genuinely absent (raw ESM without bundler, e.g. direct\n * browser import or test runners that leave identifiers in place) →\n * fail-closed (dormant).\n *\n * Exported for unit tests — pass an explicit `isDev` override to bypass\n * the environment detection in controlled test scenarios.\n */\nexport function detectDevSignal(): boolean {\n // Signal A: import.meta.env.DEV (consumer source / bundler top-level pass)\n try {\n const metaEnv = (import.meta as unknown as { env?: { DEV?: unknown } }).env;\n if (metaEnv?.DEV === true) return true;\n } catch {\n // Swallow — some environments throw on import.meta access.\n }\n // Signal B: process.env.NODE_ENV (dep-prebundle define / DefinePlugin)\n // Token written verbatim — bundler define substitution is a textual match.\n // A typeof guard must NOT be added: it would survive substitution unchanged\n // and evaluate to false in a browser, killing the comparison.\n try {\n if (process.env.NODE_ENV === 'development') return true;\n } catch {\n // ReferenceError: process is not defined — raw ESM without bundler\n // substitution → fail-closed (dormant). Do not surface the error.\n }\n return false;\n}\n\n/**\n * Pure predicate for the self-gate. Exported for unit tests.\n *\n * @param isDev - Whether the consumer's bundler signals a DEV build.\n * Default: calls `detectDevSignal()` which consults both\n * `import.meta.env.DEV` (consumer source pass) and\n * `process.env.NODE_ENV === 'development'` (dep prebundle pass, fixing\n * the env-1 regression in issue #520).\n * Pass an explicit value in tests to control the DEV signal without\n * depending on the Vite/vitest build environment.\n * @param searchStr - URL search string to inspect. Defaults to\n * `window.location.search` when called in a browser context.\n */\nexport function shouldActivate(\n isDev: boolean = detectDevSignal(),\n searchStr: string = typeof window !== 'undefined' ? window.location.search : '',\n): boolean {\n if (isDev) return true;\n const params = new URLSearchParams(searchStr);\n return params.get('debug') === '1' || params.has('relay');\n}\n\nif (!shouldActivate()) {\n // Dormant — no-op. Normal production load exits here.\n} else {\n // ---------------------------------------------------------------------------\n // Step 1: attach (runs the full Layer B/C gate — zero semantics change).\n // ---------------------------------------------------------------------------\n maybeAttach();\n\n // ---------------------------------------------------------------------------\n // Step 2: SDK bridge — install window.__sdk / window.__sdkCall.\n //\n // Dynamic import keeps the SDK out of the top-level module graph so the\n // bridge chunk stays dormant when not needed. The namespace mirror pattern\n // (iterate Object.keys) works identically for SDK 2.x and 3.x without any\n // version-specific code path (version-agnostic, umbrella §5.1).\n //\n // `@apps-in-toss/web-framework` is an optional peer. If it is absent (e.g.\n // MCP-only consumers, test environments without the SDK), the dynamic import\n // rejects and we catch + swallow silently.\n //\n // SECRET-HANDLING: no host, relay URL, or auth code is logged here.\n // ---------------------------------------------------------------------------\n void import('@apps-in-toss/web-framework')\n .then((sdk) => {\n if (typeof window === 'undefined') return;\n\n // Enumerate all exports onto a plain writable object. A namespace import\n // is frozen/read-only, so callers need a plain enumerable surface.\n const bridge: Record<string, unknown> = {};\n for (const key of Object.keys(sdk)) {\n bridge[key] = (sdk as Record<string, unknown>)[key];\n }\n window.__sdk = bridge;\n\n // Convenience call helper: window.__sdkCall('apiName', arg1, arg2)\n // returns { ok: true, value } or { ok: false, error } — safe for any\n // CDP Runtime.evaluate result consumer.\n window.__sdkCall = async (name: string, ...args: unknown[]) => {\n const fn = bridge[name];\n if (typeof fn !== 'function') {\n return { ok: false, error: `__sdk.${name} is not a function` };\n }\n try {\n const value = await (fn as (...a: unknown[]) => unknown)(...args);\n return { ok: true, value };\n } catch (e) {\n return { ok: false, error: e instanceof Error ? e.message : String(e) };\n }\n };\n })\n .catch(() => {\n // Optional peer absent or failed to resolve — fail silently.\n // Do not log: a missing SDK on MCP-only consumers or test environments\n // is expected and should not produce console noise.\n });\n}\n"],"mappings":"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"}
|
package/dist/mcp/cli.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-
|
|
3
|
-
import { t as loadRelaySecretReadOnly } from "../relay-secret-store-
|
|
2
|
+
import { i as generateTotp, n as assertRelayAuthConfigured, r as buildRelayVerifyAuth } from "../totp-Xq3ACwkm.js";
|
|
3
|
+
import { t as loadRelaySecretReadOnly } from "../relay-secret-store-J0SUUXjH.js";
|
|
4
4
|
import { createRequire } from "node:module";
|
|
5
5
|
import { existsSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
6
6
|
import { argv } from "node:process";
|
|
@@ -1051,10 +1051,10 @@ function buildLauncherAttachUrl(tunnelUrl, wssUrl, totpCode, opts) {
|
|
|
1051
1051
|
return url;
|
|
1052
1052
|
}
|
|
1053
1053
|
/**
|
|
1054
|
-
* Build a self-attaching
|
|
1054
|
+
* Build a self-attaching dog-food deep-link.
|
|
1055
1055
|
*
|
|
1056
1056
|
* `ait deploy --scheme-only` prints an `intoss-private://…?_deploymentId=<uuid>`
|
|
1057
|
-
* URL that opens a
|
|
1057
|
+
* URL that opens a dog-food bundle on a phone. The in-app debug gate
|
|
1058
1058
|
* (`src/in-app/gate.ts`) auto-attaches when the entry URL also carries
|
|
1059
1059
|
* `debug=1` and `relay=<wss-url>`. This helper splices those params (plus
|
|
1060
1060
|
* `at=<code>` when TOTP is enabled) into the scheme URL; rendering the result
|
|
@@ -1440,7 +1440,7 @@ function toLegacyEnv(env) {
|
|
|
1440
1440
|
*
|
|
1441
1441
|
* `relayOrigin` is the booted-family discriminator (NOT sniffed from the URL)
|
|
1442
1442
|
* that distinguishes the env-2 external-PWA relay (`relay-mobile`) from the
|
|
1443
|
-
* intoss-private
|
|
1443
|
+
* intoss-private dog-food relay (`relay-dev`); both are `kind: 'relay'`.
|
|
1444
1444
|
*
|
|
1445
1445
|
* Pure — used at every output boundary (envelope `meta.env`, `get_debug_status`,
|
|
1446
1446
|
* `measure_safe_area` provenance) so the surface never sniffs a URL again.
|
|
@@ -1521,7 +1521,7 @@ function tunnelDownError() {
|
|
|
1521
1521
|
* enableDomains()가 "No mini-app page attached" 에러를 던질 때.
|
|
1522
1522
|
*/
|
|
1523
1523
|
function pageMissingError(toolName) {
|
|
1524
|
-
return mcpError(`${toolName ? `${toolName}: ` : ""}페이지가 attach 안 됨.
|
|
1524
|
+
return mcpError(`${toolName ? `${toolName}: ` : ""}페이지가 attach 안 됨. dog-food 번들 배포 후 build_attach_url을 호출해 QR을 생성하세요: \`ait deploy --scheme-only\` → \`build_attach_url(scheme_url)\` → QR 스캔.`);
|
|
1525
1525
|
}
|
|
1526
1526
|
/**
|
|
1527
1527
|
* 상태 3: page crash — 연결됐던 페이지가 crash/destroy됐다.
|
|
@@ -1537,7 +1537,7 @@ function pageCrashError(toolName) {
|
|
|
1537
1537
|
*
|
|
1538
1538
|
* call_sdk 호출 시 브리지가 없을 때. 같은 "브리지 부재"라도 다음 행동은
|
|
1539
1539
|
* connection 종류에 따라 정반대다 (issue #360):
|
|
1540
|
-
* - relay(`--target` 없는 intoss / env-2):
|
|
1540
|
+
* - relay(`--target` 없는 intoss / env-2): dog-food 빌드가 아니다 → dog-food
|
|
1541
1541
|
* 채널로 재배포 후 QR 재스캔.
|
|
1542
1542
|
* - local(`--target=local`, env 1 로컬 브라우저): 재배포가 아니라 dev 서버를
|
|
1543
1543
|
* `pnpm dev`로 띄웠는지 + unplugin alias가 `@apps-in-toss/web-framework`를
|
|
@@ -1549,7 +1549,7 @@ function pageCrashError(toolName) {
|
|
|
1549
1549
|
function sdkAbsentError(toolName, isLocal = false) {
|
|
1550
1550
|
const prefix = toolName ? `${toolName}: ` : "";
|
|
1551
1551
|
if (isLocal) return mcpError(`${prefix}window.__sdkCall이 주입되지 않았습니다 (로컬 dev 브리지 부재). sdk-example을 \`pnpm dev\`로 띄웠는지, 그리고 unplugin alias가 \`@apps-in-toss/web-framework\`를 devtools mock으로 resolve하는지 확인하세요. dev 빌드(\`import.meta.env.DEV\`)면 \`window.__sdkCall\`이 자동 설치됩니다.`);
|
|
1552
|
-
return mcpError(`${prefix}window.__sdkCall이 주입되지 않았습니다 (
|
|
1552
|
+
return mcpError(`${prefix}window.__sdkCall이 주입되지 않았습니다 (dog-food 빌드가 아닙니다). dog-food 채널(intoss-private)로 재배포 후 QR을 다시 스캔하세요: \`ait build && aitcc app deploy\`.`);
|
|
1553
1553
|
}
|
|
1554
1554
|
/**
|
|
1555
1555
|
* relay-live 환경에서 side-effect 도구(`call_sdk`, `evaluate`)를 `confirm: true`
|
|
@@ -1564,7 +1564,7 @@ function liveGuardError(toolName) {
|
|
|
1564
1564
|
|
|
1565
1565
|
다음 중 하나를 선택하세요:
|
|
1566
1566
|
1. \`confirm: true\` 인자를 추가해 재호출: ${toolName}(…, confirm: true)\n 2. 읽기 전용 도구(list_pages, list_console_messages, take_screenshot 등)를 사용하세요.
|
|
1567
|
-
3.
|
|
1567
|
+
3. dog-food 빌드(relay-dev 환경)에서 먼저 검증 후 live에 적용하세요.
|
|
1568
1568
|
|
|
1569
1569
|
live-guard: MCP_ENV=relay-live + confirm: true missing`);
|
|
1570
1570
|
}
|
|
@@ -2117,15 +2117,15 @@ const en = {
|
|
|
2117
2117
|
"attach.steps.section": "How to scan",
|
|
2118
2118
|
"attach.faq.section": "Troubleshooting checklist",
|
|
2119
2119
|
"attach.url.section": "URL (fallback)",
|
|
2120
|
-
"attach.mode.sandbox": "
|
|
2121
|
-
"attach.mode.intossDev": "
|
|
2122
|
-
"attach.mode.intossLive": "
|
|
2120
|
+
"attach.mode.sandbox": "env 2 — AITC Sandbox App (PWA)",
|
|
2121
|
+
"attach.mode.intossDev": "env 3 — intoss-private relay dev",
|
|
2122
|
+
"attach.mode.intossLive": "env 4 — intoss live relay debug",
|
|
2123
2123
|
"attach.sandbox.step1": "Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).",
|
|
2124
2124
|
"attach.sandbox.step2": "Scan this QR code with <strong>\"Scan QR with camera\"</strong> inside the launcher.",
|
|
2125
2125
|
"attach.sandbox.step3": "The mini-app opens fullscreen and the debug session attaches automatically.",
|
|
2126
2126
|
"attach.sandbox.faq.notInstalled": "<strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen",
|
|
2127
2127
|
"attach.sandbox.faq.cameraApp": "<strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner",
|
|
2128
|
-
"attach.sandbox.faq.totp": "<strong>QR expired (TOTP ~3 min)</strong> — scan a fresh QR code",
|
|
2128
|
+
"attach.sandbox.faq.totp": "<strong>QR expired (TOTP — 30-second step, ±6 steps (~3 min) accepted)</strong> — scan a fresh QR code",
|
|
2129
2129
|
"attach.sandbox.faq.chii": "<strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import",
|
|
2130
2130
|
"attach.intoss.step1": "Open the Toss app.",
|
|
2131
2131
|
"attach.intoss.step2": "Scan the QR code with your phone camera app.",
|
|
@@ -2369,7 +2369,7 @@ const tables = {
|
|
|
2369
2369
|
"attach.steps.section": "스캔 절차",
|
|
2370
2370
|
"attach.faq.section": "진단 체크리스트",
|
|
2371
2371
|
"attach.url.section": "URL (fallback)",
|
|
2372
|
-
"attach.mode.sandbox": "환경 2 — AITC Sandbox PWA",
|
|
2372
|
+
"attach.mode.sandbox": "환경 2 — AITC Sandbox App (PWA)",
|
|
2373
2373
|
"attach.mode.intossDev": "환경 3 — intoss-private relay dev",
|
|
2374
2374
|
"attach.mode.intossLive": "환경 4 — intoss live relay debug",
|
|
2375
2375
|
"attach.sandbox.step1": "홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).",
|
|
@@ -2377,7 +2377,7 @@ const tables = {
|
|
|
2377
2377
|
"attach.sandbox.step3": "미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.",
|
|
2378
2378
|
"attach.sandbox.faq.notInstalled": "<strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요",
|
|
2379
2379
|
"attach.sandbox.faq.cameraApp": "<strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요",
|
|
2380
|
-
"attach.sandbox.faq.totp": "<strong>QR이 만료된 경우 (TOTP ~3분)</strong> — 새 QR을 다시 스캔하세요",
|
|
2380
|
+
"attach.sandbox.faq.totp": "<strong>QR이 만료된 경우 (TOTP — 코드 1개는 30초 창, 만료 후 ~3분(±6 step) 이내 소급 허용)</strong> — 새 QR을 다시 스캔하세요",
|
|
2381
2381
|
"attach.sandbox.faq.chii": "<strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인",
|
|
2382
2382
|
"attach.intoss.step1": "토스 앱을 실행하세요.",
|
|
2383
2383
|
"attach.intoss.step2": "폰 카메라 앱으로 QR 코드를 스캔하세요.",
|
|
@@ -2567,7 +2567,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
|
|
|
2567
2567
|
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2568
2568
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2569
2569
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2570
|
-
</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).</li><li>launcher 안의 <strong>"QR 카메라로 스캔"</strong>으로 이 QR 코드를 스캔하세요.</li><li>미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요</li><li><strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요</li><li><strong>QR이 만료된 경우 (TOTP ~3분)</strong> — 새 QR을 다시 스캔하세요</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="복사">복사</button></div></section></body></html>`;
|
|
2570
|
+
</style></head><body><h1>AIT 디버그 세션 — QR 스캔</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>스캔 절차</h2><ol><li>홈 화면의 launcher PWA 아이콘으로 실행하세요 (Safari 주소창이 보이면 standalone이 아닙니다).</li><li>launcher 안의 <strong>"QR 카메라로 스캔"</strong>으로 이 QR 코드를 스캔하세요.</li><li>미니앱이 풀스크린으로 열리고 디버그 세션이 자동으로 attach됩니다.</li></ol></section><hr/><section><h2>진단 체크리스트</h2><ul><li><strong>launcher가 설치돼 있지 않은 경우</strong> — <code>devtools.aitc.dev/launcher/</code>를 한 번 열어 홈 화면에 추가하세요</li><li><strong>카메라 앱으로 스캔하면 Safari 탭으로 열립니다 (하단 탭 바 노출)</strong> — launcher 아이콘으로 다시 실행해 인앱 스캔을 사용하세요</li><li><strong>QR이 만료된 경우 (TOTP — 코드 1개는 30초 창, 만료 후 ~3분(±6 step) 이내 소급 허용)</strong> — 새 QR을 다시 스캔하세요</li><li><strong>Chii 주입 실패 / 콘솔이 비어 있는 경우</strong> — 미니앱 번들에 <code>in-app</code> debug import가 있는지 확인</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="복사">복사</button></div></section></body></html>`;
|
|
2571
2571
|
const attachChromeHtmlKoIntoss = `<!DOCTYPE html>
|
|
2572
2572
|
<html lang="ko"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="image" href="__QR_DATA_URL__"/><title>AIT 디버그 세션 — QR 스캔</title><style>
|
|
2573
2573
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -2727,7 +2727,7 @@ hr { border: none; border-top: 1px solid #21262d; width: 100%; margin: 0.5rem 0;
|
|
|
2727
2727
|
.lang-switcher { display: flex; gap: 0.5rem; font-size: 0.75rem; }
|
|
2728
2728
|
.lang-switcher a { color: #58a6ff; text-decoration: none; opacity: 0.6; }
|
|
2729
2729
|
.lang-switcher a.active { font-weight: 700; text-decoration: underline; opacity: 1; }
|
|
2730
|
-
</style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).</li><li>Scan this QR code with <strong>"Scan QR with camera"</strong> inside the launcher.</li><li>The mini-app opens fullscreen and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen</li><li><strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner</li><li><strong>QR expired (TOTP ~3 min)</strong> — scan a fresh QR code</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="Copy">Copy</button></div></section></body></html>`;
|
|
2730
|
+
</style></head><body><h1>AIT Debug Session — QR Scan</h1>__MODE_LABEL____LANG_SWITCHER__<div id="attach-section"><img class="qr" src="__QR_DATA_URL__" alt="attach QR"/></div><section><h2>How to scan</h2><ol><li>Launch the launcher PWA icon on your home screen (if the Safari address bar is visible, it is not standalone).</li><li>Scan this QR code with <strong>"Scan QR with camera"</strong> inside the launcher.</li><li>The mini-app opens fullscreen and the debug session attaches automatically.</li></ol></section><hr/><section><h2>Troubleshooting checklist</h2><ul><li><strong>Launcher is not installed</strong> — open <code>devtools.aitc.dev/launcher/</code> once and add it to your home screen</li><li><strong>Scanning with the camera app opens a Safari tab (bottom tab bar visible)</strong> — relaunch from the launcher icon and use the in-app scanner</li><li><strong>QR expired (TOTP — 30-second step, ±6 steps (~3 min) accepted)</strong> — scan a fresh QR code</li><li><strong>Chii injection failure / console is empty</strong> — verify the mini-app bundle has an <code>in-app</code> debug import</li></ul></section><hr/><section id="url-section"><h2>URL (fallback)</h2><div class="url-row"><p class="url-box" id="url-box">__SAFE_ATTACH_URL__</p><button class="copy-btn" id="copy-btn" type="button" aria-label="Copy">Copy</button></div></section></body></html>`;
|
|
2731
2731
|
const attachChromeHtmlEnIntoss = `<!DOCTYPE html>
|
|
2732
2732
|
<html lang="en"><head><meta charSet="utf-8"/><meta name="viewport" content="width=device-width, initial-scale=1"/><link rel="preload" as="image" href="__QR_DATA_URL__"/><title>AIT Debug Session — QR Scan</title><style>
|
|
2733
2733
|
*, *::before, *::after { box-sizing: border-box; }
|
|
@@ -3646,7 +3646,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
3646
3646
|
},
|
|
3647
3647
|
{
|
|
3648
3648
|
name: "build_attach_url",
|
|
3649
|
-
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. Builds a self-attaching deep
|
|
3649
|
+
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. Builds a self-attaching deep-link for the active relay environment and returns a QR code. 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). Call list_pages first to confirm the relay/tunnel is up. If the tunnel is not up, restart: `npx @ait-co/devtools devtools-mcp`.\n\nEnvironment-specific behaviour:\n • env 3 / relay-staging (start_debug mode=\"relay-staging\"): requires scheme_url — the intoss-private://…?_deploymentId=<uuid> URL from `ait deploy --scheme-only`. Splices debug=1 + relay URL into the scheme URL to produce a self-attach deep-link.\n • env 2 / relay-sandbox (start_debug mode=\"relay-sandbox\"): scheme_url is NOT used. Instead, reads AIT_TUNNEL_BASE_URL (the https://*.trycloudflare.com app tunnel from `tunnel:{cdp:true}`) and builds a launcher PWA deep-link (https://devtools.aitc.dev/launcher/?url=…&debug=1&relay=…). When projectRoot is given, the app name from <projectRoot>/package.json is automatically added as name= so the launcher partner bar shows it. Scan the QR with the phone to open the launcher, which frames the tunnel URL and attaches CDP.\n\nSet wait_for_attach=true to block until a page attaches (polls up to 30 s). 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). \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 code is valid for ~3 minutes (the relay gate accepts ±6 TOTP steps = 180–210 s of backwards acceptance). The response includes a `totp` field with `expiresAt` (ISO timestamp, ~3 min from issuance). If the phone scan happens after expiresAt, the relay will reject the code — just call build_attach_url again to get a fresh URL. Without AIT_DEBUG_TOTP_SECRET, the attachUrl has no expiry.",
|
|
3650
3650
|
inputSchema: {
|
|
3651
3651
|
type: "object",
|
|
3652
3652
|
properties: {
|
|
@@ -3703,7 +3703,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
3703
3703
|
},
|
|
3704
3704
|
{
|
|
3705
3705
|
name: "measure_safe_area",
|
|
3706
|
-
description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel page with window.__ait state) and `relay` (real-device WebView with window.__sdk). The result includes a `source: \"mock\" | \"relay-dev\" | \"relay-live\"` field so consumers can identify provenance without inspecting payload values. Use in a relay session (phone attached) to get ground-truth values for upgrading a viewport preset from extrapolated/placeholder to measured. Requires a page to be attached — call list_pages first.",
|
|
3706
|
+
description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Tier C per RFC #277: the same Runtime.evaluate probe runs in both `mock` (devtools panel page with window.__ait state) and `relay` (real-device WebView with window.__sdk). The result includes a `source: \"mock\" | \"relay-dev\" | \"relay-live\" | \"relay-mobile\"` field so consumers can identify provenance without inspecting payload values. (`relay-mobile` = env 2 real-device PWA over an external relay; `relay-dev` = env 3 dog-food WebView; `relay-live` = env 4 production WebView.) Use in a relay session (phone attached) to get ground-truth values for upgrading a viewport preset from extrapolated/placeholder to measured. Requires a page to be attached — call list_pages first.",
|
|
3707
3707
|
inputSchema: {
|
|
3708
3708
|
type: "object",
|
|
3709
3709
|
properties: {},
|
|
@@ -3745,7 +3745,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
3745
3745
|
},
|
|
3746
3746
|
{
|
|
3747
3747
|
name: "call_sdk",
|
|
3748
|
-
description: "Calls a
|
|
3748
|
+
description: "Calls a dog-food SDK method via the window.__sdkCall bridge (exported by @apps-in-toss/web-framework only in __DEBUG_BUILD__ bundles). NOT read-only — SDK calls have side effects (navigation, payments, permissions, etc.). On env 3/4 (real device relay) this hits the real SDK; on env 1 (local mock) and env 2 (PWA relay — real WebKit, mock SDK) it hits the mock SDK. Requires the relay to be attached — call list_pages first. Returns {ok: true, value} on success or {ok: false, error} on failure. If a Runtime.exceptionThrown event was observed within [callStart-50ms, callEnd+200ms], the result also includes `recentException` for crash triage. Returns a clear error if window.__sdkCall is not available — on relay (env 3/4) that means a non-dog-food bundle (redeploy via `ait build && aitcc app deploy`); on local (--target=local, env 1) it means the dev bridge is not installed (start the dev server with `pnpm dev`).\n\nSECURITY: method name, args, and result value are not redacted — never include secrets.\n\nLIVE guard: when running against a live/production relay (relay-live env, MCP_ENV=relay-live), this tool requires `confirm: true` to acknowledge that the SDK call may affect real users. Without it the call is rejected with a structured error. mock and relay-dev sessions are unaffected.\n\nIMPORTANT — 인자 시그니처 (잘못된 인자로 호출하면 토스 앱 crash 위험):\n setDeviceOrientation: call_sdk(\"setDeviceOrientation\", [{ type: \"landscape\" }]) // NOT \"landscape\"\n setIosSwipeGestureEnabled: call_sdk(\"setIosSwipeGestureEnabled\", [{ isEnabled: false }])\n setSecureScreen: call_sdk(\"setSecureScreen\", [{ enabled: true }])\n setScreenAwakeMode: call_sdk(\"setScreenAwakeMode\", [{ enabled: true }])\n getOperationalEnvironment: call_sdk(\"getOperationalEnvironment\", [])\n getPlatformOS: call_sdk(\"getPlatformOS\", [])\n getDeviceId: call_sdk(\"getDeviceId\", [])\n getLocale: call_sdk(\"getLocale\", [])\n getNetworkStatus: call_sdk(\"getNetworkStatus\", [])\n getSchemeUri: call_sdk(\"getSchemeUri\", [])\n requestReview: call_sdk(\"requestReview\", [])\n closeView: call_sdk(\"closeView\", [])",
|
|
3749
3749
|
inputSchema: {
|
|
3750
3750
|
type: "object",
|
|
3751
3751
|
properties: {
|
|
@@ -3799,7 +3799,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
3799
3799
|
},
|
|
3800
3800
|
{
|
|
3801
3801
|
name: "start_debug",
|
|
3802
|
-
description: "Switches the active debug environment in-place (issue #348) — no Claude Code restart and no MCP re-handshake. One daemon holds both a local (env 1, mock SDK in a Chromium) and a relay (env 3/4, real-device Toss WebView over the Chii relay + cloudflared tunnel) connection at once; this tool flips which one every other tool reads from, lazily booting the requested family's infra on first use and keeping the inactive one warm so an existing attach survives the switch. After switching it emits notifications/tools/list_changed — call tools/list again to see the updated tool surface for the new environment.\n\nmodes:\n local-browser — env 1: desktop Chromium with the
|
|
3802
|
+
description: "Switches the active debug environment in-place (issue #348) — no Claude Code restart and no MCP re-handshake. One daemon holds both a local (env 1, mock SDK in a Chromium) and a relay (env 3/4, real-device Toss WebView over the Chii relay + cloudflared tunnel) connection at once; this tool flips which one every other tool reads from, lazily booting the requested family's infra on first use and keeping the inactive one warm so an existing attach survives the switch. After switching it emits notifications/tools/list_changed — call tools/list again to see the updated tool surface for the new environment.\n\nmodes:\n local-browser — env 1: desktop Chromium with the mock SDK and a local CDP attach. Side-effect tools (call_sdk/evaluate) run unguarded against the mock; nothing touches a real device or real users. No prerequisites — the default, always-available environment for state/contract and visual-layout work.\n relay-sandbox — env 2: a real-device PWA (real WebKit engine, mock SDK) over an external Chii relay. CDP covers real-device WebKit DOM, console, exceptions, and safe-area observation; call_sdk still hits the mock (SDK fidelity needs relay-staging). liveIntent off — dev-intent, LIVE guard inactive, side-effect tools run unguarded against the mock. Only the dual-connection daemon can enter relay-sandbox in-place; a single-connection session rejects it with \"동적 전환할 수 없습니다 … relay-sandbox 모드로 재시작하세요\" — follow that hint and restart the MCP server in relay-sandbox mode rather than retrying. Prerequisites: both AIT_RELAY_BASE_URL (the relay base the unplugin emits when started with tunnel:{cdp:true}, used for the CDP attach) and AIT_TUNNEL_BASE_URL (the dev-server tunnel host, required by build_attach_url to render the launcher QR) must be set before the MCP server starts — the unplugin does not auto-forward either; set them explicitly. Both carry relay/tunnel hosts (secret-class) — keep them out of logs.\n relay-staging — env 3: a real-device Toss WebView dog-food build with the REAL SDK over the intoss-private relay. The first environment where call_sdk exercises the genuine native bridge. Side-effect tools run unguarded (dog-food, not released to real users). Prerequisite: a dog-food candidate bundle built with `RELEASE_CHANNEL=dogfood ait build`, then uploaded with `ait deploy` (add `--scheme-only` to print the resulting intoss-private://…?_deploymentId=… deep-link); open that deep-link/QR on the device to cold-load the bundle with the relay injected. Unlike env 2, env 3 is NOT a dev-server tunnel — it is a deployed bundle reached via the intoss-private scheme, so `pnpm dev` plays no part here.\n relay-live — env 4: the REVIEW-PASSED, released production runtime with the REAL SDK over the intoss relay — real end users are on the other side. Read-only debugging is the intent: the LIVE guard is armed, so call_sdk/evaluate require confirm:true per call, and ENTERING relay-live ALSO requires confirm:true on this call. Use it only to observe a shipped regression; verify fixes in relay-staging first.\n\nSwitching back to local-browser automatically disarms the LIVE guard.\n\nFor a relay mode (relay-sandbox/relay-staging/relay-live), also pass projectRoot — the absolute mini-app project root — so the daemon can read the relay auth secret from <projectRoot>/.ait_relay (read-only; the daemon never mints it). Omit it for local-browser.",
|
|
3803
3803
|
inputSchema: {
|
|
3804
3804
|
type: "object",
|
|
3805
3805
|
properties: {
|
|
@@ -3828,7 +3828,7 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
3828
3828
|
},
|
|
3829
3829
|
{
|
|
3830
3830
|
name: "get_debug_status",
|
|
3831
|
-
description: "Reports the current debug session state — which environment/mode is active, whether a page is attached, and a full diagnostic snapshot — in one call. Use this any time to answer \"what mode am I in right now?\" or \"why is this not working?\" without chaining tools. Fields: mcpVersion (MCP SDK version), devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, recentErrors (last N server-side errors, PII/secret redacted), authRejects ({count, lastAt} — relay TOTP 401 rejections, secret-free; count > 0 with empty pages means the phone reached the relay but its code was rejected), environment (kind: mock|relay-dev|relay-live|relay-mobile, env: mock|relay backward-compat, reason, liveGuardActive: true when relay-live LIVE guard is active), serverLockHolder (pid + startedAt from the lock file, or null), nextRecommendedAction ({tool, reason} or null — the single next tool to call; in local-target mode tunnel.up=false is normal so \"restart\" is never recommended). All fields are nullable — missing data is null, not an error. debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. Tier C (both mock and relay).",
|
|
3831
|
+
description: "Reports the current debug session state — which environment/mode is active, whether a page is attached, and a full diagnostic snapshot — in one call. Use this any time to answer \"what mode am I in right now?\" or \"why is this not working?\" without chaining tools. Fields: mcpVersion (MCP SDK version), devtoolsVersion (@ait-co/devtools package version), tunnel (up/wssUrl/pid/startedAt), pages (list_pages result + lastSeenAt stats), lastAttachAt, lastDetachAt, recentErrors (last N server-side errors, PII/secret redacted), authRejects ({count, lastAt} — relay TOTP 401 rejections, secret-free; count > 0 with empty pages means the phone reached the relay but its code was rejected), environment (kind: mock|relay-dev|relay-live|relay-mobile, env: mock|relay backward-compat, reason, liveGuardActive: true when relay-live LIVE guard is active; start_debug mode→kind mapping: relay-sandbox→relay-mobile, relay-staging→relay-dev, relay-live→relay-live, local-browser→mock), serverLockHolder (pid + startedAt from the lock file, or null), nextRecommendedAction ({tool, reason} or null — the single next tool to call; in local-target mode tunnel.up=false is normal so \"restart\" is never recommended). All fields are nullable — missing data is null, not an error. debug-mode only — dev-mode (--mode=dev) does not support relay diagnostics. Tier C (both mock and relay).",
|
|
3832
3832
|
inputSchema: {
|
|
3833
3833
|
type: "object",
|
|
3834
3834
|
properties: { recent_errors_limit: {
|
|
@@ -3993,7 +3993,7 @@ function listPages(connection, tunnel) {
|
|
|
3993
3993
|
};
|
|
3994
3994
|
}
|
|
3995
3995
|
/**
|
|
3996
|
-
* Builds a self-attaching
|
|
3996
|
+
* Builds a self-attaching dog-food deep-link from an `ait deploy --scheme-only`
|
|
3997
3997
|
* URL plus this session's live relay. Throws if the tunnel is not up yet (no
|
|
3998
3998
|
* relay URL to splice in) — the caller surfaces that as a tool error.
|
|
3999
3999
|
*
|
|
@@ -4232,8 +4232,8 @@ async function takeScreenshot(connection) {
|
|
|
4232
4232
|
* those CSS env vars, then `getComputedStyle`.
|
|
4233
4233
|
* 2. SDK insets via a priority chain so the SAME probe works on both relay
|
|
4234
4234
|
* (real device) and mock (devtools panel page):
|
|
4235
|
-
* a. `window.__sdk.SafeAreaInsets.get()` —
|
|
4236
|
-
* b. `window.__sdk.getSafeAreaInsets()` —
|
|
4235
|
+
* a. `window.__sdk.SafeAreaInsets.get()` — dog-food bundle on real device.
|
|
4236
|
+
* b. `window.__sdk.getSafeAreaInsets()` — dog-food bundle (deprecated).
|
|
4237
4237
|
* c. `window.__ait.state.safeAreaInsets` — devtools mock state (mock env).
|
|
4238
4238
|
* The probe records `sdkInsetsSource` = `'window.__sdk'` | `'window.__ait'`
|
|
4239
4239
|
* | `null`. If all paths fail the result carries `sdkInsetsError`.
|
|
@@ -4437,13 +4437,13 @@ async function evaluate(connection, expression) {
|
|
|
4437
4437
|
*
|
|
4438
4438
|
* Name and args are embedded via `JSON.stringify` so they are safely escaped.
|
|
4439
4439
|
* The expression checks for `window.__sdkCall` and returns a clear error if
|
|
4440
|
-
* it is absent (non-
|
|
4440
|
+
* it is absent (non-dog-food bundle).
|
|
4441
4441
|
*
|
|
4442
4442
|
* SECRET-HANDLING: the expression is built here and MUST NOT be written to
|
|
4443
4443
|
* any log or stderr by the caller.
|
|
4444
4444
|
*/
|
|
4445
4445
|
function buildCallSdkExpression(name, args) {
|
|
4446
|
-
return `(async () => { if (typeof window.__sdkCall !== 'function') { return JSON.stringify({ok:false,error:'sdk-absent: window.__sdkCall이 주입되지 않았습니다 (
|
|
4446
|
+
return `(async () => { if (typeof window.__sdkCall !== 'function') { return JSON.stringify({ok:false,error:'sdk-absent: window.__sdkCall이 주입되지 않았습니다 (dog-food 빌드가 아닙니다). dog-food 채널로 재배포하세요.'}); } try { const r = await window.__sdkCall(${JSON.stringify(name)}, ...${JSON.stringify(args)}); return JSON.stringify({ok:true,value:r}); } catch(e) { return JSON.stringify({ok:false,error:String(e && e.message || e)}); }})()`;
|
|
4447
4447
|
}
|
|
4448
4448
|
/**
|
|
4449
4449
|
* Parses the JSON envelope string returned by the `call_sdk` expression.
|
|
@@ -4490,7 +4490,7 @@ function findRecentException(connection, windowStart, windowEnd) {
|
|
|
4490
4490
|
}
|
|
4491
4491
|
}
|
|
4492
4492
|
/**
|
|
4493
|
-
* Calls a
|
|
4493
|
+
* Calls a dog-food SDK method via `window.__sdkCall` on the attached page.
|
|
4494
4494
|
* NOT read-only — SDK calls may have side effects.
|
|
4495
4495
|
*
|
|
4496
4496
|
* On env 3/4 (toss WebView relay) this hits the real SDK. On env 1 (local
|
|
@@ -4660,7 +4660,7 @@ async function readMcpSdkVersion() {
|
|
|
4660
4660
|
* some test environments that skip the build step).
|
|
4661
4661
|
*/
|
|
4662
4662
|
function readDevtoolsVersion() {
|
|
4663
|
-
return "0.1.
|
|
4663
|
+
return "0.1.78";
|
|
4664
4664
|
}
|
|
4665
4665
|
/**
|
|
4666
4666
|
* Derives the next recommended action from a completed diagnostics snapshot.
|
|
@@ -4905,7 +4905,7 @@ async function renderAttachBanner(input) {
|
|
|
4905
4905
|
authNote,
|
|
4906
4906
|
"",
|
|
4907
4907
|
" Use build_attach_url to generate a deep link with the current TOTP code.",
|
|
4908
|
-
" Scan the QR to locate the relay (open the
|
|
4908
|
+
" Scan the QR to locate the relay (open the dog-food URL separately with",
|
|
4909
4909
|
" ?debug=1&relay=<wss>&at=<code> or use the build_attach_url tool):",
|
|
4910
4910
|
"",
|
|
4911
4911
|
qr
|
|
@@ -5164,7 +5164,7 @@ function createDebugServer(deps) {
|
|
|
5164
5164
|
const collector = collectorDep ?? new InMemoryDiagnosticsCollector();
|
|
5165
5165
|
const server = new Server({
|
|
5166
5166
|
name: "ait-debug",
|
|
5167
|
-
version: "0.1.
|
|
5167
|
+
version: "0.1.78"
|
|
5168
5168
|
}, { capabilities: { tools: { listChanged: true } } });
|
|
5169
5169
|
server.setRequestHandler(ListToolsRequestSchema, () => {
|
|
5170
5170
|
const conn = router.active;
|
|
@@ -5243,7 +5243,7 @@ function createDebugServer(deps) {
|
|
|
5243
5243
|
const buildProjectRoot = typeof rawBuildProjectRoot === "string" ? rawBuildProjectRoot : void 0;
|
|
5244
5244
|
let tunnelHttpUrl = process.env.AIT_TUNNEL_BASE_URL?.trim() ?? "";
|
|
5245
5245
|
if (tunnelHttpUrl === "" && buildProjectRoot !== void 0) {
|
|
5246
|
-
const { readRelayUrls } = await import("../relay-url-store-
|
|
5246
|
+
const { readRelayUrls } = await import("../relay-url-store-B_wrNe5A.js");
|
|
5247
5247
|
tunnelHttpUrl = (await readRelayUrls({ projectRoot: buildProjectRoot }))?.tunnelBaseUrl ?? "";
|
|
5248
5248
|
}
|
|
5249
5249
|
if (tunnelHttpUrl === "") return mcpError("build_attach_url(mobile): AIT_TUNNEL_BASE_URL이 설정되지 않았습니다. dev 서버가 tunnel:{cdp:true}로 기동 중이면 .ait_urls 파일이 자동 생성돼 있어야 합니다. 자동 발견이 되지 않을 경우 앱 HTTP 터널 URL을 AIT_TUNNEL_BASE_URL 환경변수로 직접 전달하세요.");
|
|
@@ -6022,7 +6022,7 @@ async function readMobileRelayBaseUrl(env = process.env, projectRoot) {
|
|
|
6022
6022
|
const envValue = typeof raw === "string" ? raw.trim() : "";
|
|
6023
6023
|
if (envValue !== "") return envValue;
|
|
6024
6024
|
if (projectRoot !== void 0) {
|
|
6025
|
-
const { readRelayUrls } = await import("../relay-url-store-
|
|
6025
|
+
const { readRelayUrls } = await import("../relay-url-store-B_wrNe5A.js");
|
|
6026
6026
|
const stored = await readRelayUrls({ projectRoot });
|
|
6027
6027
|
if (stored?.relayBaseUrl !== void 0) return stored.relayBaseUrl;
|
|
6028
6028
|
}
|
|
@@ -7105,7 +7105,7 @@ function createDevServer(deps = {}) {
|
|
|
7105
7105
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
7106
7106
|
const server = new Server({
|
|
7107
7107
|
name: "ait-devtools",
|
|
7108
|
-
version: "0.1.
|
|
7108
|
+
version: "0.1.78"
|
|
7109
7109
|
}, { capabilities: { tools: {} } });
|
|
7110
7110
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
7111
7111
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|