@ait-co/debug-console 0.1.1

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.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.cjs","names":[],"sources":["../../internal-protocol/src/debug-host.ts","../src/gate.ts","../../internal-protocol/src/attach-handshake.ts","../../internal-protocol/src/bridge-observer-state.ts","../../internal-protocol/src/relay-auth-close.ts","../src/bridge-observer.ts","../src/eruda-overlay.ts","../src/sdk-probe.ts","../src/attach.ts","../src/index.ts"],"sourcesContent":["/**\n * Host allowlist predicates — the one part of the activation gate that BOTH\n * sides of the device↔host protocol evaluate.\n *\n * The device evaluates them inside the runtime gate (`@ait-co/debug-console`'s\n * `gate.ts`, Layer B1). The daemon evaluates the same predicate when it decides\n * whether a CDP target's URL is allowed to be driven (`@ait-co/debugger`'s\n * `debug-server.ts`). Before the split those two call sites shared one module\n * by reaching from the daemon into the in-app tree — a reverse edge that would\n * have dragged the browser bundle into the daemon's graph once the two surfaces\n * became separate packages. Extracting the 5 predicates here removes the edge\n * without duplicating the rule.\n *\n * This module is intentionally dependency-free (no Node, no DOM — only\n * `String#endsWith` and one regex) so it inlines into both bundles. There is no\n * barrel index in this package: each consumer imports the one subpath it needs,\n * and its bundler inlines the handful of statements at build time. Nothing here\n * survives as a runtime cross-package import.\n *\n * SECRET-HANDLING: a hostname is an input, never an output. These functions\n * return booleans only — no predicate may grow to log or return the hostname,\n * a relay URL, or a tunnel host.\n */\n\n/**\n * The host suffix the Toss app uses to serve dogfood / private mini-apps.\n *\n * A dogfood entry maps to a host such as\n * `example.private-apps.tossmini.com`. A production entry is served from\n * `*.apps.tossmini.com` — the `.private-apps.` segment is absent.\n */\nconst PRIVATE_APPS_HOST_SUFFIX = '.private-apps.tossmini.com';\n\n/**\n * The parent host suffix for the whole mini-app serving family.\n *\n * The 3.0 runtime loader serves mini-app pages from tossmini.com hosts that are\n * NOT `*.private-apps.tossmini.com`, so under 3.0 the hostname no longer\n * distinguishes a dogfood candidate from a production entry. For those hosts\n * the host layer is demoted from a stage discriminator to a \"known host family\"\n * filter and the effective boundary moves to the opt-in + relay + TOTP layer.\n */\nconst TOSSMINI_HOST_SUFFIX = '.tossmini.com';\n\n/**\n * The host suffix Cloudflare quick-tunnels serve from — the env 2 (PWA) entry.\n */\nconst TRYCLOUDFLARE_HOST_SUFFIX = '.trycloudflare.com';\n\n/**\n * Returns whether `hostname` is a `*.private-apps.tossmini.com` subdomain —\n * the host reserved 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 bare\n * `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 * Returns whether `hostname` is any `*.tossmini.com` subdomain — the host\n * family mini-app pages are served from. Includes the 2.x\n * `*.private-apps.tossmini.com` dogfood hosts and the 3.0 unified serving\n * hosts.\n *\n * Same exact-suffix `endsWith` check as {@link isPrivateAppsHost} — never a\n * substring `.includes()`, which would accept `x.tossmini.com.evil.example`.\n */\nexport function isTossminiHost(hostname: string): boolean {\n return hostname.endsWith(TOSSMINI_HOST_SUFFIX);\n}\n\n/**\n * Returns whether `hostname` is a Cloudflare quick-tunnel host — the env 2\n * (PWA) entry.\n *\n * Env 2 serves the local dev server through a `*.trycloudflare.com` quick\n * tunnel. It has no production runtime: the SDK is the devtools mock and the\n * page is the developer's own dev build. The host safety net (which stops a\n * dogfood build that lands on a production host from attaching) has nothing to\n * protect against here — but the remaining gate layers (opt-in, relay URL,\n * TOTP) still apply, so a leaked tunnel URL is still blocked.\n *\n * Same exact-suffix `endsWith` check as {@link isPrivateAppsHost} — never a\n * substring `.includes()`, which would accept\n * `evil.trycloudflare.com.example.com`.\n */\nexport function isTrycloudflareHost(hostname: string): boolean {\n return hostname.endsWith(TRYCLOUDFLARE_HOST_SUFFIX);\n}\n\n/**\n * Returns true when the hostname is a localhost/loopback address.\n * Allowed: `localhost`, `127.x.x.x` (full RFC 5735 loopback block), `[::1]`,\n * `0.0.0.0`, `*.localhost`.\n *\n * Security note: `hostname.startsWith('127.')` is intentionally NOT used —\n * that pattern would accept `127.evil.com`, which starts with \"127.\" but is an\n * attacker-controlled hostname, not a loopback address. Instead, the 127/8\n * loopback block is matched with a strict numeric-quad regex so only valid\n * dotted-decimal IPv4 in the 127.x.x.x range pass.\n */\nexport function isLocalhostHost(hostname: string): boolean {\n if (hostname === 'localhost' || hostname === '0.0.0.0') return true;\n if (hostname === '[::1]') return true;\n // Match the entire 127/8 loopback block (127.0.0.0 – 127.255.255.255).\n // Each octet is one or more digits — no hostname label can look like this, so\n // the regex unambiguously selects IPv4 loopback addresses only.\n if (/^127\\.\\d+\\.\\d+\\.\\d+$/.test(hostname)) return true;\n if (hostname.endsWith('.localhost')) return true;\n return false;\n}\n\n/**\n * Positive-allowlist kill-switch: returns true when the hostname is a known\n * debug-allowed host. The debug surface is ONLY active on:\n * - localhost / loopback (env 1 desktop dev)\n * - `*.trycloudflare.com` (env 2 PWA tunnel)\n * - `*.tossmini.com` (env 3 dog-food — 2.x private-apps hosts AND the 3.0\n * unified serving family)\n *\n * Any other host is silently blocked. Unlisted hosts never had a debug surface\n * regardless, but this function makes it explicit and auditable in one place.\n *\n * The 3.0 loader serves dogfood candidates and production entries from the same\n * host family, so the hostname alone can no longer separate them. The \"no naked\n * attach on a production-family host\" invariant is preserved one layer down: on\n * tossmini hosts that are not `*.private-apps.*` the TOTP code is mandatory,\n * and production entry URLs carry no debug/relay/at params at all.\n *\n * SECRET-HANDLING: the hostname value MUST NOT be logged or included in any\n * error reason string — only benign labels ('host not in allowlist') are safe.\n */\nexport function isDebugAllowedHost(hostname: string): boolean {\n return isLocalhostHost(hostname) || isTrycloudflareHost(hostname) || isTossminiHost(hostname);\n}\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/debug-console`, 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 * tossmini(3.0)| (reported) | absent | (any) | (any) | BLOCKED (opt-in)\n * tossmini(3.0)| (reported) | present | invalid | (any) | BLOCKED (invalid-relay)\n * tossmini(3.0)| (reported) | present | valid | at absent| BLOCKED (auth — TOTP mandatory, #760)\n * tossmini(3.0)| (reported) | present | valid | pass/at present | ATTACH\n *\n * * \"TOTP ok\" column only applies when `verifyTotpCode` is provided.\n * When no verifier is injected, TOTP check is skipped entirely — EXCEPT\n * on tossmini(3.0) hosts (non-private-apps), where a missing `at=` code\n * blocks with 'auth' even without a verifier (devtools#760; the relay\n * side remains the authoritative verifier).\n *\n * tossmini(3.0) = `*.tossmini.com` hosts that are not\n * `*.private-apps.tossmini.com` — the 3.0 unified serving family. The 3.0\n * loader consumes `_deploymentId` natively, so B2 reports it when present\n * but never requires it there (devtools#760).\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// Layer B1's host predicates are the one part of this gate the DAEMON also\n// evaluates (it re-checks a CDP target's URL before driving it). They therefore\n// live in the shared protocol package instead of here — before the split the\n// daemon reached into this file, a reverse edge that would have dragged the\n// browser bundle into the daemon's graph. Re-exported below so this module's\n// public surface is unchanged.\nimport {\n isDebugAllowedHost,\n isLocalhostHost,\n isPrivateAppsHost,\n isTossminiHost,\n isTrycloudflareHost,\n} from '@ait-co/internal-protocol/debug-host';\n\nexport {\n isDebugAllowedHost,\n isLocalhostHost,\n isPrivateAppsHost,\n isTossminiHost,\n isTrycloudflareHost,\n} from '@ait-co/internal-protocol/debug-host';\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 * 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 // Three 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 // - Localhost/loopback: env 1 desktop dev (127.x.x.x, [::1], localhost,\n // *.localhost, 0.0.0.0). Positive-allowlist kill-switch (#665).\n const isTunnel = isTrycloudflareHost(input.hostname);\n const isLocal = isLocalhostHost(input.hostname);\n if (!isDebugAllowedHost(input.hostname)) {\n return { attach: false, reason: 'host' };\n }\n\n // Layer B2 — runtime entry query gate (2.x private-apps path only).\n // `_deploymentId` must be present and non-empty. The `intoss-private://`\n // scheme used for dogfood entries includes this param and the 2.x runtime\n // propagates it to the page URL; general user entry paths do not. The env 2\n // tunnel and localhost have no deployed bundle and therefore no\n // `_deploymentId` — B2 is skipped for them, and `deploymentId` is reported\n // as the empty string on such attaches (no consumer reads it; see\n // attach.ts). The 3.0 unified serving hosts are also skipped: the 3.0\n // loader consumes `_deploymentId` natively and does NOT propagate it to\n // the page URL (devtools#760) — requiring it there would block every 3.0\n // entry. When it does appear it is still reported.\n let deploymentId = '';\n if (isPrivateAppsHost(input.hostname)) {\n deploymentId = input.searchParams.get('_deploymentId') ?? '';\n if (deploymentId === '') {\n return { attach: false, reason: 'entry' };\n }\n } else if (!isTunnel && !isLocal) {\n deploymentId = input.searchParams.get('_deploymentId') ?? '';\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; the relay side stays authoritative).\n // The `at` query param carries the current TOTP code. When a verifier is\n // injected, an absent or invalid code → BLOCKED. When no verifier is\n // provided (the in-app path — the page has no secret and cannot verify),\n // the check is skipped for backward compatibility EXCEPT on a 3.0\n // tossmini-family host: there the hostname no longer proves a dogfood\n // context (devtools#760), so a missing `at=` code is refused outright to\n // keep the #665 invariant (\"no naked attach on a production-family host\").\n // Real verification of the code value still happens relay-side (4401\n // accept-then-close on mismatch) — this is only the fail-fast half.\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 const atCode = input.searchParams.get('at') ?? '';\n if (input.verifyTotpCode !== undefined) {\n if (!input.verifyTotpCode(atCode)) {\n return { attach: false, reason: 'auth' };\n }\n } else if (\n isTossminiHost(input.hostname) &&\n !isPrivateAppsHost(input.hostname) &&\n atCode === ''\n ) {\n return { attach: false, reason: 'auth' };\n }\n\n return { attach: true, relayUrl: relayUrl.href, deploymentId };\n}\n","/**\n * Version handshake carried on the attach path.\n *\n * WHY THIS EXISTS — every other clause of the device↔host contract is a\n * value-duplicated string or number with zero compile-time linkage: the\n * `window.__ait_bridge` snapshot and its field names, the `ait:bridge-call` /\n * `ait:relay-ws-state` event names, the `__ait_relay_ws_observed` flag, the\n * `#__ait_debug_indicator` element id, the `/at/<code>/target.js` path\n * convention, `window.__sdk` / `window.__sdkCall`, and close code 4401. When\n * the two sides drift, none of that crashes — it silently misbehaves. The\n * indicator renders empty, a field reads `undefined`, a close frame goes\n * unrecognised. Nothing in the stack says why.\n *\n * Changesets `fixed` keeps `@ait-co/debugger` and `@ait-co/debug-console` on\n * one version number, so \"same version\" is exactly \"mutually tested pair\". What\n * `fixed` cannot do is stop a consumer from installing a skewed pair. This\n * handshake turns that skew from a silent misbehaviour into one diagnostic\n * line: the device reports its build-time `__VERSION__` on a fire-and-forget\n * request just before it injects `target.js`, and the daemon compares it with\n * its own.\n *\n * Why a dedicated request rather than a query param on `target.js`: chii's\n * stock `target.js` derives its WebSocket endpoint from its own script `src`\n * via `src.replace('target.js', '')` and then appends `target/<id>`. Any query\n * string on that URL would land in the middle of the derived endpoint and break\n * the dial. The path is therefore separate, and it rides the same\n * `/at/<code>/` prefix so the relay's existing auth gate covers it unchanged.\n *\n * SECRET-HANDLING: the payload is a version string and nothing else. The\n * request carries a TOTP code only in the same path-prefix form the target\n * script already uses; neither side may log the URL, the code, or the relay\n * host. The mismatch diagnostic names two version strings — never a URL.\n */\n\n/**\n * Relay HTTP path the device pings once, immediately before injecting\n * `target.js`. Served by the daemon's relay; the response body is empty.\n */\nexport const ATTACH_HANDSHAKE_PATH = '/ait-attach';\n\n/** Query parameter carrying the device-side build-time `__VERSION__`. */\nexport const ATTACH_HANDSHAKE_VERSION_PARAM = 'v';\n\n/** Outcome of comparing the reported device version with the daemon's own. */\nexport interface ProtocolVersionCheck {\n /**\n * `true` when the two sides are a mutually tested pair, or when the device\n * reported nothing at all.\n *\n * An absent report is deliberately NOT a mismatch: a device running a build\n * from before this handshake existed, or one whose fire-and-forget request\n * was dropped by a flaky tunnel, must not be reported as skewed. Only two\n * versions that are both present and different count.\n */\n readonly match: boolean;\n /** Version the device reported, or `''` when it reported none. */\n readonly device: string;\n /** Version the daemon was built with, or `''` when unknown. */\n readonly host: string;\n}\n\n/**\n * Compares the device-reported version with the daemon's own build version.\n *\n * Exact string equality — the two packages are released as a `fixed` pair, so\n * any difference at all means the consumer assembled a combination that was\n * never tested together. Semver-range logic would only blur that.\n *\n * @param device - Version reported by `@ait-co/debug-console`, if any.\n * @param host - The daemon's own `__VERSION__`, if any.\n */\nexport function compareProtocolVersions(\n device: string | null | undefined,\n host: string | null | undefined,\n): ProtocolVersionCheck {\n const deviceVersion = typeof device === 'string' ? device.trim() : '';\n const hostVersion = typeof host === 'string' ? host.trim() : '';\n // Either side unknown → nothing to contradict. See `match` above.\n const match = deviceVersion === '' || hostVersion === '' || deviceVersion === hostVersion;\n return { match, device: deviceVersion, host: hostVersion };\n}\n","/**\n * The `window.__ait_bridge` snapshot shape and the event/global names the\n * device publishes it under.\n *\n * The device (`@ait-co/debug-console`) writes the snapshot; the daemon\n * (`@ait-co/debugger`) reads it back field-by-field from inside a CDP\n * `Runtime.evaluate` source string it assembles as text. There is therefore no\n * compile-time link between writer and reader — the daemon's use of the\n * interface is type-only (erased entirely) and its use of the name constants is\n * string interpolation. Keeping both in one module is what makes a rename a\n * compile error on the writing side and a single edit on the reading side,\n * instead of a silent no-op badge.\n *\n * SECRET-HANDLING: the snapshot holds API METHOD NAMES + timestamps + a\n * correlation id ONLY. It must never grow fields for call arguments, results,\n * relay URLs, or auth codes.\n */\n\n/** Name of the API being called (never its arguments). */\nexport interface BridgePendingCall {\n method: string;\n /** `Date.now()` epoch ms when the call was dispatched. */\n startedAt: number;\n}\n\n/** The most-recent bridge activity — API name + wall-clock + settle status. */\nexport interface BridgeLastCall {\n method: string;\n /** `Date.now()` epoch ms of the start or settle that produced this record. */\n at: number;\n status: 'pending' | 'resolved' | 'rejected';\n}\n\n/**\n * The snapshot exposed on `window.__ait_bridge`. `pending` is keyed by\n * correlation id so a settle is an O(1) delete; the indicator reads\n * `Object.values(pending)` and computes each call's live elapsed itself.\n */\nexport interface BridgeObserverState {\n pending: Record<string, BridgePendingCall>;\n last: BridgeLastCall | null;\n}\n\n/**\n * CustomEvent fired (no detail) on every bridge-call start/settle so the\n * indicator badge re-renders promptly. SECRET-HANDLING: carries no detail\n * payload at all — the badge reads the enum-only snapshot on receipt.\n */\nexport const BRIDGE_CALL_EVENT = 'ait:bridge-call';\n\n/**\n * CustomEvent fired with `{ detail: { state } }` on every relay-WebSocket\n * open/close, so a CDP-injected indicator can follow relay liveness without\n * installing a second Proxy on `window.WebSocket`. `state` is an enum\n * (`'open' | 'closed'`) — never a URL.\n */\nexport const RELAY_WS_STATE_EVENT = 'ait:relay-ws-state';\n\n/**\n * Window flag the device sets once its relay-WebSocket observer is installed.\n * The daemon reads it to decide between subscribing to\n * {@link RELAY_WS_STATE_EVENT} and installing its own fallback observer.\n */\nexport const RELAY_WS_OBSERVED_FLAG = '__ait_relay_ws_observed';\n\n/** Window property the {@link BridgeObserverState} snapshot is published on. */\nexport const BRIDGE_STATE_GLOBAL = '__ait_bridge';\n","/**\n * Shared constants for the relay's named TOTP-auth rejection.\n *\n * Before the accept-then-close fix the relay rejected an unauthenticated\n * WebSocket upgrade with a raw `HTTP/1.1 401` + `socket.destroy()`. A handshake\n * aborted that way is indistinguishable from a network failure on the browser\n * side — the WebSocket only ever sees close code 1006, so the phone (env-2\n * launcher PWA) could not tell \"stale TOTP code\" apart from \"tunnel down\" and\n * stayed silent. The fix is accept-then-close: complete the handshake, then\n * close with an application close code that NAMES the rejection.\n *\n * Three parties share this contract:\n * - the relay (`@ait-co/debugger`, Node) sends the close frame / HTTP error body;\n * - the on-device console (`@ait-co/debug-console`, browser) observes\n * relay-bound WebSockets and surfaces the code to the launcher shell;\n * - the daemon's own CDP client (`@ait-co/debugger`, Node) recognises the code\n * as an auth failure on its `/client` dial.\n *\n * This module is intentionally dependency-free (no Node, no DOM) so it is safe\n * to inline into both the browser bundle and the daemon 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 * In-app native-bridge call observer (issue #749).\n *\n * WHY THIS EXISTS — the mock's `observe()` (`src/mock/observe.ts` → `aitState.\n * sdkCallLog`) only wraps the MOCK SDK. In the on-device debug context (env 3,\n * real Toss WebView, run7) the REAL `@apps-in-toss/web-framework` is loaded, so\n * that groundwork sees nothing. The in-app debug surface needs its own\n * observation point at the one place every real async bridge call funnels\n * through, so the on-phone indicator can answer run7's question: is a spinner a\n * pending native call, the app's own UI, or a wedged JS main thread?\n *\n * OBSERVATION POINTS (version-agnostic — both SDK lines route through one of\n * these; picked so a GA flip 2.x↔3.0 does not break the signal):\n *\n * - 3.0 line — `window.__appsInTossNativeBridge.callAsyncMethod(name, params)`\n * is the single async dispatcher; it returns the native Promise. Wrapping\n * it gives the COMPLETE lifecycle (pending on call, settle on resolve/\n * reject) from ONE hook, with no reliance on how native calls back.\n * - 2.x line — there is no single dispatcher method (each async bridge is an\n * independent `createAsyncBridge` closure). The universal START choke point\n * is `window.ReactNativeWebView.postMessage(json)`; the matching SETTLE\n * signal is `window.__GRANITE_NATIVE_EMITTER.emit('<method>/resolve|reject/\n * <eventId>')`. We wrap both.\n *\n * The observer publishes a read-only snapshot on `window.__ait_bridge` (an\n * id→pending map + a last-call record) and fires a payload-less\n * {@link BRIDGE_CALL_EVENT} CustomEvent on every change, so the CDP-injected\n * indicator badge (`buildIndicatorExpression`) can render the pending list +\n * last-call stamp without polling — the same pub/sub decoupling the relay-WS\n * observer uses for `ait:relay-ws-state` (#730).\n *\n * SECRET-HANDLING: this module records API METHOD NAMES + timestamps + a\n * correlation id ONLY. It NEVER reads, stores, or forwards call arguments\n * (`params`/`args`) or results — those may carry tokens, URLs, or user data.\n * The outbound-message parser reads `type`/`name`/`functionName`/`callbackId`/\n * `eventId` and deliberately never touches `params`/`args`.\n *\n * Lives in the in-app graph (reached only via {@link maybeAttach}); a release\n * consumer build DCEs it with the rest of the debug surface (§check:debug-\n * surface-absent). Never throws into the host app — every hook is guarded.\n */\n\n// The snapshot shape and the event name are the device half of a contract the\n// daemon reads back through a CDP source string, so they live in the shared\n// protocol package rather than here (see\n// `@ait-co/internal-protocol/bridge-observer-state`). They are re-exported so\n// this module's public surface is unchanged for consumers.\nimport {\n BRIDGE_CALL_EVENT,\n type BridgeObserverState,\n} from '@ait-co/internal-protocol/bridge-observer-state';\n\nexport {\n BRIDGE_CALL_EVENT,\n type BridgeLastCall,\n type BridgeObserverState,\n type BridgePendingCall,\n} from '@ait-co/internal-protocol/bridge-observer-state';\n\n/**\n * Pending entries older than this are pruned on the next start — a safety net\n * for the fallback path where a settle signal might be missed, so the pending\n * list can never grow unbounded or show a forever-stuck row. Generous enough\n * that a genuinely slow native call (the exact signal we want to surface) still\n * shows while it is plausibly in flight.\n */\nconst MAX_PENDING_AGE_MS = 120_000;\n\ndeclare global {\n interface Window {\n /**\n * Read-only bridge-call snapshot published by {@link installBridgeObserver}\n * (#749). The CDP-injected indicator badge reads this to render the pending\n * native-call list + last-call stamp. Absent when no debug attach ran or in\n * a context with no observable bridge (env 2 mock). SECRET-HANDLING: holds\n * API names + timings only — never arguments or results.\n */\n __ait_bridge?: BridgeObserverState | undefined;\n /**\n * 3.0-line native bridge (`@apps-in-toss/webview-bridge`\n * `injectNativeBridge`). `callAsyncMethod` is the single async dispatcher\n * the observer wraps.\n */\n __appsInTossNativeBridge?: {\n callAsyncMethod?: (name: string, params?: unknown) => unknown;\n [key: string]: unknown;\n };\n /** 2.x-line native call transport (`ReactNativeWebView.postMessage`). */\n ReactNativeWebView?: {\n postMessage?: (message: string) => void;\n [key: string]: unknown;\n };\n /** 2.x-line native→JS settle emitter (`@apps-in-toss/bridge-core`). */\n __GRANITE_NATIVE_EMITTER?: {\n emit?: (event: string, args?: unknown) => void;\n [key: string]: unknown;\n };\n }\n}\n\n/** Guard so the observer wraps the bridge at most once per page lifecycle. */\nlet bridgeObserverInstalled = false;\n\n/** Monotonic id source for the primary (3.0) path, where native gives us none. */\nlet callIdCounter = 0;\n\n/** Undo hooks that {@link uninstallBridgeObserver} runs to restore originals. */\nlet restoreHooks: Array<() => void> = [];\n\n/** Drops pending entries older than {@link MAX_PENDING_AGE_MS}. */\nfunction pruneStale(state: BridgeObserverState, now: number): void {\n for (const id of Object.keys(state.pending)) {\n const entry = state.pending[id];\n if (entry !== undefined && now - entry.startedAt > MAX_PENDING_AGE_MS) {\n delete state.pending[id];\n }\n }\n}\n\n/** Fires the payload-less notify event so the badge re-renders. */\nfunction broadcast(): void {\n if (typeof window === 'undefined') return;\n window.dispatchEvent(new CustomEvent(BRIDGE_CALL_EVENT));\n}\n\n/** Records a call start: add to pending, set last=pending, notify. */\nfunction startCall(state: BridgeObserverState, id: string, method: string, now: number): void {\n pruneStale(state, now);\n state.pending[id] = { method, startedAt: now };\n state.last = { method, at: now, status: 'pending' };\n broadcast();\n}\n\n/** Records a call settle: remove from pending, stamp last, notify. */\nfunction settleCall(\n state: BridgeObserverState,\n id: string,\n status: 'resolved' | 'rejected',\n now: number,\n): void {\n const entry = state.pending[id];\n delete state.pending[id];\n const method = entry?.method ?? state.last?.method ?? 'unknown';\n state.last = { method, at: now, status };\n broadcast();\n}\n\n/**\n * Parses an outbound `ReactNativeWebView.postMessage` JSON envelope and records\n * a START for async request/response calls only. Event subscriptions\n * (`addEventListener`/`removeEventListener`/`callEventMethod`), cleanup, and\n * constants are ignored — they are not the \"spinner is a pending call\" signal.\n *\n * SECRET-HANDLING: reads `type`/`name`/`functionName`/`callbackId`/`eventId`\n * ONLY — never `params`/`args`.\n */\nfunction observeOutbound(state: BridgeObserverState, message: string): void {\n if (typeof message !== 'string') return;\n let parsed: Record<string, unknown>;\n try {\n parsed = JSON.parse(message) as Record<string, unknown>;\n } catch {\n return;\n }\n const now = Date.now();\n // 3.0 envelope: { from:'appsInTossNativeBridge', type:'callAsyncMethod', callbackId, name }\n if (\n parsed.type === 'callAsyncMethod' &&\n typeof parsed.name === 'string' &&\n typeof parsed.callbackId === 'string'\n ) {\n startCall(state, parsed.callbackId, parsed.name, now);\n return;\n }\n // 2.x envelope: { type:'method', functionName, eventId, args }\n if (\n parsed.type === 'method' &&\n typeof parsed.functionName === 'string' &&\n typeof parsed.eventId === 'string'\n ) {\n startCall(state, parsed.eventId, parsed.functionName, now);\n }\n}\n\n/**\n * Parses a `__GRANITE_NATIVE_EMITTER.emit` event name and records a SETTLE for\n * 2.x async resolves/rejects (`<method>/resolve/<eventId>` |\n * `<method>/reject/<eventId>`). Event-bridge emits (`.../onEvent/...`) are\n * ignored. SECRET-HANDLING: reads the event NAME only — never the emitted args.\n */\nfunction observeSettle(state: BridgeObserverState, event: string): void {\n if (typeof event !== 'string') return;\n const match = /\\/(resolve|reject)\\/([^/]+)$/.exec(event);\n if (match === null) return;\n settleCall(\n state,\n match[2] as string,\n match[1] === 'resolve' ? 'resolved' : 'rejected',\n Date.now(),\n );\n}\n\n/**\n * Installs the native-bridge call observer (#749). Idempotent per page\n * lifecycle. Called by {@link maybeAttach} after the gate passes (debug builds\n * only). Prefers the 3.0 single-dispatcher wrap; falls back to the universal\n * 2.x postMessage-start + emitter-settle pair. A context with no observable\n * bridge (env 2 mock) leaves `window.__ait_bridge` as an empty snapshot — the\n * badge then shows the heartbeat only, which is correct.\n *\n * Never throws into the host app — a wrapped hook that somehow fails is caught\n * and the original behavior is always preserved.\n */\nexport function installBridgeObserver(): void {\n if (bridgeObserverInstalled) return;\n if (typeof window === 'undefined') return;\n bridgeObserverInstalled = true;\n\n const state: BridgeObserverState = {\n pending: Object.create(null) as Record<string, never>,\n last: null,\n };\n window.__ait_bridge = state;\n\n // ── Primary (3.0): wrap the single async dispatcher ────────────────────────\n const nativeBridge = window.__appsInTossNativeBridge;\n if (nativeBridge !== undefined && typeof nativeBridge.callAsyncMethod === 'function') {\n const original = nativeBridge.callAsyncMethod;\n const wrapped = function (this: unknown, name: string, params?: unknown): unknown {\n const id = `c${++callIdCounter}`;\n const started = Date.now();\n startCall(state, id, String(name), started);\n let result: unknown;\n try {\n // SECRET-HANDLING: params is forwarded UNTOUCHED — never read or stored.\n result = original.call(this, name, params);\n } catch (err) {\n settleCall(state, id, 'rejected', Date.now());\n throw err;\n }\n if (result !== null && typeof (result as { then?: unknown }).then === 'function') {\n // Observe the returned native Promise WITHOUT altering it — the original\n // reference is returned unchanged (behavior-preserving).\n (result as Promise<unknown>).then(\n () => settleCall(state, id, 'resolved', Date.now()),\n () => settleCall(state, id, 'rejected', Date.now()),\n );\n } else {\n settleCall(state, id, 'resolved', Date.now());\n }\n return result;\n };\n nativeBridge.callAsyncMethod = wrapped;\n restoreHooks.push(() => {\n nativeBridge.callAsyncMethod = original;\n });\n return;\n }\n\n // ── Fallback (2.x / generic): postMessage START + emitter SETTLE ───────────\n const webView = window.ReactNativeWebView;\n if (webView !== undefined && typeof webView.postMessage === 'function') {\n const originalPost = webView.postMessage;\n webView.postMessage = function (this: unknown, message: string): void {\n try {\n observeOutbound(state, message);\n } catch {\n // Never let observation break native dispatch.\n }\n originalPost.call(this, message);\n };\n restoreHooks.push(() => {\n webView.postMessage = originalPost;\n });\n }\n const emitter = window.__GRANITE_NATIVE_EMITTER;\n if (emitter !== undefined && typeof emitter.emit === 'function') {\n const originalEmit = emitter.emit;\n emitter.emit = function (this: unknown, event: string, args?: unknown): void {\n try {\n observeSettle(state, event);\n } catch {\n // Never let observation break native settle delivery.\n }\n originalEmit.call(this, event, args);\n };\n restoreHooks.push(() => {\n emitter.emit = originalEmit;\n });\n }\n}\n\n/**\n * Restores every wrapped bridge hook and removes `window.__ait_bridge` (#749).\n * Idempotent; safe to call when nothing was installed. Wired into\n * {@link detachDebugSurface} (#748) so no bridge wrap survives a run's end.\n */\nexport function uninstallBridgeObserver(): void {\n if (!bridgeObserverInstalled) return;\n bridgeObserverInstalled = false;\n const hooks = restoreHooks;\n restoreHooks = [];\n for (const undo of hooks) {\n try {\n undo();\n } catch {\n // Best-effort restore — a failed undo must not break teardown.\n }\n }\n if (typeof window !== 'undefined') {\n window.__ait_bridge = undefined;\n }\n}\n","/**\n * In-app eruda console overlay for the debug attach flow.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n *\n * This module mounts the eruda in-page console (https://github.com/liriliri/eruda)\n * on the phone screen when a debug session attaches. It is the mobile-only\n * counterpart to the Chii `target.js` injection in {@link attach.ts}: Chii is a\n * REMOTE CDP transport (phone → relay → PC DevTools frontend), whereas eruda is\n * a LOCAL in-page view — a floating button + console/network/DOM/storage panels\n * rendered directly on the phone, with no relay or second device. The two are\n * orthogonal and coexist (eruda opens no WebSocket, mounts into its own\n * `#eruda` shadow host — it cannot collide with the relay WS or the Chii DOM).\n *\n * Build-time absence (the security contract): this module lives in the\n * `@ait-co/debug-console` graph. A consumer wraps its\n * `import('@ait-co/debug-console')` call site in `if (__DEBUG_BUILD__) { … }`;\n * a release build folds that constant to `false` and dead-code-eliminates the\n * whole module — so eruda (and its dynamic `import('eruda')` chunk) is simply\n * absent from release bundles, exactly like the Chii target.js injection. The\n * `import('eruda')` here is a dynamic import precisely so the bundler emits it\n * as a separate chunk that the dead branch never pulls in.\n *\n * Runtime gate: `mountEruda()` is called only from `maybeAttach()` AFTER the\n * full Layer B/C gate has passed (`gateResult.attach === true`) — host\n * allowlist, `debug=1`, relay URL, and TOTP. So eruda inherits the same\n * four-layer defence as the Chii injection, byte-for-byte, with no eruda-\n * specific gate of its own.\n *\n * SECRET-HANDLING: this module reads no secret, TOTP code, relay URL, or host\n * value, and logs none. eruda observes only the page it is mounted on.\n */\n\n/** Module-level guard against double mount across repeated `maybeAttach` calls. */\nlet erudaMounted = false;\n\n/**\n * The loaded eruda module, captured on a successful {@link mountEruda} so\n * {@link unmountEruda} can call `.destroy()` on the same instance during\n * graceful detach (#748). `null` when eruda was never mounted.\n */\nlet erudaModule: { init(): void; destroy(): void } | null = null;\n\n/**\n * Mounts the eruda in-page console once.\n *\n * Idempotent: repeated calls after a successful mount are no-ops, mirroring the\n * `attached` guard in {@link attach.ts}. Fail-silent: if the dynamic import or\n * `eruda.init()` throws (eruda absent, or a runtime that rejects it), the Chii\n * debug session is unaffected — eruda is an additive convenience, not a\n * dependency of the relay path.\n *\n * `eruda.init()` mounts eruda's own floating entry button on the phone screen;\n * tapping it opens the console. We do not add a separate button.\n */\nexport async function mountEruda(): Promise<void> {\n if (erudaMounted || typeof document === 'undefined') {\n return;\n }\n // Set the guard before the await so a synchronous re-entrant call cannot\n // start a second import in flight.\n erudaMounted = true;\n try {\n const eruda = (await import('eruda')).default;\n eruda.init();\n // Capture for graceful detach (#748) — only after init() succeeds.\n erudaModule = eruda;\n } catch (err) {\n // Reset so a later attach can retry; never break the Chii session.\n erudaMounted = false;\n console.debug('[@ait-co/debug-console] eruda console mount skipped:', err);\n }\n}\n\n/**\n * Unmounts the eruda in-page console (#748 graceful detach).\n *\n * Calls `eruda.destroy()`, which removes eruda's floating entry button, any\n * open panel, and its `#eruda` shadow host — returning the phone screen to a\n * clean, non-debug state when a debug session ends. After a successful unmount\n * the guard is reset so a later {@link mountEruda} (a fresh attach) can\n * re-mount.\n *\n * Idempotent: a call when eruda was never mounted (or already unmounted) is a\n * no-op. Fail-silent: a `destroy()` throw is swallowed — teardown must never\n * throw into the host app.\n */\nexport function unmountEruda(): void {\n if (!erudaMounted || erudaModule === null) {\n return;\n }\n try {\n erudaModule.destroy();\n } catch (err) {\n console.debug('[@ait-co/debug-console] eruda console unmount skipped:', err);\n } finally {\n // Reset regardless — a failed destroy should not wedge the guard, and a\n // later attach must be free to re-mount.\n erudaMounted = false;\n erudaModule = null;\n }\n}\n","/**\n * Runtime probe for the optional Apps in Toss SDK.\n *\n * WHY A PROBE AND NOT AN IMPORT — this package is the only one in the split\n * that can end up inside a consumer's production bundle, and it ships with\n * `eruda` as its single dependency and no peer dependencies at all. A static\n * `import { setScreenAwakeMode } from '@apps-in-toss/web-framework'` would undo\n * both properties at once: it forces the SDK into the module graph of every\n * consumer that reaches this file, and it pins the package to whichever SDK\n * major exports that symbol. `setScreenAwakeMode` is not part of\n * `@apps-in-toss/web-framework` 2.x's web surface at all — it arrives in the\n * 3.0 line — so a static import does not merely couple us to a version, it\n * breaks 2.x consumers outright.\n *\n * The probe keeps the dependency edge at runtime, where it can be absent\n * without consequence: `import()` inside a `try`, a `typeof === 'function'`\n * check on the export we actually want, and silence when either step fails. The\n * SDK is a devDependency here purely so `tsc` and the unit tests can resolve the\n * specifier; the bundle marks it external so it is never inlined. That makes a\n * 2.x↔3.x GA flip a no-op for this package — the same shape `@ait-co/polyfill`'s\n * `loadTossSdk()` uses.\n *\n * SECRET-HANDLING: nothing here reads or logs a relay URL, a tunnel host, or an\n * auth code. Failures are swallowed; the only thing ever printed is a package-\n * prefixed debug line naming the API that was unavailable.\n */\n\n/**\n * Resolved SDK namespace, `null` once a probe has established the SDK is\n * absent, `undefined` while no probe has run yet. Module-scoped, so a\n * `vi.resetModules()` in a test gives a fresh probe.\n */\nlet sdkCache: Record<string, unknown> | null | undefined;\n\n/**\n * Loads the SDK module if the consumer has it installed, else `null`.\n *\n * Never throws and never rejects. The return type is deliberately a bare\n * property bag rather than `typeof import('@apps-in-toss/web-framework')`:\n * callers must feature-sniff the export they need, which is what keeps this\n * file compiling — and behaving — identically against 2.x and 3.x type\n * surfaces.\n */\nexport async function loadTossSdk(): Promise<Record<string, unknown> | null> {\n if (sdkCache !== undefined) return sdkCache;\n try {\n const mod: unknown = await import('@apps-in-toss/web-framework');\n sdkCache = mod as Record<string, unknown>;\n } catch {\n // SDK absent (MCP-only consumer, plain browser, test env) — not an error.\n sdkCache = null;\n }\n return sdkCache;\n}\n\n/**\n * The SDK namespace if a previous {@link loadTossSdk} already resolved it, else\n * `null`. Never starts a probe of its own.\n *\n * This exists for the unload path. `pagehide` / `beforeunload` handlers are\n * expected to do their work synchronously — a browser tearing the page down is\n * under no obligation to drain the microtask queue first, so a teardown that\n * begins with `await import(...)` can simply never reach its own body and leave\n * the screen held awake. After a successful attach the cache is warm (the same\n * probe already ran to hold the screen), so the release can be dispatched\n * immediately instead.\n */\nexport function getLoadedTossSdk(): Record<string, unknown> | null {\n return sdkCache ?? null;\n}\n\n/**\n * Reads a callable export off the SDK namespace, or `null` when it is absent or\n * is not a function. May throw only if the namespace itself is hostile — every\n * caller therefore keeps this inside its own `try`.\n */\nfunction pickExport(\n sdk: Record<string, unknown>,\n name: string,\n): ((...args: unknown[]) => unknown) | null {\n const value = sdk[name];\n return typeof value === 'function' ? (value as (...args: unknown[]) => unknown) : null;\n}\n\n/**\n * Calls an SDK export by name when it exists, otherwise resolves quietly.\n *\n * @param name - Export to look up on the SDK namespace.\n * @param args - Arguments forwarded verbatim to the export.\n * @returns `true` when the call was dispatched and settled without throwing,\n * `false` when the SDK or the export was unavailable or the call rejected.\n */\nexport async function callTossSdk(name: string, ...args: unknown[]): Promise<boolean> {\n const sdk = await loadTossSdk();\n if (sdk === null) return false;\n try {\n // The lookup is inside the try on purpose. A plain module namespace answers\n // an unknown key with `undefined`, but a namespace behind a Proxy or a\n // throwing getter does not, and this package must not turn \"SDK shaped\n // differently than expected\" into an exception in the host app.\n const fn = pickExport(sdk, name);\n if (fn === null) return false;\n await fn(...args);\n return true;\n } catch (err) {\n console.debug(`[@ait-co/debug-console] ${name} failed:`, err);\n return false;\n }\n}\n\n/**\n * Dispatches an SDK call in the current tick, using only an already-warm cache.\n *\n * Returns `false` without doing anything when no probe has resolved yet — the\n * caller is expected to fall back to {@link callTossSdk}. Unlike that function,\n * `true` here means only \"dispatched\": a returned promise is left to settle on\n * its own, because the whole point is not to await inside an unload handler.\n *\n * @param name - Export to look up on the SDK namespace.\n * @param args - Arguments forwarded verbatim to the export.\n */\nexport function callTossSdkNow(name: string, ...args: unknown[]): boolean {\n const sdk = getLoadedTossSdk();\n if (sdk === null) return false;\n try {\n const fn = pickExport(sdk, name);\n if (fn === null) return false;\n const result: unknown = fn(...args);\n if (result instanceof Promise) {\n result.catch((err: unknown) => {\n console.debug(`[@ait-co/debug-console] ${name} failed:`, err);\n });\n }\n return true;\n } catch (err) {\n console.debug(`[@ait-co/debug-console] ${name} failed:`, err);\n return false;\n }\n}\n\n/**\n * Keeps the phone screen awake for the duration of a debug session (or releases\n * it again). No-op when the SDK is absent or does not expose the API.\n *\n * SECRET-HANDLING: only flips a boolean flag — reads nothing from the relay URL\n * or the auth code.\n *\n * @param enabled - `true` to hold the screen awake, `false` to restore normal\n * sleep behaviour.\n */\nexport async function setScreenAwake(enabled: boolean): Promise<boolean> {\n return callTossSdk('setScreenAwakeMode', { enabled });\n}\n\n/**\n * Same as {@link setScreenAwake}, but dispatched in the current tick from an\n * already-warm cache; `false` means nothing was dispatched and the caller\n * should fall back to the async form. Used by the teardown path — see\n * {@link getLoadedTossSdk} for why an unload handler cannot await.\n *\n * @param enabled - `true` to hold the screen awake, `false` to restore normal\n * sleep behaviour.\n */\nexport function setScreenAwakeNow(enabled: boolean): boolean {\n return callTossSdkNow('setScreenAwakeMode', { enabled });\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 {\n ATTACH_HANDSHAKE_PATH,\n ATTACH_HANDSHAKE_VERSION_PARAM,\n} from '@ait-co/internal-protocol/attach-handshake';\nimport { RELAY_WS_STATE_EVENT } from '@ait-co/internal-protocol/bridge-observer-state';\nimport {\n RELAY_AUTH_REJECT_CLOSE_CODE,\n RELAY_AUTH_REJECT_REASON,\n} from '@ait-co/internal-protocol/relay-auth-close';\nimport { installBridgeObserver, uninstallBridgeObserver } from './bridge-observer.js';\nimport { mountEruda, unmountEruda } from './eruda-overlay.js';\nimport { checkDebugGate, type GateResult } from './index.js';\nimport { setScreenAwake, setScreenAwakeNow } from './sdk-probe.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/**\n * Converts a validated `wss:` relay URL into the version-handshake URL.\n *\n * Same scheme/host/TOTP-prefix derivation as {@link deriveTargetScriptUrl},\n * with this package's build-time `__VERSION__` as the single query value. The\n * handshake gets its own path rather than a query on `target.js` because chii's\n * stock `target.js` derives its WebSocket endpoint from its own script `src`\n * (`src.replace('target.js', '')`) and then appends `target/<id>` — a query\n * string there would land in the middle of the derived endpoint.\n *\n * SECRET-HANDLING: `atCode` rides only inside the returned URL (the same\n * transport the target script already uses) and is never logged.\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`/`''` for the un-prefixed form.\n */\nexport function deriveHandshakeUrl(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)}${ATTACH_HANDSHAKE_PATH}`\n : ATTACH_HANDSHAKE_PATH;\n u.search = `?${ATTACH_HANDSHAKE_VERSION_PARAM}=${encodeURIComponent(__VERSION__)}`;\n u.hash = '';\n return u.toString();\n}\n\n/**\n * Reports this package's build-time version to the relay, fire-and-forget.\n *\n * Runs just before `target.js` is injected. Deliberately unawaited and\n * fail-silent: a device↔host version skew is worth a diagnostic on the daemon\n * side, never a reason to hold up or abort an attach. `mode: 'no-cors'` because\n * the relay origin differs from the page origin and we have no use for the\n * response — the daemon is the only party that acts on this.\n *\n * SECRET-HANDLING: the URL (which carries the TOTP code in its path prefix) is\n * never logged, not even on failure.\n */\nfunction reportProtocolVersion(relayUrl: string, atCode?: string | null): void {\n if (typeof fetch !== 'function') return;\n try {\n void fetch(deriveHandshakeUrl(relayUrl, atCode), {\n method: 'GET',\n mode: 'no-cors',\n cache: 'no-store',\n keepalive: true,\n }).catch(() => {\n // Handshake is best-effort — a dropped ping only costs the diagnostic.\n });\n } catch {\n // URL construction or a hardened fetch can throw synchronously; ignore.\n }\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\ndeclare global {\n interface Window {\n /**\n * Set once {@link installRelayWsObserver} has wrapped `window.WebSocket`\n * (#730). The bare CDP-injected indicator (`buildIndicatorExpression`)\n * checks this flag to decide whether it can piggy-back on the\n * `ait:relay-ws-state` CustomEvent this module broadcasts, instead of\n * installing a second competing `Proxy` on `window.WebSocket`.\n */\n __ait_relay_ws_observed?: boolean;\n }\n}\n\n/**\n * Broadcasts relay-socket lifecycle to any in-page listener (#730) — the\n * on-phone debug indicator subscribes to this instead of wrapping\n * `window.WebSocket` a second time.\n *\n * SECRET-HANDLING: the CustomEvent `detail` carries ONLY the enum\n * `'open' | 'close'` — never a close code, host, relay URL, or TOTP value.\n */\nfunction broadcastRelayWsState(state: 'open' | 'close'): void {\n if (typeof window === 'undefined') return;\n window.dispatchEvent(new CustomEvent(RELAY_WS_STATE_EVENT, { detail: { state } }));\n}\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// Graceful detach — return the mini-app to a clean, usable state on run end\n// (issue #748).\n//\n// Real-device observation (run7): after an env3 run completes the phone showed\n// a persistent \"Debugger Disconnected\" badge and the app felt wedged. This is\n// the in-app half of the fix — a single idempotent teardown that removes OUR\n// debug-surface elements (the CDP-injected indicator badge, the eruda console)\n// and restores the keepAwake side effect, so nothing we injected lingers after\n// the relay session ends.\n//\n// Trigger model (all close paths, transient-safe):\n// - Normal completion / relay death / tunnel drop that does NOT recover /\n// dashboard-initiated stop → relay WS closes (any non-4401 code). We\n// schedule teardown after a grace window; a reconnect 'open' cancels it, so\n// a transient tunnel blip that target.js recovers from does NOT flash away\n// the surface.\n// - 4401 (relay TOTP auth-reject, #478) → terminal by design (the session\n// cannot reconnect without a QR rescan), so we tear down immediately.\n// - WS 'error' with no clean close → scheduled defensively (unclean-close\n// path).\n// - pagehide → immediate (navigating away; restores keepAwake even if no\n// close fired).\n//\n// OUT OF OUR LAYER (issue #748 hypothesis (a)): a native Toss-app loading\n// overlay / spinner that absorbs touches is NOT something a JS surface can\n// dismiss — we never try. Our surface adds no full-viewport element, no\n// capture-phase document listener, and no body pointer-events/scroll lock, so\n// it structurally cannot absorb ALL input; the total-touch-absorb + spinner\n// symptom is native and stays device-gated (tracked in #748 / sdk-example#277).\n// ---------------------------------------------------------------------------\n\n/** Grace window before a non-terminal relay close tears the surface down. */\nconst RECONNECT_GRACE_MS = 5000;\n\n/** One-shot guard so the teardown runs at most once per page lifecycle. */\nlet debugSurfaceDetached = false;\n\n/** Pending grace-window timer for a non-terminal close, or `null`. */\nlet pendingDetachTimer: ReturnType<typeof setTimeout> | null = null;\n\n/** Cancels a scheduled teardown — called when a relay socket re-opens. */\nfunction cancelScheduledDetach(): void {\n if (pendingDetachTimer !== null) {\n clearTimeout(pendingDetachTimer);\n pendingDetachTimer = null;\n }\n}\n\n/**\n * Schedules {@link detachDebugSurface} after {@link RECONNECT_GRACE_MS} unless\n * a reconnect cancels it first. No-op if teardown already ran or is already\n * scheduled. Defensive: if `setTimeout` is somehow unavailable, tears down\n * immediately rather than never.\n */\nfunction scheduleDetach(): void {\n if (debugSurfaceDetached || pendingDetachTimer !== null) return;\n if (typeof setTimeout === 'undefined') {\n detachDebugSurface();\n return;\n }\n pendingDetachTimer = setTimeout(() => {\n pendingDetachTimer = null;\n detachDebugSurface();\n }, RECONNECT_GRACE_MS);\n}\n\n/**\n * Idempotent, non-throwing teardown of the in-app debug surface (issue #748).\n *\n * Removes every debug-surface element WE injected and restores the one side\n * effect WE applied:\n * 1. The CDP-injected `#__ait_debug_indicator` badge (the persistent\n * \"Debugger Disconnected\" element). Its live heartbeat/pending-call timer\n * (#749) is stopped first via the badge controller's `stop()` so no 1 Hz\n * interval leaks past detach; `buildIndicatorExpression` also\n * self-dismisses the node — this is the in-app hard guarantee, idempotent,\n * a no-op if it is already gone.\n * 2. The eruda in-page console (floating button + any open panel).\n * 3. The native-bridge call observer (#749) — its `callAsyncMethod` /\n * `postMessage` / emitter wraps are restored and `window.__ait_bridge` is\n * removed, so nothing we wrapped survives the run's end.\n * 4. keepAwake — forced on at attach; restored here so a run that ends\n * WITHOUT a page unload does not leave the screen pinned awake (the\n * existing `beforeunload` restore only covers the unload path).\n *\n * Deliberately NOT touched: the `window.WebSocket` observer proxy (kept so the\n * #478 post-4401 fail-fast survives; it is non-blocking and never absorbs\n * input), and any NATIVE overlay (out of our layer — see the block comment\n * above and issue #748 hypothesis (a)).\n *\n * Never throws into the host app — every step is individually guarded.\n * Exported for unit tests and for a consumer that wants to force a clean detach.\n */\nexport function detachDebugSurface(): void {\n if (debugSurfaceDetached) return;\n debugSurfaceDetached = true;\n cancelScheduledDetach();\n\n // 1. Stop the badge's heartbeat/pending-call interval (#749) BEFORE removing\n // the node, then remove the on-phone indicator badge if it is still present.\n // The controller (`window.__ait_indicator`) exposes `stop()`; the optional\n // chaining makes this a no-op for a bare pre-#749 badge or when absent.\n try {\n if (typeof window !== 'undefined') {\n (window as unknown as { __ait_indicator?: { stop?: () => void } }).__ait_indicator?.stop?.();\n }\n if (typeof document !== 'undefined') {\n document.getElementById('__ait_debug_indicator')?.remove();\n }\n } catch {\n // Never let a DOM edge case break teardown.\n }\n\n // 2. Tear down the eruda console (unmountEruda is itself fail-silent).\n try {\n unmountEruda();\n } catch {\n // unmountEruda already swallows internally; this is belt-and-suspenders.\n }\n\n // 3. Restore the native-bridge call observer wraps and drop the snapshot\n // (#749) — never throws (uninstallBridgeObserver guards each undo).\n try {\n uninstallBridgeObserver();\n } catch {\n // Belt-and-suspenders — uninstall is already internally guarded.\n }\n\n // 4. Restore normal screen sleep. Runtime probe — a no-op when the SDK is\n // absent or predates the API (see sdk-probe.ts). The synchronous form comes\n // first because one caller of this function is the `pagehide` handler, and a\n // page being torn down owes us no microtask turn: after a successful attach\n // the probe is already warm, so the release is dispatched in this tick. The\n // async form is the fallback for a teardown that runs before any probe has\n // resolved. SECRET-HANDLING: no relay/TOTP value is read or logged here —\n // this only flips the awake flag.\n try {\n if (!setScreenAwakeNow(false)) void setScreenAwake(false);\n } catch {\n // Neither form throws, but keep teardown bullet-proof.\n }\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 // #730: signal to the page that relay-WS lifecycle is already observed, so\n // a CDP-injected debug indicator can subscribe to `ait:relay-ws-state`\n // instead of installing a second Proxy on window.WebSocket.\n window.__ait_relay_ws_observed = true;\n\n // #748: beforeunload-safe teardown — navigating away restores keepAwake and\n // clears the surface even if no WS close fired. Registered ONLY here (inside\n // the observer install, which runs only after a debug attach), so there is\n // zero behavior change when no debug attach happened. Idempotent + fail-safe.\n window.addEventListener('pagehide', () => detachDebugSurface(), { once: 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 // #730: broadcast generic open/close lifecycle (any close code) so the\n // debug indicator can flip its live badge — additive to the existing\n // 4401-specific branch below, which is untouched.\n ws.addEventListener('open', () => {\n broadcastRelayWsState('open');\n // #748: a (re)connect aborts any pending graceful-detach — a transient\n // tunnel blip that target.js recovers from must not tear the surface down.\n cancelScheduledDetach();\n });\n ws.addEventListener('close', (event) => {\n broadcastRelayWsState('close');\n if ((event as CloseEvent).code === RELAY_AUTH_REJECT_CLOSE_CODE) {\n relayAuthExpired = true;\n notifyAuthExpired();\n // #748: 4401 is terminal by design (no reconnect without a QR rescan)\n // — tear down immediately, no grace window.\n detachDebugSurface();\n } else {\n // #748: any other close may be transient — schedule teardown after a\n // grace window; a reconnect 'open' cancels it.\n scheduleDetach();\n }\n });\n // #748: an error that never emits a clean close still needs teardown\n // (unclean-close path) — schedule defensively; a recovery 'open' cancels it.\n ws.addEventListener('error', () => {\n scheduleDetach();\n });\n return ws;\n },\n });\n window.WebSocket = observed as typeof WebSocket;\n}\n\n/**\n * The webViewType self-report postMessage type (#580).\n *\n * Canonical definition + the receive-side parser live in\n * `src/mock/safe-area-bridge.ts` (`WEB_VIEW_TYPE_MESSAGE_TYPE`,\n * `parseWebViewTypeMessage`). It is re-declared here as a local literal so the\n * in-app entry does NOT import the mock barrel (which would drag mock internals\n * — navigation/state — into the dogfood in-app graph). The two literals are\n * kept in sync by value; if one changes, change both. Same decoupling pattern\n * the launcher fixture uses for its message-type constants.\n */\nconst WEB_VIEW_TYPE_MESSAGE_TYPE = 'ait:web-view-type' as const;\n\n/** Guard so the webViewType self-report is posted at most once per page. */\nlet webViewTypeReported = false;\n\n/**\n * Self-report the mini-app's webViewType to the parent launcher shell, ONCE\n * (#580).\n *\n * The mini-app's type is the build constant `__WEB_VIEW_TYPE__`, injected by\n * the devtools unplugin from `granite.config.ts`'s `webViewProps.type`. The\n * launcher (env-2 PWA) is cross-origin and cannot read it directly, so the\n * framed page posts it to `window.parent`; the launcher switches to game mode\n * automatically (no manual `?navBarType=game` URL edit).\n *\n * Defensive by construction — must NEVER break attach:\n * - `__WEB_VIEW_TYPE__` is a CONSUMER-build define; it does not exist in\n * devtools' own build or where the unplugin did not inject it. The `typeof`\n * guard avoids a ReferenceError; an absent constant is a silent no-op.\n * - Only posts when inside an iframe (`window.parent !== window`) — a\n * top-level load has no launcher shell to receive the message.\n * - The SDK's deprecated `'external'` alias of `partner` (web-framework 2.6.1)\n * is mapped to `'partner'`; the launcher only emulates `partner` | `game`.\n * - Wrapped in try/catch so any postMessage/iframe edge case is swallowed.\n *\n * SECRET-HANDLING: the payload carries ONLY the webViewType enum — no host,\n * relay URL, code, or secret.\n */\nexport function reportWebViewType(): void {\n if (webViewTypeReported) return;\n try {\n if (typeof window === 'undefined' || window.parent === window) return;\n // `typeof` guard: the define is absent in devtools' own build and wherever\n // the unplugin did not inject it — a bare read would throw ReferenceError.\n const raw = typeof __WEB_VIEW_TYPE__ !== 'undefined' ? __WEB_VIEW_TYPE__ : undefined;\n if (raw === undefined) return;\n // Map the deprecated 'external' alias onto 'partner'; the launcher only\n // knows the two shapes it emulates. Anything unexpected → no report.\n const value: 'partner' | 'game' | null =\n raw === 'game' ? 'game' : raw === 'partner' || raw === 'external' ? 'partner' : null;\n if (value === null) return;\n webViewTypeReported = true;\n window.parent.postMessage({ type: WEB_VIEW_TYPE_MESSAGE_TYPE, value }, '*');\n } catch {\n // Never let the self-report break attach — swallow any iframe/postMessage\n // edge case silently (no log: a missing define on plain loads is expected).\n }\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 // #580: self-report the mini-app's webViewType to the launcher shell once,\n // independent of the gate outcome — the launcher auto-enters game mode for\n // a game-type mini-app. No-op outside an iframe / when the define is absent.\n reportWebViewType();\n\n if (!gateResult.attach) {\n console.debug(\n `[@ait-co/debug-console] 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 // Version handshake: tell the daemon which build of this package is attaching\n // so a device↔host skew becomes one diagnostic line instead of a silent\n // misbehaviour (see @ait-co/internal-protocol/attach-handshake). Fire-and-\n // forget — never awaited, never able to block or fail an attach.\n reportProtocolVersion(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 // Issue #749: observe the REAL native bridge (env 3) so the indicator can\n // show in-flight SDK calls + a last-call stamp — the mock's sdkCallLog does\n // not see the real SDK here. SECRET-HANDLING: records API names + timings\n // only, never arguments/results. Fail-safe: never throws into attach.\n installBridgeObserver();\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 // Mount the eruda in-page console alongside the Chii remote transport. Same\n // post-gate point, same debug session — Chii relays CDP to the PC frontend,\n // eruda shows the console on the phone itself. Fire-and-forget: eruda loads\n // lazily and fail-silent, so a slow or absent eruda never blocks the Chii\n // attach or the keepAwake step below. See eruda-overlay.ts for the build-time\n // absence + gate-inheritance contract.\n void mountEruda();\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 // Runtime probe rather than a static import: `setScreenAwakeMode` does not\n // exist on `@apps-in-toss/web-framework` 2.x's web surface at all, so a\n // static import would break every 2.x consumer — and it would drag the SDK\n // into this package's graph, which must stay `eruda`-only (see sdk-probe.ts).\n void setScreenAwake(true).then((held) => {\n // Restore normal sleep on page unload — only if the enable call landed\n // (nothing to restore when the API was unavailable or rejected).\n if (!held) return;\n window.addEventListener(\n 'beforeunload',\n () => {\n // Synchronous: the probe is warm by construction here (the enable call\n // above is what warmed it), and an unloading page may not run another\n // microtask turn. See sdk-probe.ts's `getLoadedTossSdk`.\n if (!setScreenAwakeNow(false)) void setScreenAwake(false);\n },\n { once: true },\n );\n });\n}\n","/**\n * @ait-co/debug-console 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/debug-console')` 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 {\n deriveTargetScriptUrl,\n detachDebugSurface,\n maybeAttach,\n reportWebViewType,\n} from './attach.js';\nexport {\n BRIDGE_CALL_EVENT,\n type BridgeLastCall,\n type BridgeObserverState,\n type BridgePendingCall,\n installBridgeObserver,\n uninstallBridgeObserver,\n} from './bridge-observer.js';\nexport { mountEruda, unmountEruda } from './eruda-overlay.js';\nexport type { GateInput, GateResult, GateResultAttach, GateResultBlocked } from './gate.js';\nexport {\n evaluateDebugGate,\n isPrivateAppsHost,\n isTossminiHost,\n isTrycloudflareHost,\n} from './gate.js';\n\n/**\n * Evaluates the runtime debug activation layers (B and C) against the current\n * page URL.\n *\n * Returns the gate result. Callers can check `result.attach` to decide whether\n * to proceed with debug surface attachment.\n *\n * This function reads `window.location` only — both the hostname (Layer B1\n * host allowlist) and the search params (Layers B2 and C). Layer A\n * (build-time) is enforced by the consumer's `if (__DEBUG_BUILD__)` guard\n * around the import site, not here — see the file-level comment. Consumers\n * call this with no arguments, so the Layer B1 host check is picked up with\n * no change at the call site.\n */\nexport function checkDebugGate(): GateResult {\n return evaluateDebugGate({\n hostname: window.location.hostname,\n searchParams: new URLSearchParams(window.location.search),\n });\n}\n"],"mappings":"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"}