@ait-co/devtools 0.1.107 → 0.1.108

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.
Files changed (54) hide show
  1. package/dist/bundle-BJm5jk56.d.ts +49 -0
  2. package/dist/bundle-BJm5jk56.d.ts.map +1 -0
  3. package/dist/cdp-connection-C0AP0tH2.d.ts +277 -0
  4. package/dist/cdp-connection-C0AP0tH2.d.ts.map +1 -0
  5. package/dist/in-app/auto.d.ts +17 -0
  6. package/dist/in-app/auto.d.ts.map +1 -1
  7. package/dist/in-app/auto.js +76 -0
  8. package/dist/in-app/auto.js.map +1 -1
  9. package/dist/in-app/index.d.ts +48 -1
  10. package/dist/in-app/index.d.ts.map +1 -1
  11. package/dist/in-app/index.js +60 -1
  12. package/dist/in-app/index.js.map +1 -1
  13. package/dist/mcp/cli.js +502 -6
  14. package/dist/mcp/cli.js.map +1 -1
  15. package/dist/mcp/server.d.ts.map +1 -1
  16. package/dist/mcp/server.js +59 -2
  17. package/dist/mcp/server.js.map +1 -1
  18. package/dist/panel/index.js +1 -1
  19. package/dist/pool-Dkp7I9Bf.d.ts +14577 -0
  20. package/dist/pool-Dkp7I9Bf.d.ts.map +1 -0
  21. package/dist/relay-worker-BzFQ3fv9.d.ts +74 -0
  22. package/dist/relay-worker-BzFQ3fv9.d.ts.map +1 -0
  23. package/dist/runtime-ORdrpizY.d.ts +50 -0
  24. package/dist/runtime-ORdrpizY.d.ts.map +1 -0
  25. package/dist/test-runner/bundle.d.ts +2 -0
  26. package/dist/test-runner/bundle.js +95 -0
  27. package/dist/test-runner/bundle.js.map +1 -0
  28. package/dist/test-runner/cli.d.ts +417 -0
  29. package/dist/test-runner/cli.d.ts.map +1 -0
  30. package/dist/test-runner/cli.js +377 -0
  31. package/dist/test-runner/cli.js.map +1 -0
  32. package/dist/test-runner/config.d.ts +80 -0
  33. package/dist/test-runner/config.d.ts.map +1 -0
  34. package/dist/test-runner/config.js +54 -0
  35. package/dist/test-runner/config.js.map +1 -0
  36. package/dist/test-runner/pool.d.ts +2 -0
  37. package/dist/test-runner/pool.js +136 -0
  38. package/dist/test-runner/pool.js.map +1 -0
  39. package/dist/test-runner/relay-worker.d.ts +2 -0
  40. package/dist/test-runner/relay-worker.js +96 -0
  41. package/dist/test-runner/relay-worker.js.map +1 -0
  42. package/dist/test-runner/rpc.d.ts +53 -0
  43. package/dist/test-runner/rpc.d.ts.map +1 -0
  44. package/dist/test-runner/rpc.js +78 -0
  45. package/dist/test-runner/rpc.js.map +1 -0
  46. package/dist/test-runner/task-graph.d.ts +38 -0
  47. package/dist/test-runner/task-graph.d.ts.map +1 -0
  48. package/dist/test-runner/task-graph.js +182 -0
  49. package/dist/test-runner/task-graph.js.map +1 -0
  50. package/dist/unplugin/index.d.cts +13 -32
  51. package/dist/unplugin/index.d.cts.map +1 -1
  52. package/dist/unplugin/index.d.ts +13 -32
  53. package/dist/unplugin/index.d.ts.map +1 -1
  54. package/package.json +13 -3
