@ait-co/devtools 0.1.32 → 0.1.33
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/in-app/index.d.ts +68 -14
- package/dist/in-app/index.d.ts.map +1 -1
- package/dist/in-app/index.js +7 -0
- package/dist/in-app/index.js.map +1 -1
- package/dist/mcp/cli.js +226 -26
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.d.ts +9 -0
- package/dist/mcp/server.d.ts.map +1 -1
- package/dist/mcp/server.js +53 -2
- package/dist/mcp/server.js.map +1 -1
- package/dist/mock/index.d.ts +124 -20
- package/dist/mock/index.d.ts.map +1 -1
- package/dist/mock/index.js +293 -51
- package/dist/mock/index.js.map +1 -1
- package/dist/panel/index.js +503 -67
- package/dist/panel/index.js.map +1 -1
- package/package.json +1 -1
package/dist/in-app/index.d.ts
CHANGED
|
@@ -27,14 +27,38 @@
|
|
|
27
27
|
* on a production entry — see the comment on {@link isPrivateAppsHost}.
|
|
28
28
|
* B2 — entry query: `_deploymentId` must be present and non-empty.
|
|
29
29
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
30
|
+
* Layer C — opt-in + relay + optional TOTP auth:
|
|
31
|
+
* C1 — opt-in: `debug=1` must be present.
|
|
32
|
+
* C2 — relay URL: `relay=<wss-url>` must be a valid `wss:` URL.
|
|
33
|
+
* C3 — TOTP auth: When `verifyTotpCode` is provided (consumer injected the
|
|
34
|
+
* baked secret at build time via `__DEBUG_TOTP_SECRET__`),
|
|
35
|
+
* `at=<code>` is checked. Invalid or absent code → BLOCKED.
|
|
36
|
+
* When no verifier is provided (TOTP disabled), `at` is
|
|
37
|
+
* ignored (backward compatible).
|
|
38
|
+
*
|
|
39
|
+
* Security note on baked secrets:
|
|
40
|
+
* The TOTP secret baked in via `__DEBUG_TOTP_SECRET__` is present in the
|
|
41
|
+
* dogfood bundle and is extractable by a determined reverse engineer.
|
|
42
|
+
* The practical bar raised is: "URL leak" (Slack paste, QR screenshot) →
|
|
43
|
+
* blocked; "URL + bundle extraction + live TOTP code" → not blocked.
|
|
44
|
+
* This is the intended threat model. Do not overpromise on this guarantee.
|
|
45
|
+
*
|
|
46
|
+
* SECRET-HANDLING: `verifyTotpCode` is a black-box predicate. This module
|
|
47
|
+
* does NOT log the secret, any code value, or pass/fail details beyond the
|
|
48
|
+
* `'auth'` reason enum.
|
|
49
|
+
*
|
|
50
|
+
* Decision matrix (gate only runs in a debug build — Layer A already passed):
|
|
51
|
+
*
|
|
52
|
+
* host ok | _deploymentId | debug=1 | relay ok | TOTP ok* | result
|
|
53
|
+
* no | (any) | (any) | (any) | (any) | BLOCKED (host)
|
|
54
|
+
* yes | absent | (any) | (any) | (any) | BLOCKED (entry)
|
|
55
|
+
* yes | present | absent | (any) | (any) | BLOCKED (opt-in)
|
|
56
|
+
* yes | present | present | invalid | (any) | BLOCKED (invalid-relay)
|
|
57
|
+
* yes | present | present | valid | fail* | BLOCKED (auth)
|
|
58
|
+
* yes | present | present | valid | pass/n/a | ATTACH
|
|
59
|
+
*
|
|
60
|
+
* * "TOTP ok" column only applies when `verifyTotpCode` is provided.
|
|
61
|
+
* When no verifier is injected, TOTP check is skipped entirely.
|
|
38
62
|
*/
|
|
39
63
|
/** Shape returned when the gate allows attachment. */
|
|
40
64
|
interface GateResultAttach {
|
|
@@ -48,21 +72,26 @@ interface GateResultAttach {
|
|
|
48
72
|
interface GateResultBlocked {
|
|
49
73
|
readonly attach: false;
|
|
50
74
|
/**
|
|
51
|
-
* - `'host'`
|
|
52
|
-
* - `'entry'`
|
|
53
|
-
* - `'opt-in'`
|
|
54
|
-
* - `'invalid-relay'` Layer
|
|
75
|
+
* - `'host'` Layer B1: `hostname` is not a `*.private-apps.tossmini.com` host.
|
|
76
|
+
* - `'entry'` Layer B2: `_deploymentId` param is absent or empty.
|
|
77
|
+
* - `'opt-in'` Layer C1: `debug=1` param is absent.
|
|
78
|
+
* - `'invalid-relay'` Layer C2: `relay` param is absent, empty, or not a `wss:` URL.
|
|
79
|
+
* - `'auth'` Layer C3: TOTP `at=` code is absent, invalid, or expired
|
|
80
|
+
* (only when a `verifyTotpCode` predicate is injected).
|
|
55
81
|
*
|
|
56
82
|
* There is no `'build'` reason: Layer A is enforced by the consumer's
|
|
57
83
|
* `if (__DEBUG_BUILD__)` guard, not by this function.
|
|
84
|
+
*
|
|
85
|
+
* SECRET-HANDLING: `'auth'` is the only value surfaced for auth failures —
|
|
86
|
+
* no code value, expected value, or secret fragment is ever exposed.
|
|
58
87
|
*/
|
|
59
|
-
readonly reason: 'host' | 'entry' | 'opt-in' | 'invalid-relay';
|
|
88
|
+
readonly reason: 'host' | 'entry' | 'opt-in' | 'invalid-relay' | 'auth';
|
|
60
89
|
}
|
|
61
90
|
type GateResult = GateResultAttach | GateResultBlocked;
|
|
62
91
|
/**
|
|
63
92
|
* Input for {@link evaluateDebugGate}.
|
|
64
93
|
*
|
|
65
|
-
*
|
|
94
|
+
* All fields are explicit so the function is trivially testable without
|
|
66
95
|
* touching `window`.
|
|
67
96
|
*/
|
|
68
97
|
interface GateInput {
|
|
@@ -86,6 +115,31 @@ interface GateInput {
|
|
|
86
115
|
* without coupling the pure function to `window`.
|
|
87
116
|
*/
|
|
88
117
|
readonly searchParams: URLSearchParams;
|
|
118
|
+
/**
|
|
119
|
+
* Optional TOTP code verifier for Layer C3 auth gate.
|
|
120
|
+
*
|
|
121
|
+
* When provided, `evaluateDebugGate` reads the `at` query param and passes
|
|
122
|
+
* it to this predicate. Return `true` to allow, `false` to block with
|
|
123
|
+
* `reason: 'auth'`.
|
|
124
|
+
*
|
|
125
|
+
* Inject via the consumer's build define, e.g.:
|
|
126
|
+
* ```ts
|
|
127
|
+
* // dogfood build entry — consumer's build injects __DEBUG_TOTP_SECRET__
|
|
128
|
+
* declare const __DEBUG_TOTP_SECRET__: string | undefined;
|
|
129
|
+
* const verifyTotpCode = typeof __DEBUG_TOTP_SECRET__ !== 'undefined'
|
|
130
|
+
* ? (code: string) => verifyTotp(__DEBUG_TOTP_SECRET__, code)
|
|
131
|
+
* : undefined;
|
|
132
|
+
* maybeAttach(evaluateDebugGate({ ...params, verifyTotpCode }));
|
|
133
|
+
* ```
|
|
134
|
+
*
|
|
135
|
+
* Security note: this predicate is a black-box from the gate's perspective.
|
|
136
|
+
* The gate only surfaces pass/fail and the `'auth'` reason code — no code
|
|
137
|
+
* value or secret fragment is ever logged or returned.
|
|
138
|
+
*
|
|
139
|
+
* When `undefined` (TOTP disabled), `at=` is silently ignored and the gate
|
|
140
|
+
* proceeds to ATTACH if all other layers pass.
|
|
141
|
+
*/
|
|
142
|
+
readonly verifyTotpCode?: (code: string) => boolean;
|
|
89
143
|
}
|
|
90
144
|
/**
|
|
91
145
|
* Returns whether `hostname` is a `*.private-apps.tossmini.com` subdomain —
|
|
@@ -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":";;
|
|
1
|
+
{"version":3,"file":"index.d.ts","names":[],"sources":["../../src/in-app/gate.ts","../../src/in-app/attach.ts","../../src/in-app/index.ts"],"mappings":";;AA+DA;;;;;;;;;AASA;;;;;AAmBA;;;;;AAQA;;;;;;;;;;;AAwEA;;;;;AAyBA;;;;;;;;;;;;ACvKA;;;;;AA4BA;;;;;;;;ACbA;AAAA,UFmBiB,gBAAA;EAAA,SACN,MAAA;EEpBuB;EAAA,SFsBvB,QAAA;;WAEA,YAAA;AAAA;;UAIM,iBAAA;EAAA,SACN,MAAA;;;;;;;;;;;;;;;WAeA,MAAA;AAAA;AAAA,KAGC,UAAA,GAAa,gBAAA,GAAmB,iBAAA;;;;;;;UAQ3B,SAAA;;;;;;;;;;;;;WAaN,QAAA;;;;;;;WAQA,YAAA,EAAc,eAAA;;;;;;;;;;;;;;;;;;;;;;;;;WA0Bd,cAAA,IAAkB,IAAA;AAAA;;;;;;;;;;;;iBAyBb,iBAAA,CAAkB,QAAA;;;;;;;;;;;;;;;;;;;;;;iBAyBlB,iBAAA,CAAkB,KAAA,EAAO,SAAA,GAAY,UAAA;;;;;;AAzGrD;;;;;AAQA;;;;;;;iBCtEgB,qBAAA,CAAsB,QAAA;;;;AD8ItC;;;;;AAyBA;;;;;;;;iBC3IgB,WAAA,CAAY,UAAA,GAAY,UAAA;;;;;;;ADkHxC;;;;;AAyBA;;;;;iBExJgB,cAAA,CAAA,GAAkB,UAAA"}
|
package/dist/in-app/index.js
CHANGED
|
@@ -76,6 +76,13 @@ function evaluateDebugGate(input) {
|
|
|
76
76
|
attach: false,
|
|
77
77
|
reason: "invalid-relay"
|
|
78
78
|
};
|
|
79
|
+
if (input.verifyTotpCode !== void 0) {
|
|
80
|
+
const code = input.searchParams.get("at") ?? "";
|
|
81
|
+
if (!input.verifyTotpCode(code)) return {
|
|
82
|
+
attach: false,
|
|
83
|
+
reason: "auth"
|
|
84
|
+
};
|
|
85
|
+
}
|
|
79
86
|
return {
|
|
80
87
|
attach: true,
|
|
81
88
|
relayUrl: relayUrl.href,
|
package/dist/in-app/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","names":[],"sources":["../../src/in-app/gate.ts","../../src/in-app/attach.ts","../../src/in-app/index.ts"],"sourcesContent":["/**\n * Runtime activation gate for the in-app debug surface.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n * \"3-layer activation gate\". This is the pure gate decision; the Chii client,\n * WebSocket transport, MCP server, and CLI that consume it live in src/mcp/.\n *\n * This function evaluates the two RUNTIME layers, B and C. Layer A — the\n * build-time gate — is NOT evaluated here, and deliberately so: it is enforced\n * entirely by the consumer's `if (__DEBUG_BUILD__) { … }` guard around the\n * import site (see sdk-example `src/main.tsx`). `__DEBUG_BUILD__` is a\n * consumer-build-time constant; a release consumer build folds it to `false`\n * and dead-code-eliminates the whole import of `@ait-co/devtools/in-app`, so\n * this code is simply absent from release bundles. A pre-built npm package\n * cannot re-check that flag — it was already baked at devtools' own publish\n * time — so any `isDebugBuild` check inside this function would be permanently\n * `false` and could never pass. Layer A is the consumer guard; B and C are\n * here.\n *\n * Layer B has two parts. Both must pass:\n * B1 — host allowlist: `hostname` must be a `*.private-apps.tossmini.com`\n * subdomain. The Toss app serves dogfood / private mini-apps from a\n * separate `private-apps` host; a production (`intoss://`) entry is\n * served from `*.apps.tossmini.com` WITHOUT the `private-apps` segment.\n * This is the security gate against a dogfood build that somehow lands\n * on a production entry — see the comment on {@link isPrivateAppsHost}.\n * B2 — entry query: `_deploymentId` must be present and non-empty.\n *\n * Decision matrix (the gate only ever runs in a debug build — Layer A already\n * passed by the time this code is reachable):\n *\n * private-apps host | _deploymentId | debug=1 | result\n * no | (any) | (any) | BLOCKED (Layer B1 — host)\n * yes | absent | (any) | BLOCKED (Layer B2 — entry)\n * yes | present | absent | BLOCKED (Layer C — opt-in)\n * yes | present | present | ATTACH\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 C: `debug=1` param is absent.\n * - `'invalid-relay'` Layer C: `relay` param is absent, empty, or not a `wss:` URL.\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 readonly reason: 'host' | 'entry' | 'opt-in' | 'invalid-relay';\n}\n\nexport type GateResult = GateResultAttach | GateResultBlocked;\n\n/**\n * Input for {@link evaluateDebugGate}.\n *\n * Both 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/**\n * The host suffix the Toss app uses to serve dogfood / private mini-apps.\n *\n * A `intoss-private://` (dogfood) entry maps to a host such as\n * `aitc-sdk-example.private-apps.tossmini.com`. A production `intoss://`\n * entry is served from `*.apps.tossmini.com` — the `.private-apps.` segment\n * is absent. Confirmed live over CDP for mini-app 31146; the exact production\n * host is to be re-confirmed once 31146 passes review (spec open question 2).\n */\nconst PRIVATE_APPS_HOST_SUFFIX = '.private-apps.tossmini.com';\n\n/**\n * Returns whether `hostname` is a `*.private-apps.tossmini.com` subdomain —\n * the host the Toss app reserves for dogfood / private mini-app entries.\n *\n * The match is an exact suffix check, not a substring `.includes()`: a\n * substring test would also accept an attacker-controlled host like\n * `private-apps.tossmini.com.evil.example`, which ends in `.example`, not in\n * `.tossmini.com`. Requiring the string to END with the suffix closes that.\n * The leading `.` in the suffix also forces a real subdomain label, so a\n * bare `private-apps.tossmini.com` (no mini-app subdomain) does not match.\n */\nexport function isPrivateAppsHost(hostname: string): boolean {\n return hostname.endsWith(PRIVATE_APPS_HOST_SUFFIX);\n}\n\n/**\n * Pure function that evaluates the runtime debug activation layers (B and C).\n *\n * Has no side effects. The input is explicit. Returns a discriminated union\n * so callers can pattern-match on `result.attach`.\n *\n * Layer A (build-time) is intentionally not evaluated here — see the file-level\n * comment. By the time this function runs, the consumer's `if (__DEBUG_BUILD__)`\n * guard has already passed; this function only decides B and C.\n *\n * @example\n * ```ts\n * const result = evaluateDebugGate({\n * hostname: window.location.hostname,\n * searchParams: new URLSearchParams(window.location.search),\n * });\n * if (result.attach) {\n * // Proceed to load Chii client\n * }\n * ```\n */\nexport function evaluateDebugGate(input: GateInput): GateResult {\n // Layer B1 — host allowlist (the security gate).\n // The page must be served from a `*.private-apps.tossmini.com` host. A\n // production `intoss://` entry is served from `*.apps.tossmini.com` and is\n // rejected here. This is what stops a dogfood build that somehow reaches a\n // production entry from attaching: Layer A keeps debug code out of release\n // bundles, and this layer keeps a dogfood bundle that lands on a production\n // host from attaching even though its code is present.\n if (!isPrivateAppsHost(input.hostname)) {\n return { attach: false, reason: 'host' };\n }\n\n // Layer B2 — runtime entry query gate.\n // `_deploymentId` must be present and non-empty. The `intoss-private://`\n // scheme used for dogfood entries includes this param; general user entry\n // paths do not.\n const deploymentId = input.searchParams.get('_deploymentId') ?? '';\n if (deploymentId === '') {\n return { attach: false, reason: 'entry' };\n }\n\n // Layer C — explicit opt-in gate.\n // Require `debug=1` so that an operator who opens a dogfood URL by accident\n // does not inadvertently trigger the debug surface.\n const debugParam = input.searchParams.get('debug');\n if (debugParam !== '1') {\n return { attach: false, reason: 'opt-in' };\n }\n\n // Layer C continued — relay URL validation.\n // `relay=<wss-url>` must be present and must use the `wss:` scheme.\n // Plain `ws:` is rejected (no TLS). `http:`/`https:` are rejected.\n const relayRaw = input.searchParams.get('relay') ?? '';\n if (relayRaw === '') {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n let relayUrl: URL;\n try {\n relayUrl = new URL(relayRaw);\n } catch {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n if (relayUrl.protocol !== 'wss:') {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n return { attach: true, relayUrl: relayUrl.href, deploymentId };\n}\n","/**\n * In-app Chii target injection for the debug attach flow.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n * \"MCP attach\" topology section — Phase 1 browser-side implementation.\n *\n * This module bridges the 3-layer gate result to a Chii `target.js` script\n * injection. The Chii npm package is the relay SERVER — the in-app side is\n * a plain `<script src=\"…/target.js\">` pointing at the relay host. No chii\n * npm dependency is needed here.\n */\n\nimport { checkDebugGate, type GateResult } from './index.js';\n\n/**\n * Converts a validated `wss:` relay URL into the Chii `target.js` script URL.\n *\n * Scheme is mapped `wss:` → `https:`. Host and port are preserved.\n * Pathname is set to `/target.js` regardless of the relay path.\n * Query params and hash from the relay URL are dropped — the target script\n * URL is a static asset path on the same host.\n *\n * @example\n * deriveTargetScriptUrl('wss://abc.trycloudflare.com/relay')\n * // → 'https://abc.trycloudflare.com/target.js'\n *\n * deriveTargetScriptUrl('wss://h.example.com:9100/')\n * // → 'https://h.example.com:9100/target.js'\n */\nexport function deriveTargetScriptUrl(relayUrl: string): string {\n const u = new URL(relayUrl);\n u.protocol = 'https:';\n u.pathname = '/target.js';\n u.search = '';\n u.hash = '';\n return u.toString();\n}\n\n/** Module-level guard against double-injection within a page lifecycle. */\nlet attached = false;\n\n/**\n * Evaluates the 3-layer debug gate and, if the gate passes, injects the Chii\n * `target.js` script into `document.head`.\n *\n * Idempotent — calling more than once is safe. The second call is a no-op if\n * a script with the same `src` is already present in the document, and the\n * module-level `attached` flag prevents redundant DOM queries after the first\n * successful injection.\n *\n * Safe to call even if `document` is somehow unavailable (defensive boundary\n * guard — in practice this always runs in a real WebView).\n *\n * @param gateResult - Optional pre-evaluated gate result for testability.\n * Defaults to `checkDebugGate()` which reads the current page URL. Passing a\n * custom value avoids the need to manipulate `window.location` in tests.\n */\nexport function maybeAttach(gateResult: GateResult = checkDebugGate()): void {\n if (!gateResult.attach) {\n console.debug(\n `[@ait-co/devtools] debug attach skipped — gate blocked (reason: ${gateResult.reason})`,\n );\n return;\n }\n\n // Guard against double-injection across repeated calls.\n if (attached) {\n return;\n }\n\n // Defensive: if document is not available (unusual, but possible in some\n // SSR-adjacent edge cases), bail silently rather than throwing.\n if (typeof document === 'undefined') {\n return;\n }\n\n const src = deriveTargetScriptUrl(gateResult.relayUrl);\n\n // Also guard against a script with the same src already in the DOM\n // (e.g. injected by a different code path or a page reload within SPA).\n const existing = document.querySelector<HTMLScriptElement>(`script[src=\"${src}\"]`);\n if (existing !== null) {\n attached = true;\n return;\n }\n\n const script = document.createElement('script');\n script.src = src;\n script.async = true;\n (document.head ?? document.documentElement).appendChild(script);\n\n attached = true;\n}\n","/**\n * @ait-co/devtools/in-app entry point.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n *\n * Phase 1 — gate + browser-side Chii target injection.\n * WebSocket relay, QR/paste UI, and AI-host MCP bin are later phases that\n * require real-device validation and are not included here.\n *\n * This thin entry reads `window.location` and calls the pure\n * {@link evaluateDebugGate} function. All testable logic lives in `./gate.ts`\n * and `./attach.ts`, not here.\n *\n * Layer A of the activation gate (build-time) is NOT enforced in this module.\n * It is the consumer's responsibility: the consumer wraps its\n * `import('@ait-co/devtools/in-app')` call site in `if (__DEBUG_BUILD__) { … }`\n * (see sdk-example `src/main.tsx`), where `__DEBUG_BUILD__` is a\n * consumer-build-time constant. A release consumer build folds that constant\n * to `false` and dead-code-eliminates this whole module. This package is\n * pre-built and ships with `__DEBUG_BUILD__` already resolved at devtools'\n * publish time, so it could never re-evaluate the consumer's build channel —\n * which is exactly why Layer A lives at the consumer guard, not here.\n */\n\nimport { evaluateDebugGate, type GateResult } from './gate.js';\n\nexport { deriveTargetScriptUrl, maybeAttach } from './attach.js';\nexport type { GateInput, GateResult, GateResultAttach, GateResultBlocked } from './gate.js';\nexport { evaluateDebugGate, isPrivateAppsHost } from './gate.js';\n\n/**\n * Evaluates the runtime debug activation layers (B and C) against the current\n * page URL.\n *\n * Returns the gate result. Callers can check `result.attach` to decide whether\n * to proceed with debug surface attachment.\n *\n * This function reads `window.location` only — both the hostname (Layer B1\n * host allowlist) and the search params (Layers B2 and C). Layer A\n * (build-time) is enforced by the consumer's `if (__DEBUG_BUILD__)` guard\n * around the import site, not here — see the file-level comment. Consumers\n * call this with no arguments, so the Layer B1 host check is picked up with\n * no change at the call site.\n */\nexport function checkDebugGate(): GateResult {\n return evaluateDebugGate({\n hostname: window.location.hostname,\n searchParams: new URLSearchParams(window.location.search),\n });\n}\n"],"mappings":";;;;;;;;;;AAuGA,MAAM,2BAA2B;;;;;;;;;;;;AAajC,SAAgB,kBAAkB,UAA2B;AAC3D,QAAO,SAAS,SAAS,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;AAwBpD,SAAgB,kBAAkB,OAA8B;AAQ9D,KAAI,CAAC,kBAAkB,MAAM,SAAS,CACpC,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAQ;CAO1C,MAAM,eAAe,MAAM,aAAa,IAAI,gBAAgB,IAAI;AAChE,KAAI,iBAAiB,GACnB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAS;AAO3C,KADmB,MAAM,aAAa,IAAI,QAAQ,KAC/B,IACjB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAU;CAM5C,MAAM,WAAW,MAAM,aAAa,IAAI,QAAQ,IAAI;AACpD,KAAI,aAAa,GACf,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAiB;CAGnD,IAAI;AACJ,KAAI;AACF,aAAW,IAAI,IAAI,SAAS;SACtB;AACN,SAAO;GAAE,QAAQ;GAAO,QAAQ;GAAiB;;AAGnD,KAAI,SAAS,aAAa,OACxB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAiB;AAGnD,QAAO;EAAE,QAAQ;EAAM,UAAU,SAAS;EAAM;EAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AChKhE,SAAgB,sBAAsB,UAA0B;CAC9D,MAAM,IAAI,IAAI,IAAI,SAAS;AAC3B,GAAE,WAAW;AACb,GAAE,WAAW;AACb,GAAE,SAAS;AACX,GAAE,OAAO;AACT,QAAO,EAAE,UAAU;;;AAIrB,IAAI,WAAW;;;;;;;;;;;;;;;;;AAkBf,SAAgB,YAAY,aAAyB,gBAAgB,EAAQ;AAC3E,KAAI,CAAC,WAAW,QAAQ;AACtB,UAAQ,MACN,mEAAmE,WAAW,OAAO,GACtF;AACD;;AAIF,KAAI,SACF;AAKF,KAAI,OAAO,aAAa,YACtB;CAGF,MAAM,MAAM,sBAAsB,WAAW,SAAS;AAKtD,KADiB,SAAS,cAAiC,eAAe,IAAI,IAAI,KACjE,MAAM;AACrB,aAAW;AACX;;CAGF,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,QAAO,MAAM;AACb,QAAO,QAAQ;AACf,EAAC,SAAS,QAAQ,SAAS,iBAAiB,YAAY,OAAO;AAE/D,YAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC/Cb,SAAgB,iBAA6B;AAC3C,QAAO,kBAAkB;EACvB,UAAU,OAAO,SAAS;EAC1B,cAAc,IAAI,gBAAgB,OAAO,SAAS,OAAO;EAC1D,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","names":[],"sources":["../../src/in-app/gate.ts","../../src/in-app/attach.ts","../../src/in-app/index.ts"],"sourcesContent":["/**\n * Runtime activation gate for the in-app debug surface.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n * \"3-layer activation gate\". This is the pure gate decision; the Chii client,\n * WebSocket transport, MCP server, and CLI that consume it live in src/mcp/.\n *\n * This function evaluates the two RUNTIME layers, B and C. Layer A — the\n * build-time gate — is NOT evaluated here, and deliberately so: it is enforced\n * entirely by the consumer's `if (__DEBUG_BUILD__) { … }` guard around the\n * import site (see sdk-example `src/main.tsx`). `__DEBUG_BUILD__` is a\n * consumer-build-time constant; a release consumer build folds it to `false`\n * and dead-code-eliminates the whole import of `@ait-co/devtools/in-app`, so\n * this code is simply absent from release bundles. A pre-built npm package\n * cannot re-check that flag — it was already baked at devtools' own publish\n * time — so any `isDebugBuild` check inside this function would be permanently\n * `false` and could never pass. Layer A is the consumer guard; B and C are\n * here.\n *\n * Layer B has two parts. Both must pass:\n * B1 — host allowlist: `hostname` must be a `*.private-apps.tossmini.com`\n * subdomain. The Toss app serves dogfood / private mini-apps from a\n * separate `private-apps` host; a production (`intoss://`) entry is\n * served from `*.apps.tossmini.com` WITHOUT the `private-apps` segment.\n * This is the security gate against a dogfood build that somehow lands\n * on a production entry — see the comment on {@link isPrivateAppsHost}.\n * B2 — entry query: `_deploymentId` must be present and non-empty.\n *\n * Layer C — opt-in + relay + optional TOTP auth:\n * C1 — opt-in: `debug=1` must be present.\n * C2 — relay URL: `relay=<wss-url>` must be a valid `wss:` URL.\n * C3 — TOTP auth: When `verifyTotpCode` is provided (consumer injected the\n * baked secret at build time via `__DEBUG_TOTP_SECRET__`),\n * `at=<code>` is checked. Invalid or absent code → BLOCKED.\n * When no verifier is provided (TOTP disabled), `at` is\n * ignored (backward compatible).\n *\n * Security note on baked secrets:\n * The TOTP secret baked in via `__DEBUG_TOTP_SECRET__` is present in the\n * dogfood bundle and is extractable by a determined reverse engineer.\n * The practical bar raised is: \"URL leak\" (Slack paste, QR screenshot) →\n * blocked; \"URL + bundle extraction + live TOTP code\" → not blocked.\n * This is the intended threat model. Do not overpromise on this guarantee.\n *\n * SECRET-HANDLING: `verifyTotpCode` is a black-box predicate. This module\n * does NOT log the secret, any code value, or pass/fail details beyond the\n * `'auth'` reason enum.\n *\n * Decision matrix (gate only runs in a debug build — Layer A already passed):\n *\n * host ok | _deploymentId | debug=1 | relay ok | TOTP ok* | result\n * no | (any) | (any) | (any) | (any) | BLOCKED (host)\n * yes | absent | (any) | (any) | (any) | BLOCKED (entry)\n * yes | present | absent | (any) | (any) | BLOCKED (opt-in)\n * yes | present | present | invalid | (any) | BLOCKED (invalid-relay)\n * yes | present | present | valid | fail* | BLOCKED (auth)\n * yes | present | present | valid | pass/n/a | ATTACH\n *\n * * \"TOTP ok\" column only applies when `verifyTotpCode` is provided.\n * When no verifier is injected, TOTP check is skipped entirely.\n */\n\n/** Shape returned when the gate allows attachment. */\nexport interface GateResultAttach {\n readonly attach: true;\n /** The validated `wss:` relay URL from the `relay` query param. */\n readonly relayUrl: string;\n /** The deployment ID extracted from the `_deploymentId` query param. */\n readonly deploymentId: string;\n}\n\n/** Shape returned when the gate blocks attachment, with a reason code. */\nexport interface GateResultBlocked {\n readonly attach: false;\n /**\n * - `'host'` Layer B1: `hostname` is not a `*.private-apps.tossmini.com` host.\n * - `'entry'` Layer B2: `_deploymentId` param is absent or empty.\n * - `'opt-in'` Layer C1: `debug=1` param is absent.\n * - `'invalid-relay'` Layer C2: `relay` param is absent, empty, or not a `wss:` URL.\n * - `'auth'` Layer C3: TOTP `at=` code is absent, invalid, or expired\n * (only when a `verifyTotpCode` predicate is injected).\n *\n * There is no `'build'` reason: Layer A is enforced by the consumer's\n * `if (__DEBUG_BUILD__)` guard, not by this function.\n *\n * SECRET-HANDLING: `'auth'` is the only value surfaced for auth failures —\n * no code value, expected value, or secret fragment is ever exposed.\n */\n readonly reason: 'host' | 'entry' | 'opt-in' | 'invalid-relay' | 'auth';\n}\n\nexport type GateResult = GateResultAttach | GateResultBlocked;\n\n/**\n * Input for {@link evaluateDebugGate}.\n *\n * All fields are explicit so the function is trivially testable without\n * touching `window`.\n */\nexport interface GateInput {\n /**\n * The host the page is served from — `window.location.hostname`.\n *\n * This is the Layer B1 security signal. Why hostname and not the entry\n * scheme: the Toss SDK normalises `intoss-private://` to `intoss://` in\n * `getSchemeUri()`, and `getOperationalEnvironment()` / `getWebViewType()`\n * return the same value (`\"toss\"` / `\"partner\"`) for both dogfood and\n * production entries — none of them distinguish a dogfood entry. The host\n * does: a dogfood / private-apps entry is served from\n * `*.private-apps.tossmini.com`, a production entry is not. This was\n * confirmed live over CDP against mini-app 31146 (see spec open question 2).\n */\n readonly hostname: string;\n\n /**\n * The URL search params to inspect for gate signals (Layers B2 and C).\n *\n * Prefer `URLSearchParams` so callers can pass `new URLSearchParams(location.search)`\n * without coupling the pure function to `window`.\n */\n readonly searchParams: URLSearchParams;\n\n /**\n * Optional TOTP code verifier for Layer C3 auth gate.\n *\n * When provided, `evaluateDebugGate` reads the `at` query param and passes\n * it to this predicate. Return `true` to allow, `false` to block with\n * `reason: 'auth'`.\n *\n * Inject via the consumer's build define, e.g.:\n * ```ts\n * // dogfood build entry — consumer's build injects __DEBUG_TOTP_SECRET__\n * declare const __DEBUG_TOTP_SECRET__: string | undefined;\n * const verifyTotpCode = typeof __DEBUG_TOTP_SECRET__ !== 'undefined'\n * ? (code: string) => verifyTotp(__DEBUG_TOTP_SECRET__, code)\n * : undefined;\n * maybeAttach(evaluateDebugGate({ ...params, verifyTotpCode }));\n * ```\n *\n * Security note: this predicate is a black-box from the gate's perspective.\n * The gate only surfaces pass/fail and the `'auth'` reason code — no code\n * value or secret fragment is ever logged or returned.\n *\n * When `undefined` (TOTP disabled), `at=` is silently ignored and the gate\n * proceeds to ATTACH if all other layers pass.\n */\n readonly verifyTotpCode?: (code: string) => boolean;\n}\n\n/**\n * The host suffix the Toss app uses to serve dogfood / private mini-apps.\n *\n * A `intoss-private://` (dogfood) entry maps to a host such as\n * `aitc-sdk-example.private-apps.tossmini.com`. A production `intoss://`\n * entry is served from `*.apps.tossmini.com` — the `.private-apps.` segment\n * is absent. Confirmed live over CDP for mini-app 31146; the exact production\n * host is to be re-confirmed once 31146 passes review (spec open question 2).\n */\nconst PRIVATE_APPS_HOST_SUFFIX = '.private-apps.tossmini.com';\n\n/**\n * Returns whether `hostname` is a `*.private-apps.tossmini.com` subdomain —\n * the host the Toss app reserves for dogfood / private mini-app entries.\n *\n * The match is an exact suffix check, not a substring `.includes()`: a\n * substring test would also accept an attacker-controlled host like\n * `private-apps.tossmini.com.evil.example`, which ends in `.example`, not in\n * `.tossmini.com`. Requiring the string to END with the suffix closes that.\n * The leading `.` in the suffix also forces a real subdomain label, so a\n * bare `private-apps.tossmini.com` (no mini-app subdomain) does not match.\n */\nexport function isPrivateAppsHost(hostname: string): boolean {\n return hostname.endsWith(PRIVATE_APPS_HOST_SUFFIX);\n}\n\n/**\n * Pure function that evaluates the runtime debug activation layers (B and C).\n *\n * Has no side effects. The input is explicit. Returns a discriminated union\n * so callers can pattern-match on `result.attach`.\n *\n * Layer A (build-time) is intentionally not evaluated here — see the file-level\n * comment. By the time this function runs, the consumer's `if (__DEBUG_BUILD__)`\n * guard has already passed; this function only decides B and C.\n *\n * @example\n * ```ts\n * const result = evaluateDebugGate({\n * hostname: window.location.hostname,\n * searchParams: new URLSearchParams(window.location.search),\n * });\n * if (result.attach) {\n * // Proceed to load Chii client\n * }\n * ```\n */\nexport function evaluateDebugGate(input: GateInput): GateResult {\n // Layer B1 — host allowlist (the security gate).\n // The page must be served from a `*.private-apps.tossmini.com` host. A\n // production `intoss://` entry is served from `*.apps.tossmini.com` and is\n // rejected here. This is what stops a dogfood build that somehow reaches a\n // production entry from attaching: Layer A keeps debug code out of release\n // bundles, and this layer keeps a dogfood bundle that lands on a production\n // host from attaching even though its code is present.\n if (!isPrivateAppsHost(input.hostname)) {\n return { attach: false, reason: 'host' };\n }\n\n // Layer B2 — runtime entry query gate.\n // `_deploymentId` must be present and non-empty. The `intoss-private://`\n // scheme used for dogfood entries includes this param; general user entry\n // paths do not.\n const deploymentId = input.searchParams.get('_deploymentId') ?? '';\n if (deploymentId === '') {\n return { attach: false, reason: 'entry' };\n }\n\n // Layer C — explicit opt-in gate.\n // Require `debug=1` so that an operator who opens a dogfood URL by accident\n // does not inadvertently trigger the debug surface.\n const debugParam = input.searchParams.get('debug');\n if (debugParam !== '1') {\n return { attach: false, reason: 'opt-in' };\n }\n\n // Layer C continued — relay URL validation.\n // `relay=<wss-url>` must be present and must use the `wss:` scheme.\n // Plain `ws:` is rejected (no TLS). `http:`/`https:` are rejected.\n const relayRaw = input.searchParams.get('relay') ?? '';\n if (relayRaw === '') {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n let relayUrl: URL;\n try {\n relayUrl = new URL(relayRaw);\n } catch {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n if (relayUrl.protocol !== 'wss:') {\n return { attach: false, reason: 'invalid-relay' };\n }\n\n // Layer C3 — TOTP auth gate (fail-fast, only when a verifier is injected).\n // The `at` query param carries the current TOTP code. Absent or invalid code\n // → BLOCKED. When no verifier is provided (TOTP disabled), this check is\n // skipped entirely for backward compatibility.\n //\n // SECRET-HANDLING: we do NOT log `code`, the verifier's result, or anything\n // derived from the secret. Only the `'auth'` enum is surfaced on failure.\n if (input.verifyTotpCode !== undefined) {\n const code = input.searchParams.get('at') ?? '';\n if (!input.verifyTotpCode(code)) {\n return { attach: false, reason: 'auth' };\n }\n }\n\n return { attach: true, relayUrl: relayUrl.href, deploymentId };\n}\n","/**\n * In-app Chii target injection for the debug attach flow.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n * \"MCP attach\" topology section — Phase 1 browser-side implementation.\n *\n * This module bridges the 3-layer gate result to a Chii `target.js` script\n * injection. The Chii npm package is the relay SERVER — the in-app side is\n * a plain `<script src=\"…/target.js\">` pointing at the relay host. No chii\n * npm dependency is needed here.\n */\n\nimport { checkDebugGate, type GateResult } from './index.js';\n\n/**\n * Converts a validated `wss:` relay URL into the Chii `target.js` script URL.\n *\n * Scheme is mapped `wss:` → `https:`. Host and port are preserved.\n * Pathname is set to `/target.js` regardless of the relay path.\n * Query params and hash from the relay URL are dropped — the target script\n * URL is a static asset path on the same host.\n *\n * @example\n * deriveTargetScriptUrl('wss://abc.trycloudflare.com/relay')\n * // → 'https://abc.trycloudflare.com/target.js'\n *\n * deriveTargetScriptUrl('wss://h.example.com:9100/')\n * // → 'https://h.example.com:9100/target.js'\n */\nexport function deriveTargetScriptUrl(relayUrl: string): string {\n const u = new URL(relayUrl);\n u.protocol = 'https:';\n u.pathname = '/target.js';\n u.search = '';\n u.hash = '';\n return u.toString();\n}\n\n/** Module-level guard against double-injection within a page lifecycle. */\nlet attached = false;\n\n/**\n * Evaluates the 3-layer debug gate and, if the gate passes, injects the Chii\n * `target.js` script into `document.head`.\n *\n * Idempotent — calling more than once is safe. The second call is a no-op if\n * a script with the same `src` is already present in the document, and the\n * module-level `attached` flag prevents redundant DOM queries after the first\n * successful injection.\n *\n * Safe to call even if `document` is somehow unavailable (defensive boundary\n * guard — in practice this always runs in a real WebView).\n *\n * @param gateResult - Optional pre-evaluated gate result for testability.\n * Defaults to `checkDebugGate()` which reads the current page URL. Passing a\n * custom value avoids the need to manipulate `window.location` in tests.\n */\nexport function maybeAttach(gateResult: GateResult = checkDebugGate()): void {\n if (!gateResult.attach) {\n console.debug(\n `[@ait-co/devtools] debug attach skipped — gate blocked (reason: ${gateResult.reason})`,\n );\n return;\n }\n\n // Guard against double-injection across repeated calls.\n if (attached) {\n return;\n }\n\n // Defensive: if document is not available (unusual, but possible in some\n // SSR-adjacent edge cases), bail silently rather than throwing.\n if (typeof document === 'undefined') {\n return;\n }\n\n const src = deriveTargetScriptUrl(gateResult.relayUrl);\n\n // Also guard against a script with the same src already in the DOM\n // (e.g. injected by a different code path or a page reload within SPA).\n const existing = document.querySelector<HTMLScriptElement>(`script[src=\"${src}\"]`);\n if (existing !== null) {\n attached = true;\n return;\n }\n\n const script = document.createElement('script');\n script.src = src;\n script.async = true;\n (document.head ?? document.documentElement).appendChild(script);\n\n attached = true;\n}\n","/**\n * @ait-co/devtools/in-app entry point.\n *\n * Spec: docs/superpowers/specs/2026-05-18-in-app-debug-mcp.md\n *\n * Phase 1 — gate + browser-side Chii target injection.\n * WebSocket relay, QR/paste UI, and AI-host MCP bin are later phases that\n * require real-device validation and are not included here.\n *\n * This thin entry reads `window.location` and calls the pure\n * {@link evaluateDebugGate} function. All testable logic lives in `./gate.ts`\n * and `./attach.ts`, not here.\n *\n * Layer A of the activation gate (build-time) is NOT enforced in this module.\n * It is the consumer's responsibility: the consumer wraps its\n * `import('@ait-co/devtools/in-app')` call site in `if (__DEBUG_BUILD__) { … }`\n * (see sdk-example `src/main.tsx`), where `__DEBUG_BUILD__` is a\n * consumer-build-time constant. A release consumer build folds that constant\n * to `false` and dead-code-eliminates this whole module. This package is\n * pre-built and ships with `__DEBUG_BUILD__` already resolved at devtools'\n * publish time, so it could never re-evaluate the consumer's build channel —\n * which is exactly why Layer A lives at the consumer guard, not here.\n */\n\nimport { evaluateDebugGate, type GateResult } from './gate.js';\n\nexport { deriveTargetScriptUrl, maybeAttach } from './attach.js';\nexport type { GateInput, GateResult, GateResultAttach, GateResultBlocked } from './gate.js';\nexport { evaluateDebugGate, isPrivateAppsHost } from './gate.js';\n\n/**\n * Evaluates the runtime debug activation layers (B and C) against the current\n * page URL.\n *\n * Returns the gate result. Callers can check `result.attach` to decide whether\n * to proceed with debug surface attachment.\n *\n * This function reads `window.location` only — both the hostname (Layer B1\n * host allowlist) and the search params (Layers B2 and C). Layer A\n * (build-time) is enforced by the consumer's `if (__DEBUG_BUILD__)` guard\n * around the import site, not here — see the file-level comment. Consumers\n * call this with no arguments, so the Layer B1 host check is picked up with\n * no change at the call site.\n */\nexport function checkDebugGate(): GateResult {\n return evaluateDebugGate({\n hostname: window.location.hostname,\n searchParams: new URLSearchParams(window.location.search),\n });\n}\n"],"mappings":";;;;;;;;;;AA8JA,MAAM,2BAA2B;;;;;;;;;;;;AAajC,SAAgB,kBAAkB,UAA2B;AAC3D,QAAO,SAAS,SAAS,yBAAyB;;;;;;;;;;;;;;;;;;;;;;;AAwBpD,SAAgB,kBAAkB,OAA8B;AAQ9D,KAAI,CAAC,kBAAkB,MAAM,SAAS,CACpC,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAQ;CAO1C,MAAM,eAAe,MAAM,aAAa,IAAI,gBAAgB,IAAI;AAChE,KAAI,iBAAiB,GACnB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAS;AAO3C,KADmB,MAAM,aAAa,IAAI,QAAQ,KAC/B,IACjB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAU;CAM5C,MAAM,WAAW,MAAM,aAAa,IAAI,QAAQ,IAAI;AACpD,KAAI,aAAa,GACf,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAiB;CAGnD,IAAI;AACJ,KAAI;AACF,aAAW,IAAI,IAAI,SAAS;SACtB;AACN,SAAO;GAAE,QAAQ;GAAO,QAAQ;GAAiB;;AAGnD,KAAI,SAAS,aAAa,OACxB,QAAO;EAAE,QAAQ;EAAO,QAAQ;EAAiB;AAUnD,KAAI,MAAM,mBAAmB,KAAA,GAAW;EACtC,MAAM,OAAO,MAAM,aAAa,IAAI,KAAK,IAAI;AAC7C,MAAI,CAAC,MAAM,eAAe,KAAK,CAC7B,QAAO;GAAE,QAAQ;GAAO,QAAQ;GAAQ;;AAI5C,QAAO;EAAE,QAAQ;EAAM,UAAU,SAAS;EAAM;EAAc;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;ACrOhE,SAAgB,sBAAsB,UAA0B;CAC9D,MAAM,IAAI,IAAI,IAAI,SAAS;AAC3B,GAAE,WAAW;AACb,GAAE,WAAW;AACb,GAAE,SAAS;AACX,GAAE,OAAO;AACT,QAAO,EAAE,UAAU;;;AAIrB,IAAI,WAAW;;;;;;;;;;;;;;;;;AAkBf,SAAgB,YAAY,aAAyB,gBAAgB,EAAQ;AAC3E,KAAI,CAAC,WAAW,QAAQ;AACtB,UAAQ,MACN,mEAAmE,WAAW,OAAO,GACtF;AACD;;AAIF,KAAI,SACF;AAKF,KAAI,OAAO,aAAa,YACtB;CAGF,MAAM,MAAM,sBAAsB,WAAW,SAAS;AAKtD,KADiB,SAAS,cAAiC,eAAe,IAAI,IAAI,KACjE,MAAM;AACrB,aAAW;AACX;;CAGF,MAAM,SAAS,SAAS,cAAc,SAAS;AAC/C,QAAO,MAAM;AACb,QAAO,QAAQ;AACf,EAAC,SAAS,QAAQ,SAAS,iBAAiB,YAAY,OAAO;AAE/D,YAAW;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AC/Cb,SAAgB,iBAA6B;AAC3C,QAAO,kBAAkB;EACvB,UAAU,OAAO,SAAS;EAC1B,cAAc,IAAI,gBAAgB,OAAO,SAAS,OAAO;EAC1D,CAAC"}
|
package/dist/mcp/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema } from "@modelcontextprot
|
|
|
8
8
|
import { EventEmitter } from "node:events";
|
|
9
9
|
import { WebSocket } from "ws";
|
|
10
10
|
import { createServer } from "node:http";
|
|
11
|
-
import { randomBytes } from "node:crypto";
|
|
11
|
+
import { createHmac, randomBytes, timingSafeEqual } from "node:crypto";
|
|
12
12
|
import { Tunnel, bin, install } from "cloudflared";
|
|
13
13
|
import qrcode from "qrcode-terminal";
|
|
14
14
|
//#region src/mcp/ait-chii-source.ts
|
|
@@ -239,6 +239,23 @@ var ChiiCdpConnection = class {
|
|
|
239
239
|
*
|
|
240
240
|
* Node-only: `chii` pulls in Koa + ws. Never bundled into the browser/in-app
|
|
241
241
|
* entries.
|
|
242
|
+
*
|
|
243
|
+
* TOTP auth (relay-side, authoritative gate):
|
|
244
|
+
* When `verifyAuth` is provided, this module registers an HTTP upgrade
|
|
245
|
+
* listener on the server BEFORE calling `chii.start({server})`. Node's
|
|
246
|
+
* `http.Server` allows multiple 'upgrade' listeners; the first to call
|
|
247
|
+
* `socket.destroy()` wins. Invalid auth → 401 + destroy (chii never sees
|
|
248
|
+
* the connection). Valid auth → return without side-effect (chii handles it).
|
|
249
|
+
*
|
|
250
|
+
* Threat model: "URL leak" — someone obtains the tunnel URL (Slack paste, QR
|
|
251
|
+
* screenshot, shoulder-surfing) but does not have the shared TOTP secret.
|
|
252
|
+
* Rotating 6-digit code makes the URL stale after 30 s.
|
|
253
|
+
* A determined attacker who extracts the secret from the dogfood bundle can
|
|
254
|
+
* still compute valid codes; that is out of scope (see umbrella CLAUDE.md §4).
|
|
255
|
+
*
|
|
256
|
+
* SECRET-HANDLING: The secret value and computed TOTP codes MUST NOT appear
|
|
257
|
+
* in any log, error message, or process output. `verifyAuth` is a black-box
|
|
258
|
+
* predicate from the caller's perspective; this module only forwards pass/fail.
|
|
242
259
|
*/
|
|
243
260
|
const require = createRequire(import.meta.url);
|
|
244
261
|
function loadChiiServer() {
|
|
@@ -250,7 +267,15 @@ function loadChiiServer() {
|
|
|
250
267
|
async function startChiiRelay(options = {}) {
|
|
251
268
|
const port = options.port ?? 9100;
|
|
252
269
|
const host = options.host ?? "127.0.0.1";
|
|
270
|
+
const { verifyAuth } = options;
|
|
253
271
|
const httpServer = createServer();
|
|
272
|
+
if (verifyAuth) httpServer.on("upgrade", (req, socket) => {
|
|
273
|
+
if (!verifyAuth(req)) {
|
|
274
|
+
socket.write("HTTP/1.1 401 Unauthorized\r\nContent-Length: 0\r\n\r\n");
|
|
275
|
+
socket.destroy();
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
});
|
|
254
279
|
await loadChiiServer().start({
|
|
255
280
|
server: httpServer,
|
|
256
281
|
domain: `${host}:${port}`,
|
|
@@ -278,21 +303,25 @@ function stripExisting(query, key) {
|
|
|
278
303
|
return query.split("&").filter((pair) => pair !== "" && pair.split("=")[0] !== key).join("&");
|
|
279
304
|
}
|
|
280
305
|
/**
|
|
281
|
-
* Splices `debug=1` and `
|
|
282
|
-
* preserving everything else (scheme, authority,
|
|
283
|
-
* `_deploymentId` param). If
|
|
284
|
-
* replaced so the helper is idempotent.
|
|
306
|
+
* Splices `debug=1`, `relay=<wssUrl>`, and (optionally) `at=<totpCode>` into a
|
|
307
|
+
* scheme URL's query string, preserving everything else (scheme, authority,
|
|
308
|
+
* path, hash, and the existing `_deploymentId` param). If any of the spliced
|
|
309
|
+
* params is already present it is replaced so the helper is idempotent.
|
|
285
310
|
*
|
|
286
311
|
* @param schemeUrl - The `intoss-private://…?_deploymentId=<uuid>` URL printed
|
|
287
312
|
* by `ait deploy --scheme-only`. Must already carry `_deploymentId` (Layer B
|
|
288
313
|
* of the gate); this helper does not invent one.
|
|
289
314
|
* @param wssUrl - The live relay URL (`wss://…trycloudflare.com`) from the
|
|
290
315
|
* running debug MCP server's quick tunnel.
|
|
291
|
-
* @
|
|
316
|
+
* @param totpCode - Optional current TOTP code (6 digits). When provided, it
|
|
317
|
+
* is spliced as `at=<totpCode>`. Must be computed at call time — it rotates
|
|
318
|
+
* every 30 s. Pass `undefined` or omit when TOTP is disabled.
|
|
319
|
+
* @returns The same URL with `debug=1&relay=<encoded wssUrl>[&at=<totpCode>]`
|
|
320
|
+
* appended.
|
|
292
321
|
* @throws If `wssUrl` is not a `wss:` URL (the gate rejects anything else, so
|
|
293
322
|
* producing such a link would be a silent dead end).
|
|
294
323
|
*/
|
|
295
|
-
function buildDeepLinkAttachUrl(schemeUrl, wssUrl) {
|
|
324
|
+
function buildDeepLinkAttachUrl(schemeUrl, wssUrl, totpCode) {
|
|
296
325
|
let relay;
|
|
297
326
|
try {
|
|
298
327
|
relay = new URL(wssUrl);
|
|
@@ -307,6 +336,8 @@ function buildDeepLinkAttachUrl(schemeUrl, wssUrl) {
|
|
|
307
336
|
const base = queryIndex === -1 ? beforeHash : beforeHash.slice(0, queryIndex);
|
|
308
337
|
let query = queryIndex === -1 ? "" : beforeHash.slice(queryIndex + 1);
|
|
309
338
|
const appended = [["debug", "1"], ["relay", wssUrl]];
|
|
339
|
+
if (totpCode !== void 0 && totpCode !== "") appended.push(["at", totpCode]);
|
|
340
|
+
query = stripExisting(query, "at");
|
|
310
341
|
for (const [key] of appended) query = stripExisting(query, key);
|
|
311
342
|
for (const [key, value] of appended) {
|
|
312
343
|
const pair = `${key}=${encodeURIComponent(value)}`;
|
|
@@ -384,6 +415,15 @@ const DEBUG_TOOL_DEFINITIONS = [
|
|
|
384
415
|
required: []
|
|
385
416
|
}
|
|
386
417
|
},
|
|
418
|
+
{
|
|
419
|
+
name: "measure_safe_area",
|
|
420
|
+
description: "Runs a safe-area probe on the attached mini-app page via Runtime.evaluate and returns normalized safe-area insets, viewport geometry, device pixel ratio, and User-Agent. Read-only — does not modify page state. Use in a relay session (phone attached) to get ground-truth values for upgrading a viewport preset from extrapolated/placeholder to measured. Requires the relay to be attached — call list_pages first.",
|
|
421
|
+
inputSchema: {
|
|
422
|
+
type: "object",
|
|
423
|
+
properties: {},
|
|
424
|
+
required: []
|
|
425
|
+
}
|
|
426
|
+
},
|
|
387
427
|
{
|
|
388
428
|
name: "AIT.getSdkCallHistory",
|
|
389
429
|
description: "Returns the recent Apps In Toss SDK call trace (method, args, result/error, timestamp) that raw CDP cannot observe. Read-only. Use to confirm an SDK call fired and how it resolved (e.g. a saveBase64Data permission regression).",
|
|
@@ -498,6 +538,45 @@ async function takeScreenshot(connection) {
|
|
|
498
538
|
mimeType: "image/png"
|
|
499
539
|
};
|
|
500
540
|
}
|
|
541
|
+
`
|
|
542
|
+
(function() {
|
|
543
|
+
var el = document.createElement('div');
|
|
544
|
+
el.style.cssText = 'position:fixed;top:0;left:0;width:0;height:0;visibility:hidden;' +
|
|
545
|
+
'padding-top:env(safe-area-inset-top,0px);' +
|
|
546
|
+
'padding-right:env(safe-area-inset-right,0px);' +
|
|
547
|
+
'padding-bottom:env(safe-area-inset-bottom,0px);' +
|
|
548
|
+
'padding-left:env(safe-area-inset-left,0px)';
|
|
549
|
+
document.documentElement.appendChild(el);
|
|
550
|
+
var cs = window.getComputedStyle(el);
|
|
551
|
+
var cssEnv = {
|
|
552
|
+
top: parseFloat(cs.paddingTop) || 0,
|
|
553
|
+
right: parseFloat(cs.paddingRight) || 0,
|
|
554
|
+
bottom: parseFloat(cs.paddingBottom) || 0,
|
|
555
|
+
left: parseFloat(cs.paddingLeft) || 0
|
|
556
|
+
};
|
|
557
|
+
document.documentElement.removeChild(el);
|
|
558
|
+
var sdkInsets = null;
|
|
559
|
+
try {
|
|
560
|
+
if (typeof SafeAreaInsets !== 'undefined' && SafeAreaInsets && typeof SafeAreaInsets.get === 'function') {
|
|
561
|
+
sdkInsets = SafeAreaInsets.get();
|
|
562
|
+
}
|
|
563
|
+
} catch(_) {}
|
|
564
|
+
var navBarHeight = null;
|
|
565
|
+
try {
|
|
566
|
+
var nb = document.querySelector('.ait-navbar');
|
|
567
|
+
if (nb) navBarHeight = nb.getBoundingClientRect().height;
|
|
568
|
+
} catch(_) {}
|
|
569
|
+
return JSON.stringify({
|
|
570
|
+
cssEnv: cssEnv,
|
|
571
|
+
sdkInsets: sdkInsets,
|
|
572
|
+
navBarHeight: navBarHeight,
|
|
573
|
+
innerWidth: window.innerWidth,
|
|
574
|
+
innerHeight: window.innerHeight,
|
|
575
|
+
devicePixelRatio: window.devicePixelRatio,
|
|
576
|
+
userAgent: navigator.userAgent
|
|
577
|
+
});
|
|
578
|
+
})()
|
|
579
|
+
`.trim();
|
|
501
580
|
/** Set of tool names served by the AIT source rather than the CDP connection. */
|
|
502
581
|
const AIT_TOOL_NAMES = new Set([
|
|
503
582
|
"AIT.getSdkCallHistory",
|
|
@@ -521,16 +600,97 @@ function getOperationalEnvironment(source) {
|
|
|
521
600
|
return source.get("AIT.getOperationalEnvironment");
|
|
522
601
|
}
|
|
523
602
|
//#endregion
|
|
603
|
+
//#region src/mcp/totp.ts
|
|
604
|
+
/**
|
|
605
|
+
* RFC 6238 TOTP implementation (Node.js, node:crypto only).
|
|
606
|
+
*
|
|
607
|
+
* External TOTP libraries (otplib, speakeasy, …) are intentionally NOT used
|
|
608
|
+
* to keep the dependency surface minimal. This hand-roll is ~30 lines and
|
|
609
|
+
* covers exactly what relay-side auth needs.
|
|
610
|
+
*
|
|
611
|
+
* Algorithm summary (RFC 6238 + RFC 4226):
|
|
612
|
+
* T = floor(now / 30) — 30-second time step counter
|
|
613
|
+
* K = Buffer.from(secret, 'hex') — shared secret (raw bytes, hex-encoded)
|
|
614
|
+
* MAC = HMAC-SHA1(K, T as 8-byte big-endian uint64)
|
|
615
|
+
* offset = MAC[19] & 0x0f
|
|
616
|
+
* code = (MAC[offset..offset+4] & 0x7fffffff) % 10^6 — 6 digits
|
|
617
|
+
*
|
|
618
|
+
* Security note (keep this comment accurate):
|
|
619
|
+
* The baked-in secret in a dogfood build is extractable from the bundle by a
|
|
620
|
+
* determined reverse engineer. This mechanism raises the bar from
|
|
621
|
+
* "anyone with the URL" to "URL + bundle extraction + live TOTP calculation".
|
|
622
|
+
* Casual URL leaks (Slack paste, QR screenshot, shoulder-surfing) are
|
|
623
|
+
* blocked; deliberate reverse engineering is not. See threat model in
|
|
624
|
+
* src/mcp/chii-relay.ts and umbrella CLAUDE.md §4.
|
|
625
|
+
*
|
|
626
|
+
* SECRET-HANDLING: secret values and computed codes MUST NOT appear in any
|
|
627
|
+
* log, error message, or string visible outside this module. Only boolean
|
|
628
|
+
* pass/fail and reason enum values are safe to surface.
|
|
629
|
+
*/
|
|
630
|
+
/** Time step window in seconds (RFC 6238 default). */
|
|
631
|
+
const TIME_STEP = 30;
|
|
632
|
+
/** Number of digits in the generated code. */
|
|
633
|
+
const DIGITS = 6;
|
|
634
|
+
/**
|
|
635
|
+
* Derives a 6-digit TOTP code from a hex-encoded secret at the given wall-
|
|
636
|
+
* clock time.
|
|
637
|
+
*
|
|
638
|
+
* @param secret - The shared secret as a hex string (e.g. 64 hex chars = 32
|
|
639
|
+
* bytes). Must be the output of `generateAttachToken()` or compatible.
|
|
640
|
+
* @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
|
|
641
|
+
* @returns A zero-padded 6-digit decimal string, e.g. `"042193"`.
|
|
642
|
+
*/
|
|
643
|
+
function generateTotp(secret, when = Date.now()) {
|
|
644
|
+
const key = Buffer.from(secret, "hex");
|
|
645
|
+
const counter = Math.max(0, Math.floor(when / 1e3 / TIME_STEP));
|
|
646
|
+
const counterBuf = Buffer.alloc(8);
|
|
647
|
+
const hi = Math.floor(counter / 4294967296);
|
|
648
|
+
const lo = counter >>> 0;
|
|
649
|
+
counterBuf.writeUInt32BE(hi, 0);
|
|
650
|
+
counterBuf.writeUInt32BE(lo, 4);
|
|
651
|
+
const mac = createHmac("sha1", key).update(counterBuf).digest();
|
|
652
|
+
const offset = mac[19] & 15;
|
|
653
|
+
return (((mac[offset] & 127) << 24 | (mac[offset + 1] & 255) << 16 | (mac[offset + 2] & 255) << 8 | mac[offset + 3] & 255) % 10 ** DIGITS).toString().padStart(DIGITS, "0");
|
|
654
|
+
}
|
|
655
|
+
/**
|
|
656
|
+
* Verifies a TOTP code against the secret, accepting ±`skew` time steps to
|
|
657
|
+
* tolerate clock drift between the relay host and the client device.
|
|
658
|
+
*
|
|
659
|
+
* Uses `timingSafeEqual` for constant-time comparison to prevent timing
|
|
660
|
+
* side-channel attacks.
|
|
661
|
+
*
|
|
662
|
+
* @param secret - Hex-encoded shared secret.
|
|
663
|
+
* @param code - The 6-digit code to verify (string or numeric).
|
|
664
|
+
* @param when - Unix timestamp in milliseconds. Defaults to `Date.now()`.
|
|
665
|
+
* @param skew - Number of adjacent steps to accept on either side. Default 1
|
|
666
|
+
* (accepts T-1, T, T+1 — a 90-second acceptance window).
|
|
667
|
+
* @returns `true` if the code matches any accepted step, `false` otherwise.
|
|
668
|
+
*/
|
|
669
|
+
function verifyTotp(secret, code, when = Date.now(), skew = 1) {
|
|
670
|
+
const normalised = String(code).padStart(DIGITS, "0");
|
|
671
|
+
if (normalised.length !== DIGITS || !/^\d{6}$/.test(normalised)) return false;
|
|
672
|
+
const candidateBuf = Buffer.from(normalised, "utf8");
|
|
673
|
+
for (let delta = -skew; delta <= skew; delta++) {
|
|
674
|
+
const expected = generateTotp(secret, when + delta * TIME_STEP * 1e3);
|
|
675
|
+
if (timingSafeEqual(Buffer.from(expected, "utf8"), candidateBuf)) return true;
|
|
676
|
+
}
|
|
677
|
+
return false;
|
|
678
|
+
}
|
|
679
|
+
//#endregion
|
|
524
680
|
//#region src/mcp/tunnel.ts
|
|
525
681
|
/**
|
|
526
682
|
* cloudflared quick tunnel + attach banner for the debug-mode MCP server.
|
|
527
683
|
*
|
|
528
684
|
* On spawn, the debug server opens an accountless `*.trycloudflare.com` quick
|
|
529
685
|
* tunnel to the local Chii relay so the phone can attach over a public wss URL,
|
|
530
|
-
* then prints
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
686
|
+
* then prints an ASCII QR + attach instructions. When TOTP auth is enabled
|
|
687
|
+
* (`AIT_DEBUG_TOTP_SECRET` is set), the QR encodes only the base relay URL —
|
|
688
|
+
* the TOTP code (`at=`) is NOT included because it rotates every 30 s and
|
|
689
|
+
* would be stale by the time a human scans. The in-app deep-link builder
|
|
690
|
+
* splices the live code at attach time.
|
|
691
|
+
*
|
|
692
|
+
* SECRET-HANDLING: The TOTP secret and computed code values MUST NOT appear
|
|
693
|
+
* in any output from this module.
|
|
534
694
|
*
|
|
535
695
|
* Node-only: spawns the cloudflared binary and writes to stdout/stderr.
|
|
536
696
|
*/
|
|
@@ -580,22 +740,31 @@ async function startQuickTunnel(localPort) {
|
|
|
580
740
|
}
|
|
581
741
|
};
|
|
582
742
|
}
|
|
583
|
-
/**
|
|
743
|
+
/**
|
|
744
|
+
* Renders the attach banner (relay URL + ASCII QR) as a string.
|
|
745
|
+
*
|
|
746
|
+
* The QR encodes the base `wssUrl` only. When `totpEnabled` is true, a note
|
|
747
|
+
* is added that attach URLs generated by `build_attach_url` will include a
|
|
748
|
+
* live TOTP code (`at=`) appended at call time.
|
|
749
|
+
*
|
|
750
|
+
* SECRET-HANDLING: no secret value, TOTP code, or intermediate value is
|
|
751
|
+
* included in this output.
|
|
752
|
+
*/
|
|
584
753
|
async function renderAttachBanner(input) {
|
|
585
|
-
const payload = `${input.wssUrl}?token=${input.token}`;
|
|
586
754
|
const qr = await new Promise((resolve) => {
|
|
587
|
-
qrcode.generate(
|
|
755
|
+
qrcode.generate(input.wssUrl, { small: true }, (rendered) => resolve(rendered));
|
|
588
756
|
});
|
|
757
|
+
const authNote = input.totpEnabled ? " auth: TOTP enabled — attach URLs include a rotating code (at=)." : " auth: none (set AIT_DEBUG_TOTP_SECRET to enable TOTP).";
|
|
589
758
|
return [
|
|
590
759
|
"",
|
|
591
760
|
"AIT debug — attach a mini-app to this session",
|
|
592
761
|
"",
|
|
593
762
|
` relay (wss): ${input.wssUrl}`,
|
|
594
|
-
|
|
595
|
-
` (token is a pairing hint — relay-side validation lands in a later phase)`,
|
|
763
|
+
authNote,
|
|
596
764
|
"",
|
|
597
|
-
"
|
|
598
|
-
"
|
|
765
|
+
" Use build_attach_url to generate a deep link with the current TOTP code.",
|
|
766
|
+
" Scan the QR to locate the relay (open the dogfood URL separately with",
|
|
767
|
+
" ?debug=1&relay=<wss>&at=<code> or use the build_attach_url tool):",
|
|
599
768
|
"",
|
|
600
769
|
qr
|
|
601
770
|
].join("\n");
|
|
@@ -635,7 +804,7 @@ function createDebugServer(deps) {
|
|
|
635
804
|
const { connection, aitSource, getTunnelStatus } = deps;
|
|
636
805
|
const server = new Server({
|
|
637
806
|
name: "ait-debug",
|
|
638
|
-
version: "0.1.
|
|
807
|
+
version: "0.1.33"
|
|
639
808
|
}, { capabilities: { tools: {} } });
|
|
640
809
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEBUG_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
641
810
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|
|
@@ -734,21 +903,49 @@ function errorResult(err, name) {
|
|
|
734
903
|
};
|
|
735
904
|
}
|
|
736
905
|
/**
|
|
906
|
+
* Reads `AIT_DEBUG_TOTP_SECRET` from `process.env` at runtime and builds a
|
|
907
|
+
* `verifyAuth` predicate for the Chii relay's WebSocket upgrade gate.
|
|
908
|
+
*
|
|
909
|
+
* The predicate checks the `at` query parameter against the current and
|
|
910
|
+
* adjacent TOTP time steps (±1 skew) using `verifyTotp`.
|
|
911
|
+
*
|
|
912
|
+
* Returns `undefined` when the env var is not set — callers treat that as
|
|
913
|
+
* "auth disabled" (no predicate registered on the relay).
|
|
914
|
+
*
|
|
915
|
+
* SECRET-HANDLING: The secret value read from env is captured in a closure and
|
|
916
|
+
* is NEVER written to any log, error message, or process output.
|
|
917
|
+
*/
|
|
918
|
+
function buildRelayVerifyAuth() {
|
|
919
|
+
const secret = process.env.AIT_DEBUG_TOTP_SECRET;
|
|
920
|
+
if (!secret) return void 0;
|
|
921
|
+
return (req) => {
|
|
922
|
+
const rawUrl = req.url ?? "";
|
|
923
|
+
const qIndex = rawUrl.indexOf("?");
|
|
924
|
+
const queryStr = qIndex === -1 ? "" : rawUrl.slice(qIndex + 1);
|
|
925
|
+
return verifyTotp(secret, new URLSearchParams(queryStr).get("at") ?? "");
|
|
926
|
+
};
|
|
927
|
+
}
|
|
928
|
+
/**
|
|
737
929
|
* Boots the live debug stack and serves it over stdio:
|
|
738
|
-
* 1. start the Chii relay,
|
|
930
|
+
* 1. start the Chii relay (with TOTP auth if AIT_DEBUG_TOTP_SECRET is set),
|
|
739
931
|
* 2. open a cloudflared quick tunnel to it,
|
|
740
|
-
* 3. print
|
|
932
|
+
* 3. print relay URL + attach instructions,
|
|
741
933
|
* 4. expose the debug tools backed by a `ChiiCdpConnection` + `ChiiAitSource`.
|
|
742
934
|
*/
|
|
743
935
|
async function runDebugServer(options = {}) {
|
|
744
936
|
const relayPort = options.relayPort ?? 9100;
|
|
745
|
-
const
|
|
937
|
+
const verifyAuth = buildRelayVerifyAuth();
|
|
938
|
+
const totpEnabled = verifyAuth !== void 0;
|
|
939
|
+
const relay = await startChiiRelay({
|
|
940
|
+
port: relayPort,
|
|
941
|
+
verifyAuth
|
|
942
|
+
});
|
|
746
943
|
let tunnel = null;
|
|
747
944
|
let tunnelStatus = {
|
|
748
945
|
up: false,
|
|
749
946
|
wssUrl: null
|
|
750
947
|
};
|
|
751
|
-
|
|
948
|
+
generateAttachToken();
|
|
752
949
|
try {
|
|
753
950
|
tunnel = await startQuickTunnel(relayPort);
|
|
754
951
|
tunnelStatus = {
|
|
@@ -757,7 +954,7 @@ async function runDebugServer(options = {}) {
|
|
|
757
954
|
};
|
|
758
955
|
await printAttachBanner({
|
|
759
956
|
wssUrl: tunnel.wssUrl,
|
|
760
|
-
|
|
957
|
+
totpEnabled
|
|
761
958
|
});
|
|
762
959
|
} catch (err) {
|
|
763
960
|
const message = err instanceof Error ? err.message : String(err);
|
|
@@ -809,7 +1006,10 @@ var HttpAitSource = class {
|
|
|
809
1006
|
sdkVersion: typeof state.appVersion === "string" ? state.appVersion : null
|
|
810
1007
|
};
|
|
811
1008
|
}
|
|
812
|
-
case "AIT.getSdkCallHistory":
|
|
1009
|
+
case "AIT.getSdkCallHistory": {
|
|
1010
|
+
const raw = (await this.fetchState()).sdkCallLog;
|
|
1011
|
+
return { calls: Array.isArray(raw) ? raw : [] };
|
|
1012
|
+
}
|
|
813
1013
|
default: throw new Error(`Unknown AIT method: ${String(method)}`);
|
|
814
1014
|
}
|
|
815
1015
|
}
|
|
@@ -897,7 +1097,7 @@ function createDevServer(deps = {}) {
|
|
|
897
1097
|
const aitSource = deps.aitSource ?? new HttpAitSource({ stateEndpoint });
|
|
898
1098
|
const server = new Server({
|
|
899
1099
|
name: "ait-devtools",
|
|
900
|
-
version: "0.1.
|
|
1100
|
+
version: "0.1.33"
|
|
901
1101
|
}, { capabilities: { tools: {} } });
|
|
902
1102
|
server.setRequestHandler(ListToolsRequestSchema, () => ({ tools: DEV_TOOL_DEFINITIONS.map((tool) => ({ ...tool })) }));
|
|
903
1103
|
server.setRequestHandler(CallToolRequestSchema, async (request) => {
|