@@ -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, reportWebViewType } 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 * 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/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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"}
1
+ {"version":3,"file":"auto.js","names":[],"sources":["../../src/shared/relay-auth-close.ts","../../src/in-app/eruda-overlay.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 * 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/devtools/in-app` graph. A consumer wraps its\n * `import('@ait-co/devtools/in-app')` 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 * 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 } catch (err) {\n // Reset so a later attach can retry; never break the Chii session.\n erudaMounted = false;\n console.debug('[@ait-co/devtools] eruda console mount skipped:', err);\n }\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/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, reportWebViewType } from './attach.js';\nexport { mountEruda } from './eruda-overlay.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 { mountEruda } from './eruda-overlay.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 * 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/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 // 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 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 * DEPRECATED for builds that require debug code to be PHYSICALLY ABSENT from\n * the release bundle. This entry is a RUNTIME self-gate, not a build-time one:\n * the imported chunk (Chii target.js injection, the SDK bridge, and the eruda\n * console it pulls in via `maybeAttach()`) stays in the production bundle as a\n * dormant chunk and is only kept asleep at runtime. If your threat model needs\n * \"zero bytes of debug surface in release\" (no dormant chunk to extract or\n * re-enable), do NOT use this entry. Instead guard the call site yourself:\n *\n * if (__DEBUG_BUILD__) {\n * import('@ait-co/devtools/in-app').then((m) => m.maybeAttach());\n * }\n *\n * with `define: { __DEBUG_BUILD__: 'false' }` in your release build — the\n * bundler then dead-code-eliminates the whole `@ait-co/devtools/in-app` graph\n * (verified on Vite 8/rolldown). This entry stays for the convenience case\n * where a dormant chunk gated at runtime is acceptable.\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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"}
@@ -293,6 +293,53 @@ declare function reportWebViewType(): void;
293
293
  */
294
294
  declare function maybeAttach(gateResult?: GateResult): void;
295
295
  //#endregion
296
+ //#region src/in-app/eruda-overlay.d.ts
297
+ /**
298
+ * In-app eruda console overlay for the debug attach flow.
299
+ *
300
+ * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md
301
+ *
302
+ * This module mounts the eruda in-page console (https://github.com/liriliri/eruda)
303
+ * on the phone screen when a debug session attaches. It is the mobile-only
304
+ * counterpart to the Chii `target.js` injection in {@link attach.ts}: Chii is a
305
+ * REMOTE CDP transport (phone → relay → PC DevTools frontend), whereas eruda is
306
+ * a LOCAL in-page view — a floating button + console/network/DOM/storage panels
307
+ * rendered directly on the phone, with no relay or second device. The two are
308
+ * orthogonal and coexist (eruda opens no WebSocket, mounts into its own
309
+ * `#eruda` shadow host — it cannot collide with the relay WS or the Chii DOM).
310
+ *
311
+ * Build-time absence (the security contract): this module lives in the
312
+ * `@ait-co/devtools/in-app` graph. A consumer wraps its
313
+ * `import('@ait-co/devtools/in-app')` call site in `if (__DEBUG_BUILD__) { … }`;
314
+ * a release build folds that constant to `false` and dead-code-eliminates the
315
+ * whole module — so eruda (and its dynamic `import('eruda')` chunk) is simply
316
+ * absent from release bundles, exactly like the Chii target.js injection. The
317
+ * `import('eruda')` here is a dynamic import precisely so the bundler emits it
318
+ * as a separate chunk that the dead branch never pulls in.
319
+ *
320
+ * Runtime gate: `mountEruda()` is called only from `maybeAttach()` AFTER the
321
+ * full Layer B/C gate has passed (`gateResult.attach === true`) — host
322
+ * allowlist, `debug=1`, relay URL, and TOTP. So eruda inherits the same
323
+ * four-layer defence as the Chii injection, byte-for-byte, with no eruda-
324
+ * specific gate of its own.
325
+ *
326
+ * SECRET-HANDLING: this module reads no secret, TOTP code, relay URL, or host
327
+ * value, and logs none. eruda observes only the page it is mounted on.
328
+ */
329
+ /**
330
+ * Mounts the eruda in-page console once.
331
+ *
332
+ * Idempotent: repeated calls after a successful mount are no-ops, mirroring the
333
+ * `attached` guard in {@link attach.ts}. Fail-silent: if the dynamic import or
334
+ * `eruda.init()` throws (eruda absent, or a runtime that rejects it), the Chii
335
+ * debug session is unaffected — eruda is an additive convenience, not a
336
+ * dependency of the relay path.
337
+ *
338
+ * `eruda.init()` mounts eruda's own floating entry button on the phone screen;
339
+ * tapping it opens the console. We do not add a separate button.
340
+ */
341
+ declare function mountEruda(): Promise<void>;
342
+ //#endregion
296
343
  //#region src/in-app/index.d.ts
297
344
  /**
298
345
  * Evaluates the runtime debug activation layers (B and C) against the current
@@ -310,5 +357,5 @@ declare function maybeAttach(gateResult?: GateResult): void;
310
357
  */
311
358
  declare function checkDebugGate(): GateResult;
312
359
  //#endregion
313
- export { type GateInput, type GateResult, type GateResultAttach, type GateResultBlocked, checkDebugGate, deriveTargetScriptUrl, evaluateDebugGate, isPrivateAppsHost, isTrycloudflareHost, maybeAttach, reportWebViewType };
360
+ export { type GateInput, type GateResult, type GateResultAttach, type GateResultBlocked, checkDebugGate, deriveTargetScriptUrl, evaluateDebugGate, isPrivateAppsHost, isTrycloudflareHost, maybeAttach, mountEruda, reportWebViewType };
314
361
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/in-app/gate.ts","../../src/in-app/attach.ts","../../src/in-app/index.ts"],"mappings":";;AA4EA;;;;;;;;;AASA;;;;;AAmBA;;;;;AAQA;;;;;;;;;;;AA8EA;;;;;AAuBA;;;;;AAyBA;;;;;;;;;;;;AC1LA;;;;;AAuNA;;;;;AA4CA;;;;;;;;AC3QA;;;;UFgCiB,gBAAA;EAAA,SACN,MAAA;;WAEA,QAAA;;WAEA,YAAA;AAAA;;UAIM,iBAAA;EAAA,SACN,MAAA;;;;;;;;;;;;;;;WAeA,MAAA;AAAA;AAAA,KAGC,UAAA,GAAa,gBAAA,GAAmB,iBAAA;;;;;;;UAQ3B,SAAA;;;;;;;;;;;;;WAaN,QAAA;;;;;;;WAQA,YAAA,EAAc,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;WA0Bd,cAAA,IAAkB,IAAA;AAAA;;;;;;;;;;;;iBA+Bb,iBAAA,CAAkB,QAAA;;;;;;;;;;;;;;;;;;;;iBAuBlB,mBAAA,CAAoB,QAAA;;;;;;;;;;;;;;;;;;;;;;iBAyBpB,iBAAA,CAAkB,KAAA,EAAO,SAAA,GAAY,UAAA;;;;;;AAtIrD;;;;;AAQA;;;;;;;;;;;AA8EA;;;;;AAuBA;;;;;AAyBA;;;;iBC1LgB,qBAAA,CAAsB,QAAA,UAAkB,MAAA;;AAmQxD;;;;;;;;AC3QA;;;;;;;;;;;;;;iBD+NgB,iBAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;iBA4CA,WAAA,CAAY,UAAA,GAAY,UAAA;;;;;;;ADzHxC;;;;;AAuBA;;;;;iBEzKgB,cAAA,CAAA,GAAkB,UAAA"}
1
+ {"version":3,"file":"index.d.ts","names":[],"sources":["../../src/in-app/gate.ts","../../src/in-app/attach.ts","../../src/in-app/eruda-overlay.ts","../../src/in-app/index.ts"],"mappings":";;AA4EA;;;;;;;;;AASA;;;;;AAmBA;;;;;AAQA;;;;;;;;;;;AA8EA;;;;;AAuBA;;;;;AAyBA;;;;;;;;;;;;ACzLA;;;;;AAuNA;;;;;AA4CA;;;;;;;;ACxQA;;;;UF4BiB,gBAAA;EAAA,SACN,MAAA;;WAEA,QAAA;EGlCK;EAAA,SHoCL,YAAA;AAAA;;UAIM,iBAAA;EAAA,SACN,MAAA;;;;;;;;;;;;;;;WAeA,MAAA;AAAA;AAAA,KAGC,UAAA,GAAa,gBAAA,GAAmB,iBAAA;;;;;;;UAQ3B,SAAA;;;;;;;;;;;;;WAaN,QAAA;;;;;;;WAQA,YAAA,EAAc,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;WA0Bd,cAAA,IAAkB,IAAA;AAAA;;;;;;;;;;;;iBA+Bb,iBAAA,CAAkB,QAAA;;;;;;;;;;;;;;;;;;;;iBAuBlB,mBAAA,CAAoB,QAAA;;;;;;;;;;;;;;;;;;;;;;iBAyBpB,iBAAA,CAAkB,KAAA,EAAO,SAAA,GAAY,UAAA;;;;;;AAtIrD;;;;;AAQA;;;;;;;;;;;AA8EA;;;;;AAuBA;;;;;AAyBA;;;;iBCzLgB,qBAAA,CAAsB,QAAA,UAAkB,MAAA;;AAmQxD;;;;;;;;ACxQA;;;;;;;;ACHA;;;;;;iBF+NgB,iBAAA,CAAA;;;;;;;;;;;;;;;;;;;;;;;;iBA4CA,WAAA,CAAY,UAAA,GAAY,UAAA;;;;AD5OxC;;;;;;;;;AASA;;;;;AAmBA;;;;;AAQA;;;;;;;;;;;AA8EA;;;;;AAuBA;;;;;AAyBA;;;iBE9LsB,UAAA,CAAA,GAAc,OAAA;;;;;;AF8IpC;;;;;AAuBA;;;;;AAyBA;iBGjMgB,cAAA,CAAA,GAAkB,UAAA"}
@@ -163,6 +163,64 @@ const RELAY_AUTH_REJECT_CLOSE_CODE = 4401;
163
163
  */
164
164
  const RELAY_AUTH_REJECT_REASON = "totp-rejected";
165
165
  //#endregion
166
+ //#region src/in-app/eruda-overlay.ts
167
+ /**
168
+ * In-app eruda console overlay for the debug attach flow.
169
+ *
170
+ * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md
171
+ *
172
+ * This module mounts the eruda in-page console (https://github.com/liriliri/eruda)
173
+ * on the phone screen when a debug session attaches. It is the mobile-only
174
+ * counterpart to the Chii `target.js` injection in {@link attach.ts}: Chii is a
175
+ * REMOTE CDP transport (phone → relay → PC DevTools frontend), whereas eruda is
176
+ * a LOCAL in-page view — a floating button + console/network/DOM/storage panels
177
+ * rendered directly on the phone, with no relay or second device. The two are
178
+ * orthogonal and coexist (eruda opens no WebSocket, mounts into its own
179
+ * `#eruda` shadow host — it cannot collide with the relay WS or the Chii DOM).
180
+ *
181
+ * Build-time absence (the security contract): this module lives in the
182
+ * `@ait-co/devtools/in-app` graph. A consumer wraps its
183
+ * `import('@ait-co/devtools/in-app')` call site in `if (__DEBUG_BUILD__) { … }`;
184
+ * a release build folds that constant to `false` and dead-code-eliminates the
185
+ * whole module — so eruda (and its dynamic `import('eruda')` chunk) is simply
186
+ * absent from release bundles, exactly like the Chii target.js injection. The
187
+ * `import('eruda')` here is a dynamic import precisely so the bundler emits it
188
+ * as a separate chunk that the dead branch never pulls in.
189
+ *
190
+ * Runtime gate: `mountEruda()` is called only from `maybeAttach()` AFTER the
191
+ * full Layer B/C gate has passed (`gateResult.attach === true`) — host
192
+ * allowlist, `debug=1`, relay URL, and TOTP. So eruda inherits the same
193
+ * four-layer defence as the Chii injection, byte-for-byte, with no eruda-
194
+ * specific gate of its own.
195
+ *
196
+ * SECRET-HANDLING: this module reads no secret, TOTP code, relay URL, or host
197
+ * value, and logs none. eruda observes only the page it is mounted on.
198
+ */
199
+ /** Module-level guard against double mount across repeated `maybeAttach` calls. */
200
+ let erudaMounted = false;
201
+ /**
202
+ * Mounts the eruda in-page console once.
203
+ *
204
+ * Idempotent: repeated calls after a successful mount are no-ops, mirroring the
205
+ * `attached` guard in {@link attach.ts}. Fail-silent: if the dynamic import or
206
+ * `eruda.init()` throws (eruda absent, or a runtime that rejects it), the Chii
207
+ * debug session is unaffected — eruda is an additive convenience, not a
208
+ * dependency of the relay path.
209
+ *
210
+ * `eruda.init()` mounts eruda's own floating entry button on the phone screen;
211
+ * tapping it opens the console. We do not add a separate button.
212
+ */
213
+ async function mountEruda() {
214
+ if (erudaMounted || typeof document === "undefined") return;
215
+ erudaMounted = true;
216
+ try {
217
+ (await import("eruda")).default.init();
218
+ } catch (err) {
219
+ erudaMounted = false;
220
+ console.debug("[@ait-co/devtools] eruda console mount skipped:", err);
221
+ }
222
+ }
223
+ //#endregion
166
224
  //#region src/in-app/attach.ts
167
225
  /**
168
226
  * In-app Chii target injection for the debug attach flow.
@@ -454,6 +512,7 @@ function maybeAttach(gateResult = checkDebugGate()) {
454
512
  };
455
513
  (document.head ?? document.documentElement).appendChild(script);
456
514
  attached = true;
515
+ mountEruda();
457
516
  if (typeof window !== "undefined" && new URLSearchParams(window.location.search).get("noKeepAwake") === "1") return;
458
517
  setScreenAwakeMode({ enabled: true }).then(() => {
459
518
  window.addEventListener("beforeunload", () => {
@@ -509,6 +568,6 @@ function checkDebugGate() {
509
568
  });
510
569
  }
511
570
  //#endregion
512
- export { checkDebugGate, deriveTargetScriptUrl, evaluateDebugGate, isPrivateAppsHost, isTrycloudflareHost, maybeAttach, reportWebViewType };
571
+ export { checkDebugGate, deriveTargetScriptUrl, evaluateDebugGate, isPrivateAppsHost, isTrycloudflareHost, maybeAttach, mountEruda, reportWebViewType };
513
572
 
514
573
  //# sourceMappingURL=index.js.map