@ait-co/devtools 0.1.112 → 0.1.115
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/bundle-KFs4t-wc.d.ts.map +1 -1
- package/dist/mcp/cli.js +218 -28
- package/dist/mcp/cli.js.map +1 -1
- package/dist/mcp/server.js +1 -1
- package/dist/panel/index.js +7 -1
- package/dist/panel/index.js.map +1 -1
- package/dist/{pool-mZlgCmNQ.d.ts → pool-D23t4ibd.d.ts} +2 -2
- package/dist/{pool-mZlgCmNQ.d.ts.map → pool-D23t4ibd.d.ts.map} +1 -1
- package/dist/{qr-http-server-BVS-HZjU.cjs → qr-http-server-BpTvfeku.cjs} +92 -8
- package/dist/qr-http-server-BpTvfeku.cjs.map +1 -0
- package/dist/{qr-http-server-BJJt3ush.js → qr-http-server-BsWlOA85.js} +92 -8
- package/dist/qr-http-server-BsWlOA85.js.map +1 -0
- package/dist/{qr-http-server-C1T4RNbq.cjs → qr-http-server-CrVKno9V.cjs} +92 -8
- package/dist/qr-http-server-CrVKno9V.cjs.map +1 -0
- package/dist/{qr-http-server-Cs93vEPH.js → qr-http-server-UBV2qUEd.js} +92 -8
- package/dist/qr-http-server-UBV2qUEd.js.map +1 -0
- package/dist/{relay-worker-Dppp2yZj.d.ts → relay-worker-DERQUao2.d.ts} +2 -2
- package/dist/{relay-worker-Dppp2yZj.d.ts.map → relay-worker-DERQUao2.d.ts.map} +1 -1
- package/dist/runtime-BKMMoeMj.d.ts +153 -0
- package/dist/runtime-BKMMoeMj.d.ts.map +1 -0
- package/dist/test-runner/bundle.js +124 -18
- package/dist/test-runner/bundle.js.map +1 -1
- package/dist/test-runner/cli.js +125 -19
- package/dist/test-runner/cli.js.map +1 -1
- package/dist/test-runner/config.d.ts +1 -1
- package/dist/test-runner/pool.d.ts +1 -1
- package/dist/test-runner/relay-worker.d.ts +1 -1
- package/dist/test-runner/rpc.d.ts +1 -1
- package/dist/test-runner/runtime.d.ts +2 -2
- package/dist/test-runner/runtime.js +296 -18
- package/dist/test-runner/runtime.js.map +1 -1
- package/dist/test-runner/task-graph.d.ts +1 -1
- package/dist/{tunnel-Cpn3mA4u.js → tunnel-B7U5k1xa.js} +2 -2
- package/dist/{tunnel-Cpn3mA4u.js.map → tunnel-B7U5k1xa.js.map} +1 -1
- package/dist/{tunnel-Dj8Kf2QS.cjs → tunnel-C-cgMnyY.cjs} +2 -2
- package/dist/{tunnel-Dj8Kf2QS.cjs.map → tunnel-C-cgMnyY.cjs.map} +1 -1
- package/dist/unplugin/index.cjs +1 -1
- package/dist/unplugin/index.js +1 -1
- package/dist/unplugin/tunnel.cjs +1 -1
- package/dist/unplugin/tunnel.js +1 -1
- package/package.json +1 -1
- package/dist/qr-http-server-BJJt3ush.js.map +0 -1
- package/dist/qr-http-server-BVS-HZjU.cjs.map +0 -1
- package/dist/qr-http-server-C1T4RNbq.cjs.map +0 -1
- package/dist/qr-http-server-Cs93vEPH.js.map +0 -1
- package/dist/runtime-C7uxh3Mf.d.ts +0 -62
- package/dist/runtime-C7uxh3Mf.d.ts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"runtime.js","names":["#received","#negated","#assert"],"sources":["../../src/test-runner/runtime.ts"],"sourcesContent":["/**\n * Thin test runtime for WebView execution.\n *\n * This file is bundled by `bundle.ts` together with the user's test file\n * into a single IIFE injected into the WebView via `Runtime.evaluate`.\n * It MUST stay browser-compatible — no Node.js APIs allowed.\n *\n * Design: rather than shipping @vitest/runner verbatim into a WebView (where\n * its Node-side internals cause issues), this runtime provides a minimal\n * compatible API surface — describe/it/test/expect globals — collects results\n * into a plain JSON-safe object, and exports `runTestModule` as the entry point.\n *\n * @vitest/runner and @vitest/expect are listed as dependencies so that the\n * package's type contracts are available and so the browser-compatible subsets\n * can be referenced. The Vitest custom pool that drives this runtime through\n * Vitest's `PoolRunnerInitializer` lives in `pool.ts`.\n *\n * NOTE: this file is imported by type from Node-side code (rpc.ts / relay-worker.ts)\n * for the RunReport / TestResult type shapes. The runtime ITSELF is not imported\n * at runtime on the Node side — only the types are used.\n */\n\n/* -------------------------------------------------------------------------- */\n/* Public result types (used by rpc.ts and relay-worker.ts) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result of a single test case.\n * All fields are JSON-serialisable.\n */\nexport interface TestResult {\n /** Full dot-joined test name including nested suite names. */\n name: string;\n /** `'pass'` or `'fail'`. `'skip'` for skipped tests. */\n status: 'pass' | 'fail' | 'skip';\n /** Duration in milliseconds. */\n duration: number;\n /** Error message (fail only). Does NOT include the expression/secret. */\n error?: string;\n}\n\n/** Aggregate report returned by `runTestModule`. */\nexport interface RunReport {\n /** ISO timestamp of when `runTestModule` was called. */\n startedAt: string;\n /** Total elapsed milliseconds (wall-clock). */\n duration: number;\n passed: number;\n failed: number;\n skipped: number;\n tests: TestResult[];\n}\n\n/* -------------------------------------------------------------------------- */\n/* Lightweight expect implementation */\n/* -------------------------------------------------------------------------- */\n\n/** Thrown by expect matchers on failure. */\nclass AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AssertionError';\n }\n}\n\n/** Minimal expect builder compatible with Vitest's jest-style API. */\nclass Expectation {\n #received: unknown;\n #negated = false;\n\n constructor(received: unknown) {\n this.#received = received;\n }\n\n get not(): this {\n const neg = new Expectation(this.#received) as this;\n neg.#negated = true;\n return neg;\n }\n\n #assert(pass: boolean, msg: string): void {\n const actual = this.#negated ? !pass : pass;\n if (!actual) {\n throw new AssertionError(this.#negated ? `Expected NOT: ${msg}` : msg);\n }\n }\n\n toBe(expected: unknown): void {\n this.#assert(\n Object.is(this.#received, expected),\n `Expected ${String(expected)}, received ${String(this.#received)}`,\n );\n }\n\n toEqual(expected: unknown): void {\n this.#assert(\n JSON.stringify(this.#received) === JSON.stringify(expected),\n `Expected ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toBeTruthy(): void {\n this.#assert(Boolean(this.#received), `Expected truthy, received ${String(this.#received)}`);\n }\n\n toBeFalsy(): void {\n this.#assert(!this.#received, `Expected falsy, received ${String(this.#received)}`);\n }\n\n toBeNull(): void {\n this.#assert(this.#received === null, `Expected null, received ${String(this.#received)}`);\n }\n\n toBeUndefined(): void {\n this.#assert(\n this.#received === undefined,\n `Expected undefined, received ${String(this.#received)}`,\n );\n }\n\n toBeGreaterThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received > n,\n `Expected > ${n}, received ${String(this.#received)}`,\n );\n }\n\n toBeLessThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received < n,\n `Expected < ${n}, received ${String(this.#received)}`,\n );\n }\n\n toContain(sub: string): void {\n this.#assert(\n typeof this.#received === 'string' && this.#received.includes(sub),\n `Expected to contain \"${sub}\", received \"${String(this.#received)}\"`,\n );\n }\n\n toThrow(msgFragment?: string): void {\n if (typeof this.#received !== 'function') {\n throw new AssertionError('toThrow: expected a function');\n }\n let threw = false;\n let errorMsg = '';\n try {\n (this.#received as () => unknown)();\n } catch (e) {\n threw = true;\n errorMsg = e instanceof Error ? e.message : String(e);\n }\n if (msgFragment !== undefined) {\n this.#assert(\n threw && errorMsg.includes(msgFragment),\n `Expected to throw containing \"${msgFragment}\", got \"${errorMsg}\"`,\n );\n } else {\n this.#assert(threw, 'Expected function to throw');\n }\n }\n}\n\n/** The `expect` function installed as a global. */\nfunction expect(received: unknown): Expectation {\n return new Expectation(received);\n}\n\n/* -------------------------------------------------------------------------- */\n/* describe / it / test registry */\n/* -------------------------------------------------------------------------- */\n\ninterface PendingTest {\n suitePath: string[];\n name: string;\n fn: () => void | Promise<void>;\n skip: boolean;\n}\n\nconst _pendingTests: PendingTest[] = [];\nconst _suiteStack: string[] = [];\n\n/** Registers a suite scope; calls `fn` synchronously to collect inner tests. */\nfunction describe(name: string, fn: () => void): void {\n _suiteStack.push(name);\n fn();\n _suiteStack.pop();\n}\n\n/** Registers a test. */\nfunction it(name: string, fn: () => void | Promise<void>): void {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip: false });\n}\n\n/** Alias for `it`. */\nconst test = it;\n\n/** Skipped test — registered but not executed. */\ndescribe.skip = (name: string, _fn: () => void): void => {\n void name;\n};\nit.skip = (name: string, _fn?: () => void | Promise<void>): void => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn: () => {}, skip: true });\n};\ntest.skip = it.skip;\n\n/* -------------------------------------------------------------------------- */\n/* Runtime entry point */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Installs describe/it/test/expect as globals, invokes `moduleFactory` to\n * register the user's tests, then executes them and returns a RunReport.\n *\n * This function is exported as `__testBundle.runTestModule` by the IIFE wrapper\n * that bundle.ts generates. The Node-side rpc.ts calls it via `Runtime.evaluate`.\n *\n * @param moduleFactory - A zero-argument function that contains the user's\n * top-level test code (describe/it/test calls). The bundler wraps the entire\n * test module so that its top-level statements become the body of this factory.\n */\nexport async function runTestModule(\n moduleFactory?: () => void | Promise<void>,\n): Promise<RunReport> {\n // Reset state for re-entrant calls within the same page context.\n _pendingTests.length = 0;\n _suiteStack.length = 0;\n\n // Install globals that user test code expects.\n type G = typeof globalThis & {\n describe: typeof describe;\n it: typeof it;\n test: typeof test;\n expect: typeof expect;\n };\n const g = globalThis as G;\n g.describe = describe;\n g.it = it;\n g.test = test;\n g.expect = expect;\n\n // Run the factory (which registers describe/it/test blocks via globals).\n if (moduleFactory) {\n await moduleFactory();\n }\n\n const wallStart = Date.now();\n const startedAt = new Date(wallStart).toISOString();\n const results: TestResult[] = [];\n\n for (const pending of _pendingTests) {\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n if (pending.skip) {\n results.push({ name: fullName, status: 'skip', duration: 0 });\n continue;\n }\n const tStart = Date.now();\n try {\n await pending.fn();\n results.push({ name: fullName, status: 'pass', duration: Date.now() - tStart });\n } catch (e) {\n const errorMsg = e instanceof Error ? e.message : String(e);\n results.push({\n name: fullName,\n status: 'fail',\n duration: Date.now() - tStart,\n error: errorMsg,\n });\n }\n }\n\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n\n return { startedAt, duration, passed, failed, skipped, tests: results };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"runtime.js","names":["#received","#negated","#assert"],"sources":["../../src/test-runner/runtime.ts"],"sourcesContent":["/**\n * Thin test runtime for WebView execution.\n *\n * This file is bundled by `bundle.ts` together with the user's test file\n * into a single IIFE injected into the WebView via `Runtime.evaluate`.\n * It MUST stay browser-compatible — no Node.js APIs allowed.\n *\n * Design: rather than shipping @vitest/runner verbatim into a WebView (where\n * its Node-side internals cause issues), this runtime provides a minimal\n * compatible API surface — describe/it/test/expect globals — collects results\n * into a plain JSON-safe object, and exports `runTestModule` as the entry point.\n *\n * @vitest/runner and @vitest/expect are listed as dependencies so that the\n * package's type contracts are available and so the browser-compatible subsets\n * can be referenced. The Vitest custom pool that drives this runtime through\n * Vitest's `PoolRunnerInitializer` lives in `pool.ts`.\n *\n * NOTE: this file is imported by type from Node-side code (rpc.ts / relay-worker.ts)\n * for the RunReport / TestResult type shapes. The runtime ITSELF is not imported\n * at runtime on the Node side — only the types are used.\n */\n\n/* -------------------------------------------------------------------------- */\n/* Public result types (used by rpc.ts and relay-worker.ts) */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Result of a single test case.\n * All fields are JSON-serialisable.\n */\nexport interface TestResult {\n /** Full dot-joined test name including nested suite names. */\n name: string;\n /** `'pass'` or `'fail'`. `'skip'` for skipped tests. */\n status: 'pass' | 'fail' | 'skip';\n /** Duration in milliseconds. */\n duration: number;\n /** Error message (fail only). Does NOT include the expression/secret. */\n error?: string;\n}\n\n/** Aggregate report returned by `runTestModule`. */\nexport interface RunReport {\n /** ISO timestamp of when `runTestModule` was called. */\n startedAt: string;\n /** Total elapsed milliseconds (wall-clock). */\n duration: number;\n passed: number;\n failed: number;\n skipped: number;\n tests: TestResult[];\n}\n\n/* -------------------------------------------------------------------------- */\n/* Deep equality helpers */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Recursive structural equality — replaces JSON.stringify comparison to\n * handle key-order differences and undefined values correctly.\n */\nfunction deepEqual(a: unknown, b: unknown): boolean {\n if (Object.is(a, b)) return true;\n if (a === null || b === null) return false;\n if (typeof a !== 'object' || typeof b !== 'object') return false;\n\n if (Array.isArray(a) && Array.isArray(b)) {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) {\n if (!deepEqual(a[i], b[i])) return false;\n }\n return true;\n }\n if (Array.isArray(a) || Array.isArray(b)) return false;\n\n const aObj = a as Record<string, unknown>;\n const bObj = b as Record<string, unknown>;\n const aKeys = Object.keys(aObj);\n const bKeys = Object.keys(bObj);\n if (aKeys.length !== bKeys.length) return false;\n for (const key of aKeys) {\n if (!Object.hasOwn(bObj, key)) return false;\n if (!deepEqual(aObj[key], bObj[key])) return false;\n }\n return true;\n}\n\n/**\n * Partial-match: every key in `expected` must exist in `received` with a\n * recursively matching value. Extra keys in `received` are ignored.\n */\nfunction deepMatchObject(received: unknown, expected: unknown): boolean {\n if (Object.is(received, expected)) return true;\n if (expected === null || received === null) return Object.is(received, expected);\n if (typeof expected !== 'object' || typeof received !== 'object')\n return Object.is(received, expected);\n\n if (Array.isArray(expected) && Array.isArray(received)) {\n if (expected.length !== received.length) return false;\n for (let i = 0; i < expected.length; i++) {\n if (!deepMatchObject(received[i], expected[i])) return false;\n }\n return true;\n }\n if (Array.isArray(expected) || Array.isArray(received)) return false;\n\n const expObj = expected as Record<string, unknown>;\n const recObj = received as Record<string, unknown>;\n for (const key of Object.keys(expObj)) {\n if (!Object.hasOwn(recObj, key)) return false;\n if (!deepMatchObject(recObj[key], expObj[key])) return false;\n }\n return true;\n}\n\n/**\n * Resolves a dot-separated property path on an object.\n * Returns `{ found: true, value }` or `{ found: false }`.\n */\nfunction resolvePath(obj: unknown, dotPath: string): { found: boolean; value?: unknown } {\n const parts = dotPath.split('.');\n let cur: unknown = obj;\n for (const part of parts) {\n if (cur === null || cur === undefined || typeof cur !== 'object') return { found: false };\n const record = cur as Record<string, unknown>;\n if (!Object.hasOwn(record, part)) return { found: false };\n cur = record[part];\n }\n return { found: true, value: cur };\n}\n\n/* -------------------------------------------------------------------------- */\n/* Lightweight expect implementation */\n/* -------------------------------------------------------------------------- */\n\n/** Thrown by expect matchers on failure. */\nclass AssertionError extends Error {\n constructor(message: string) {\n super(message);\n this.name = 'AssertionError';\n }\n}\n\n/** Minimal expect builder compatible with Vitest's jest-style API. */\nclass Expectation {\n #received: unknown;\n #negated = false;\n\n constructor(received: unknown) {\n this.#received = received;\n }\n\n get not(): this {\n const neg = new Expectation(this.#received) as this;\n neg.#negated = true;\n return neg;\n }\n\n #assert(pass: boolean, msg: string): void {\n const actual = this.#negated ? !pass : pass;\n if (!actual) {\n throw new AssertionError(this.#negated ? `Expected NOT: ${msg}` : msg);\n }\n }\n\n toBe(expected: unknown): void {\n this.#assert(\n Object.is(this.#received, expected),\n `Expected ${String(expected)}, received ${String(this.#received)}`,\n );\n }\n\n toEqual(expected: unknown): void {\n this.#assert(\n deepEqual(this.#received, expected),\n `Expected ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toBeTruthy(): void {\n this.#assert(Boolean(this.#received), `Expected truthy, received ${String(this.#received)}`);\n }\n\n toBeFalsy(): void {\n this.#assert(!this.#received, `Expected falsy, received ${String(this.#received)}`);\n }\n\n toBeNull(): void {\n this.#assert(this.#received === null, `Expected null, received ${String(this.#received)}`);\n }\n\n toBeUndefined(): void {\n this.#assert(\n this.#received === undefined,\n `Expected undefined, received ${String(this.#received)}`,\n );\n }\n\n toBeGreaterThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received > n,\n `Expected > ${n}, received ${String(this.#received)}`,\n );\n }\n\n toBeLessThan(n: number): void {\n this.#assert(\n typeof this.#received === 'number' && this.#received < n,\n `Expected < ${n}, received ${String(this.#received)}`,\n );\n }\n\n toContain(sub: string): void {\n this.#assert(\n typeof this.#received === 'string' && this.#received.includes(sub),\n `Expected to contain \"${sub}\", received \"${String(this.#received)}\"`,\n );\n }\n\n toThrow(msgFragment?: string): void {\n if (typeof this.#received !== 'function') {\n throw new AssertionError('toThrow: expected a function');\n }\n let threw = false;\n let errorMsg = '';\n try {\n (this.#received as () => unknown)();\n } catch (e) {\n threw = true;\n errorMsg = e instanceof Error ? e.message : String(e);\n }\n if (msgFragment !== undefined) {\n this.#assert(\n threw && errorMsg.includes(msgFragment),\n `Expected to throw containing \"${msgFragment}\", got \"${errorMsg}\"`,\n );\n } else {\n this.#assert(threw, 'Expected function to throw');\n }\n }\n\n // ---- New matchers (devtools#683) -----------------------------------------\n\n toMatchObject(expected: Record<string, unknown>): void {\n this.#assert(\n deepMatchObject(this.#received, expected),\n `Expected object to match ${JSON.stringify(expected)}, received ${JSON.stringify(this.#received)}`,\n );\n }\n\n toHaveProperty(dotPath: string, ...rest: unknown[]): void {\n const { found, value: actual } = resolvePath(this.#received, dotPath);\n if (rest.length > 0) {\n const value = rest[0];\n this.#assert(\n found && deepEqual(actual, value),\n `Expected property \"${dotPath}\" to equal ${JSON.stringify(value)}, got ${JSON.stringify(actual)}`,\n );\n } else {\n this.#assert(found, `Expected property \"${dotPath}\" to exist`);\n }\n }\n\n // The `abstract new (...args: never) => unknown` signature is the widest\n // constructor type that TypeScript allows without explicit `any`. Real-world\n // constructors like `ErrorConstructor` are assignable to it because `never`\n // in parameter position is contravariant (any arg list satisfies `never[]`).\n toBeInstanceOf(ctor: abstract new (...args: never) => unknown): void {\n this.#assert(\n this.#received instanceof (ctor as new (...args: unknown[]) => unknown),\n `Expected instance of ${(ctor as { name?: string }).name ?? String(ctor)}, received ${String(this.#received)}`,\n );\n }\n\n toBeTypeOf(typeStr: string): void {\n this.#assert(\n typeof this.#received === typeStr,\n `Expected typeof \"${typeStr}\", received \"${typeof this.#received}\"`,\n );\n }\n}\n\n/** The `expect` function installed as a global. */\nfunction expect(received: unknown): Expectation {\n return new Expectation(received);\n}\n\n/* -------------------------------------------------------------------------- */\n/* describe / it / test registry */\n/* -------------------------------------------------------------------------- */\n\ninterface PendingTest {\n suitePath: string[];\n name: string;\n fn: () => void | Promise<void>;\n skip: boolean;\n}\n\nconst _pendingTests: PendingTest[] = [];\nconst _suiteStack: string[] = [];\n\n/** Registers a suite scope; calls `fn` synchronously to collect inner tests. */\nfunction describe(name: string, fn: () => void): void {\n _suiteStack.push(name);\n fn();\n _suiteStack.pop();\n}\n\n/** Registers a test. */\nfunction it(name: string, fn: () => void | Promise<void>): void {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip: false });\n}\n\n/** Alias for `it`. */\nconst test = it;\n\n/** Skipped test — registered but not executed. */\ndescribe.skip = (name: string, _fn: () => void): void => {\n void name;\n};\nit.skip = (name: string, _fn?: () => void | Promise<void>): void => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn: () => {}, skip: true });\n};\ntest.skip = it.skip;\n\n/**\n * `it.skipIf(cond)(name, fn)` / `it.runIf(cond)(name, fn)` — conditional test\n * registration. `skipIf` skips when `cond` is truthy; `runIf` runs only when\n * `cond` is truthy (skips otherwise). sdk-example uses\n * `it.skipIf(cell.platform === 'mock')(...)` to skip real-SDK-only cases in env1.\n */\ntype ItRegistrar = (name: string, fn: () => void | Promise<void>) => void;\nfunction _conditionalIt(skip: boolean): ItRegistrar {\n return (name, fn) => {\n _pendingTests.push({ suitePath: [..._suiteStack], name, fn, skip });\n };\n}\nit.skipIf = (cond: unknown): ItRegistrar => _conditionalIt(Boolean(cond));\nit.runIf = (cond: unknown): ItRegistrar => _conditionalIt(!cond);\ntest.skipIf = it.skipIf;\ntest.runIf = it.runIf;\n\n/**\n * `describe.skipIf(cond)(name, fn)` / `describe.runIf(cond)(name, fn)`.\n * When skipped, the suite body still runs to register its tests but every test\n * inside is marked skipped (collected via a temporary skip flag is overkill —\n * a skipped describe simply does not invoke its body, mirroring `describe.skip`).\n */\ntype DescribeRegistrar = (name: string, fn: () => void) => void;\ndescribe.skipIf = (cond: unknown): DescribeRegistrar =>\n cond ? (name, _fn) => void name : (name, fn) => describe(name, fn);\ndescribe.runIf = (cond: unknown): DescribeRegistrar =>\n cond ? (name, fn) => describe(name, fn) : (name, _fn) => void name;\n\n/* -------------------------------------------------------------------------- */\n/* Lifecycle hooks (devtools#683) */\n/* -------------------------------------------------------------------------- */\n\ntype HookType = 'beforeAll' | 'afterAll' | 'beforeEach' | 'afterEach';\n\ninterface HookEntry {\n /** Suite path at the time of registration ([] = module scope). */\n suitePath: string[];\n type: HookType;\n fn: () => void | Promise<void>;\n}\n\nconst _hooks: HookEntry[] = [];\n\nfunction _registerHook(type: HookType, fn: () => void | Promise<void>): void {\n _hooks.push({ suitePath: [..._suiteStack], type, fn });\n}\n\n/**\n * Returns hooks whose suitePath is a prefix of `testSuitePath`\n * (i.e. the hook's scope contains the test).\n */\nfunction _hooksFor(type: HookType, testSuitePath: string[]): Array<() => void | Promise<void>> {\n return _hooks\n .filter((h) => {\n if (h.type !== type) return false;\n // Hook scope must be a prefix of the test's suite path\n if (h.suitePath.length > testSuitePath.length) return false;\n for (let i = 0; i < h.suitePath.length; i++) {\n if (h.suitePath[i] !== testSuitePath[i]) return false;\n }\n return true;\n })\n .map((h) => h.fn);\n}\n\n/** Runs an array of hook functions in order, awaiting each. */\nasync function _runHooks(fns: Array<() => void | Promise<void>>): Promise<Error | null> {\n for (const fn of fns) {\n try {\n await fn();\n } catch (e) {\n return e instanceof Error ? e : new Error(String(e));\n }\n }\n return null;\n}\n\nfunction beforeAll(fn: () => void | Promise<void>): void {\n _registerHook('beforeAll', fn);\n}\nfunction afterAll(fn: () => void | Promise<void>): void {\n _registerHook('afterAll', fn);\n}\nfunction beforeEach(fn: () => void | Promise<void>): void {\n _registerHook('beforeEach', fn);\n}\nfunction afterEach(fn: () => void | Promise<void>): void {\n _registerHook('afterEach', fn);\n}\n\n/* -------------------------------------------------------------------------- */\n/* vi shim (devtools#683) */\n/* -------------------------------------------------------------------------- */\n\ninterface SpyCall {\n args: unknown[];\n returnValue: unknown;\n}\n\ninterface MockFn<T extends unknown[], R> {\n (...args: T): R;\n mock: { calls: SpyCall[] };\n mockImplementation(fn: (...args: T) => R): this;\n mockReturnValue(value: R): this;\n mockRestore(): void;\n}\n\ninterface SpyRecord {\n obj: Record<string, unknown>;\n method: string;\n original: unknown;\n}\n\nconst _spyRegistry: SpyRecord[] = [];\n\nfunction _createMockFn<T extends unknown[], R>(impl?: (...args: T) => R): MockFn<T, R> {\n let currentImpl: ((...args: T) => R) | undefined = impl;\n const calls: SpyCall[] = [];\n\n const mockFn = ((...args: T): R => {\n const returnValue = currentImpl ? currentImpl(...args) : (undefined as unknown as R);\n calls.push({ args, returnValue });\n return returnValue;\n }) as MockFn<T, R>;\n\n mockFn.mock = { calls };\n\n mockFn.mockImplementation = function (fn: (...args: T) => R): typeof mockFn {\n currentImpl = fn;\n return this;\n };\n\n mockFn.mockReturnValue = function (value: R): typeof mockFn {\n currentImpl = () => value;\n return this;\n };\n\n // No-op restore for standalone fn (only spyOn-created fns have a real restore)\n mockFn.mockRestore = (): void => {};\n\n return mockFn;\n}\n\nconst vi = {\n /** Creates a spy on `obj[method]`, replacing it with a mock function. */\n spyOn<T extends Record<string, unknown>, K extends keyof T>(\n obj: T,\n method: K,\n ): MockFn<unknown[], unknown> {\n const original = obj[method];\n const spy = _createMockFn<unknown[], unknown>(\n typeof original === 'function' ? (original as (...args: unknown[]) => unknown) : undefined,\n );\n\n spy.mockRestore = (): void => {\n obj[method] = original as T[K];\n };\n\n _spyRegistry.push({ obj: obj as Record<string, unknown>, method: method as string, original });\n obj[method] = spy as unknown as T[K];\n return spy;\n },\n\n /** Creates a standalone mock function, optionally wrapping `impl`. */\n fn<T extends unknown[], R>(impl?: (...args: T) => R): MockFn<T, R> {\n return _createMockFn(impl);\n },\n\n /** Restores all spies created via `vi.spyOn` to their original values. */\n restoreAllMocks(): void {\n for (const { obj, method, original } of _spyRegistry) {\n obj[method] = original;\n }\n _spyRegistry.length = 0;\n },\n};\n\n/* -------------------------------------------------------------------------- */\n/* Runtime entry point */\n/* -------------------------------------------------------------------------- */\n\n/**\n * Runtime globals object — exported for direct use in unit tests so that\n * test factories can reference runtime's own `it`/`expect`/`beforeAll`/etc.\n * without depending on `globalThis` injection.\n *\n * In a real WebView bundle these are accessed via globals installed by\n * `runTestModule`; in Node tests, import from here directly.\n */\nexport const runtimeGlobals = {\n describe,\n it,\n test,\n expect,\n beforeAll,\n afterAll,\n beforeEach,\n afterEach,\n vi,\n} as const;\n\n/**\n * Installs describe/it/test/expect/afterAll/afterEach/beforeAll/beforeEach/vi\n * as globals, invokes `moduleFactory` to register the user's tests, then\n * executes them and returns a RunReport.\n *\n * This function is exported as `__testBundle.runTestModule` by the IIFE wrapper\n * that bundle.ts generates. The Node-side rpc.ts calls it via `Runtime.evaluate`.\n *\n * @param moduleFactory - A zero-argument function that contains the user's\n * top-level test code (describe/it/test calls). The bundler wraps the entire\n * test module so that its top-level statements become the body of this factory.\n */\nexport async function runTestModule(\n moduleFactory?: () => void | Promise<void>,\n): Promise<RunReport> {\n // Reset state for re-entrant calls within the same page context.\n _pendingTests.length = 0;\n _suiteStack.length = 0;\n _hooks.length = 0;\n _spyRegistry.length = 0;\n\n // Install globals that user test code expects.\n type G = typeof globalThis & {\n describe: typeof describe;\n it: typeof it;\n test: typeof test;\n expect: typeof expect;\n beforeAll: typeof beforeAll;\n afterAll: typeof afterAll;\n beforeEach: typeof beforeEach;\n afterEach: typeof afterEach;\n vi: typeof vi;\n };\n const g = globalThis as G;\n g.describe = describe;\n g.it = it;\n g.test = test;\n g.expect = expect;\n g.beforeAll = beforeAll;\n g.afterAll = afterAll;\n g.beforeEach = beforeEach;\n g.afterEach = afterEach;\n g.vi = vi;\n\n // Run the factory (which registers describe/it/test blocks via globals).\n if (moduleFactory) {\n await moduleFactory();\n }\n\n const wallStart = Date.now();\n const startedAt = new Date(wallStart).toISOString();\n const results: TestResult[] = [];\n\n // Determine unique suite scopes from registered tests, in discovery order.\n // We need to fire beforeAll/afterAll once per scope (grouped by suitePath).\n //\n // Simplified model: we run beforeAll hooks before the first test in a scope\n // and afterAll hooks after the last test in a scope. Scope identity is the\n // entire suitePath string (e.g. \"Suite A > Suite B\").\n //\n // For module-scope hooks (suitePath=[]), they wrap the entire test run.\n\n // Group tests by their suite key to track beforeAll/afterAll per scope.\n // We keep a Set of scopes for which beforeAll has been fired.\n const firedBeforeAll = new Set<string>();\n // Map from scope key → last index in _pendingTests that belongs to that scope.\n const lastIndexForScope = new Map<string, number>();\n for (let i = 0; i < _pendingTests.length; i++) {\n const key = _pendingTests[i].suitePath.join('\\0');\n lastIndexForScope.set(key, i);\n // Also compute for parent scopes (module scope = '')\n const path = _pendingTests[i].suitePath;\n for (let depth = 0; depth < path.length; depth++) {\n const parentKey = path.slice(0, depth).join('\\0');\n const cur = lastIndexForScope.get(parentKey) ?? -1;\n if (i > cur) lastIndexForScope.set(parentKey, i);\n }\n }\n // Module scope key\n const MODULE_KEY = '';\n if (!lastIndexForScope.has(MODULE_KEY) && _pendingTests.length > 0) {\n lastIndexForScope.set(MODULE_KEY, _pendingTests.length - 1);\n }\n\n // Fire module-scope beforeAll before any tests\n if (_pendingTests.length > 0) {\n const baFns = _hooksFor('beforeAll', []);\n const baErr = await _runHooks(baFns);\n firedBeforeAll.add(MODULE_KEY);\n if (baErr) {\n // If module beforeAll fails, mark all tests as failed.\n for (const pending of _pendingTests) {\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n results.push({\n name: fullName,\n status: 'fail',\n duration: 0,\n error: `beforeAll failed: ${baErr.message}`,\n });\n }\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n // Still run module afterAll for cleanup (e.g. flushCapture)\n const aaFns = _hooksFor('afterAll', []);\n await _runHooks(aaFns);\n return { startedAt, duration, passed, failed, skipped, tests: results };\n }\n }\n\n for (let i = 0; i < _pendingTests.length; i++) {\n const pending = _pendingTests[i];\n const fullName = [...pending.suitePath, pending.name].join(' > ');\n\n if (pending.skip) {\n results.push({ name: fullName, status: 'skip', duration: 0 });\n continue;\n }\n\n // Fire suite-scoped beforeAll for any new scopes entered by this test\n for (let depth = 1; depth <= pending.suitePath.length; depth++) {\n const scopePath = pending.suitePath.slice(0, depth);\n const scopeKey = scopePath.join('\\0');\n if (!firedBeforeAll.has(scopeKey)) {\n // Only hooks registered exactly at this scope (not broader/narrower)\n const scopedFns = _hooks\n .filter((h) => h.type === 'beforeAll' && h.suitePath.join('\\0') === scopeKey)\n .map((h) => h.fn);\n const baErr = await _runHooks(scopedFns);\n firedBeforeAll.add(scopeKey);\n if (baErr) {\n // Mark remaining tests in this scope as failed\n results.push({\n name: fullName,\n status: 'fail',\n duration: 0,\n error: `beforeAll failed: ${baErr.message}`,\n });\n }\n }\n }\n\n // beforeEach\n const beFns = _hooksFor('beforeEach', pending.suitePath);\n const beErr = await _runHooks(beFns);\n\n const tStart = Date.now();\n let testErr: string | undefined;\n\n if (beErr) {\n testErr = `beforeEach failed: ${beErr.message}`;\n } else {\n try {\n await pending.fn();\n } catch (e) {\n testErr = e instanceof Error ? e.message : String(e);\n }\n }\n\n // afterEach — always run even if test failed\n const aeFns = _hooksFor('afterEach', pending.suitePath);\n const aeErr = await _runHooks(aeFns);\n if (aeErr && !testErr) testErr = `afterEach failed: ${aeErr.message}`;\n\n if (testErr !== undefined) {\n results.push({\n name: fullName,\n status: 'fail',\n duration: Date.now() - tStart,\n error: testErr,\n });\n } else {\n results.push({ name: fullName, status: 'pass', duration: Date.now() - tStart });\n }\n\n // Fire suite-scoped afterAll when this is the last test in each scope\n for (let depth = pending.suitePath.length; depth >= 1; depth--) {\n const scopePath = pending.suitePath.slice(0, depth);\n const scopeKey = scopePath.join('\\0');\n const lastIdx = lastIndexForScope.get(scopeKey) ?? -1;\n if (lastIdx === i) {\n const scopedFns = _hooks\n .filter((h) => h.type === 'afterAll' && h.suitePath.join('\\0') === scopeKey)\n .map((h) => h.fn);\n await _runHooks(scopedFns);\n }\n }\n }\n\n // Fire module-scope afterAll after all tests — critical for flushCapture\n const moduleAfterAllFns = _hooks\n .filter((h) => h.type === 'afterAll' && h.suitePath.length === 0)\n .map((h) => h.fn);\n await _runHooks(moduleAfterAllFns);\n\n const duration = Date.now() - wallStart;\n const passed = results.filter((r) => r.status === 'pass').length;\n const failed = results.filter((r) => r.status === 'fail').length;\n const skipped = results.filter((r) => r.status === 'skip').length;\n\n return { startedAt, duration, passed, failed, skipped, tests: results };\n}\n"],"mappings":"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"}
|
|
@@ -154,7 +154,7 @@ async function startTunnelDashboard(opts) {
|
|
|
154
154
|
if (opts.qr === false) return void 0;
|
|
155
155
|
const { isAutoDevtoolsDisabled } = await import("./devtools-opener-B8nxrxqu.js");
|
|
156
156
|
if (!(opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser()))()) return void 0;
|
|
157
|
-
const { startQrHttpServer } = await import("./qr-http-server-
|
|
157
|
+
const { startQrHttpServer } = await import("./qr-http-server-BsWlOA85.js");
|
|
158
158
|
const { buildLauncherAttachUrl } = await import("./deeplink-B5-Hxu0Q.js");
|
|
159
159
|
const { generateTotp } = await import("./totp-DIbrZtI7.js");
|
|
160
160
|
const getDashboardState = () => {
|
|
@@ -288,4 +288,4 @@ async function startQuickTunnel(port) {
|
|
|
288
288
|
//#endregion
|
|
289
289
|
export { printTunnelBanner, startQuickTunnel, startTunnelDashboard };
|
|
290
290
|
|
|
291
|
-
//# sourceMappingURL=tunnel-
|
|
291
|
+
//# sourceMappingURL=tunnel-B7U5k1xa.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel-Cpn3mA4u.js","names":[],"sources":["../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * The `wss://` relay URL of the env-2 CDP tunnel, if `tunnel.cdp` is on. When\n * present the QR deep-link additionally carries `&debug=1&relay=<wss>` so the\n * framed PWA passes the in-app debug gate and attaches a Chii target — the\n * same single scan opens screen preview *and* CDP debugging.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, the deep-link carries `&navBarType=game`\n * so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the default) is the launcher's implicit default — not added to\n * keep the URL clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, the deep-link carries `&navBarTransparent=1` so the launcher\n * partner bar renders with a transparent background (content shows through).\n * `false` / omitted → not added (URL clean, back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (`granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, the deep-link carries\n * `&navBarTheme=<v>` so the launcher partner bar uses the matching foreground\n * colour. Omitted / other values → not added (URL clean, back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Options for {@link buildLauncherDeepLink}.\n */\nexport interface BuildLauncherDeepLinkOptions {\n /**\n * `wss://` relay URL for env-2 CDP wiring. When present the deep-link carries\n * `&debug=1&relay=<wss>`.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param, #498).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, adds `&navBarType=game` to the\n * deep-link so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the launcher's implicit default) is not added to keep the URL\n * clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, adds `&navBarTransparent=1` to the deep-link so the launcher\n * partner bar renders with a transparent background. Omitted when `false` /\n * undefined to keep the URL clean (back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, adds `&navBarTheme=<v>` to\n * the deep-link so the launcher partner bar uses the matching foreground colour.\n * Omitted when undefined / other values to keep the URL clean (back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n *\n * When `opts.relayWssUrl` is given (env-2 CDP wiring), the deep-link also carries\n * `&debug=1&relay=<wss>`; the launcher folds those onto the framed tunnel URL so\n * the in-app debug gate's Layer C (`debug=1` opt-in + `relay=<wss>`) is met and\n * a Chii target.js is injected into the live view.\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the launcher\n * partner bar shows the app name instead of the generic default (#498).\n *\n * When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the\n * launcher enters game nav chrome (floating capsule, no full bar) automatically\n * on scan. `'partner'` is the launcher's implicit default and is not added to\n * keep the URL clean (#584).\n *\n * When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended\n * so the launcher partner bar renders with a transparent background (#587).\n *\n * When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is\n * appended so the launcher partner bar uses the matching foreground colour (#587).\n *\n * Back-compat: the second argument may also be a plain string (`relayWssUrl`)\n * for callers that haven't migrated to the options object yet.\n */\nexport function buildLauncherDeepLink(\n tunnelUrl: string,\n optsOrRelay?: string | BuildLauncherDeepLinkOptions,\n): string {\n // Normalise the overloaded second argument.\n const opts: BuildLauncherDeepLinkOptions =\n typeof optsOrRelay === 'string' ? { relayWssUrl: optsOrRelay } : (optsOrRelay ?? {});\n\n const base = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n let url = base;\n if (opts.relayWssUrl) {\n url += `&debug=1&relay=${encodeURIComponent(opts.relayWssUrl)}`;\n }\n if (opts.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts.webViewType === 'game') {\n url += '&navBarType=game';\n }\n if (opts.navBarTransparent === true) {\n url += '&navBarTransparent=1';\n }\n if (opts.navBarTheme === 'light' || opts.navBarTheme === 'dark') {\n url += `&navBarTheme=${opts.navBarTheme}`;\n }\n return url;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url, {\n relayWssUrl: opts.relayWssUrl,\n name: opts.name,\n webViewType: opts.webViewType,\n navBarTransparent: opts.navBarTransparent,\n navBarTheme: opts.navBarTheme,\n });\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ...(opts.relayWssUrl\n ? [\n ' │ The same scan also attaches CDP — connect your AI host',\n ' │ to the relay and debug the live view on-device.',\n ]\n : []),\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\n/**\n * Heuristic: can this process open a GUI browser? Mirrors `canOpenBrowser` in\n * `src/mcp/tools.ts` but is re-declared here (not imported) so the tunnel path\n * does not statically pull the heavy MCP `tools.ts` module graph into the lazy\n * `import('./tunnel.js')` chunk. Kept in sync with the MCP copy.\n *\n * - macOS / Windows → assume yes (env-2 dev normally runs on the user's Mac).\n * - Linux → require `DISPLAY` or `WAYLAND_DISPLAY`.\n * - CI (`CI=true`/`CI=1`) → no.\n */\nfunction canOpenBrowser(): boolean {\n if (process.env.CI === 'true' || process.env.CI === '1') return false;\n const platform = process.platform;\n if (platform === 'darwin' || platform === 'win32') return true;\n if (platform === 'linux') {\n return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);\n }\n return false;\n}\n\n/** Handle returned by {@link startTunnelDashboard}. */\nexport interface TunnelDashboard {\n /** `http://127.0.0.1:<port>` — the local dashboard URL opened in the browser. */\n url: string;\n /** Tear down the local HTTP server. Idempotent via the underlying server. */\n close: () => Promise<void>;\n}\n\nexport interface StartTunnelDashboardOptions {\n /** The public `https://*.trycloudflare.com` app tunnel URL the launcher frames. */\n tunnelUrl: string;\n /** The `wss://` relay URL of the env-2 CDP tunnel. REQUIRED — the dashboard is a CDP-only UX. */\n relayWssUrl: string;\n /** Mirror of `tunnel.qr` — when `false` the dashboard is skipped (no browser open). */\n qr?: boolean;\n /**\n * Override the GUI/opt-out gate (testing only). When omitted the real\n * `canOpenBrowser()` + `AIT_AUTO_DEVTOOLS` checks decide.\n */\n shouldOpen?: () => boolean;\n /** Sink for the one-line \"opened in browser\" note (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n}\n\n/**\n * Env-2 UX parity with env 3/4 (issue #408): when CDP wiring is on and a GUI is\n * available, start the SAME `127.0.0.1` HTML dashboard (QR image + connect steps\n * + FAQ) that the MCP `start_attach` path serves, and auto-open it in the\n * browser. headless / opt-out falls back to the terminal ASCII QR (printed\n * separately by {@link printTunnelBanner}).\n *\n * Every part the install-graph invariant depends on (`qrcode`, the MCP HTTP\n * server, the opener) is reached only through dynamic `import()` here, inside\n * the already-lazy `tunnel.js` chunk — nothing is added to the common build\n * graph or the MCP-only install graph.\n *\n * TOTP encapsulation: the dashboard's `getDashboardState` closure mints a FRESH\n * TOTP `at=` code on every call via `generateTotp(secret, Date.now())` and folds\n * it into a fresh `buildLauncherAttachUrl(...)`. Because the QR is re-rendered on\n * each SSE push / page reload from this closure, the code a phone scans is always\n * within its 30 s window — no stale code is baked into static HTML.\n *\n * SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`\n * value/path are NEVER written to stdout/stderr/logs here. They live only inside\n * the attach URL (HTML body + `/qr.png` query, per qr-http-server's invariant).\n * The only thing opened/logged is `http://127.0.0.1:<port>` (local, safe).\n *\n * @returns the dashboard handle when it started (caller wires `close()` into the\n * tunnel cleanup), or `undefined` when skipped (no relay, `qr:false`, headless,\n * opt-out, or a start failure) — in which case ASCII QR fallback stands alone.\n */\nexport async function startTunnelDashboard(\n opts: StartTunnelDashboardOptions,\n): Promise<TunnelDashboard | undefined> {\n const log = opts.log ?? ((m: string) => console.log(m));\n\n // Gate: dashboard is a CDP-only UX (needs a relay to attach to).\n if (!opts.relayWssUrl) return undefined;\n // Opt-out via `tunnel.qr:false` (same toggle that suppresses the ASCII QR).\n if (opts.qr === false) return undefined;\n\n // GUI + AIT_AUTO_DEVTOOLS gate. Reuse the MCP opener's opt-out predicate so\n // the env-2 path honours the same `AIT_AUTO_DEVTOOLS=0` switch as env 3/4.\n const { isAutoDevtoolsDisabled } = await import('../mcp/devtools-opener.js');\n const gateOpen = opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser());\n if (!gateOpen()) return undefined;\n\n const { startQrHttpServer } = await import('../mcp/qr-http-server.js');\n const { buildLauncherAttachUrl } = await import('../mcp/deeplink.js');\n const { generateTotp } = await import('../mcp/totp.js');\n\n // getDashboardState — mints a fresh TOTP + attach URL on every call so the QR\n // the dashboard renders (on load and on each SSE push) is never expired.\n // SECRET-HANDLING: the secret is read from env AT CALL TIME (it was injected\n // by ensureRelaySecret in the same CDP block) and is used only to compute the\n // at= code folded into attachUrl. tunnel.up is always true here — the relay\n // tunnel is already up by the time this runs.\n const getDashboardState = () => {\n const secret = process.env.AIT_DEBUG_TOTP_SECRET;\n const totpCode = secret ? generateTotp(secret, Date.now()) : undefined;\n const attachUrl = buildLauncherAttachUrl(opts.tunnelUrl, opts.relayWssUrl, totpCode, {\n name: opts.name,\n });\n // pages: null — env 2(unplugin)는 데몬이 아니라 vite 플러그인 안이라\n // startChiiRelay 핸들이 connected target을 노출하지 않는다. 라이브 page 목록을\n // 알 수 없으므로 거짓 빈 목록 대신 \"연결된 Pages\" 섹션 자체를 숨긴다(#411).\n // env 3/4(debug-server.ts)는 router.active.listTargets()로 실제 목록을 채운다.\n // mode: 'relay-mobile' — 이 대시보드는 항상 환경 2(AITC Sandbox PWA) 전용이므로\n // /attach 카피가 launcher PWA 절차(sandbox family)로 분기된다(#468).\n // inspectorUrl: null — env 2에서는 unplugin relay가 connected target ID를 노출하지\n // 않아 buildChiiInspectorUrl에 필요한 targetId를 알 수 없다. target attach 후\n // target ID가 필요하므로 env 3/4에서만 non-null이 된다(#503).\n return {\n tunnel: { up: true, wssUrl: opts.relayWssUrl },\n pages: null,\n attachUrl,\n inspectorUrl: null,\n mode: 'relay-mobile' as const,\n };\n };\n\n let server: Awaited<ReturnType<typeof startQrHttpServer>>;\n try {\n server = await startQrHttpServer(getDashboardState);\n } catch {\n // SECRET-HANDLING: do not surface the error (could embed paths/hosts). The\n // ASCII QR printed by printTunnelBanner stays as the fallback.\n return undefined;\n }\n\n // TOTP periodic refresh timer — pushes a fresh at= code to SSE clients every\n // 20 s so a page left open never stales past the 90 s acceptance window (#448).\n // tunnel.ts always has relayWssUrl available here (gated above), so no\n // lastAttachParts guard is needed — getDashboardState mints a fresh TOTP on\n // every call unconditionally.\n // SECRET-HANDLING: callback is a plain trigger only — TOTP value and at= code\n // must never be logged or written to stdout.\n const TOTP_REFRESH_INTERVAL_MS = 20_000;\n let totpRefreshHandle: ReturnType<typeof setInterval> | null = setInterval(() => {\n server.notifyStateChange();\n }, TOTP_REFRESH_INTERVAL_MS);\n totpRefreshHandle.unref();\n\n const dashboardUrl = `http://127.0.0.1:${server.port}`;\n\n const { openUrlInBrowser } = await import('../mcp/devtools-opener.js');\n const opened = openUrlInBrowser(dashboardUrl);\n // SECRET-HANDLING: only the local 127.0.0.1 URL is logged — never the tunnel\n // host, relay wssUrl, or TOTP code.\n log(\n opened\n ? ` │ Opened a QR dashboard in your browser: ${dashboardUrl}`\n : ` │ Open this QR dashboard in your browser: ${dashboardUrl}`,\n );\n\n return {\n url: dashboardUrl,\n close: () => {\n if (totpRefreshHandle) {\n clearInterval(totpRefreshHandle);\n totpRefreshHandle = null;\n }\n return server.close();\n },\n };\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\n/**\n * Sanitize cloudflared stderr output for error diagnostics (#421).\n *\n * Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs\n * that carry those hostnames so tunnel host values never appear in error\n * messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.\n *\n * SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only\n * placeholder text is emitted.\n */\nexport function sanitizeCloudflaredOutput(line: string): string {\n // Full URL forms: https://xxx.trycloudflare.com/… and wss://xxx.trycloudflare.com/…\n let s = line.replace(/(?:https?|wss?):\\/\\/[a-z0-9-]+\\.trycloudflare\\.com(?:\\/[^\\s]*)*/gi, (m) =>\n m.replace(/[a-z0-9-]+\\.trycloudflare\\.com/i, '<HOST>.trycloudflare.com'),\n );\n // Bare hostname without scheme (e.g. printed in cloudflared JSON logs)\n s = s.replace(/[a-z0-9-]+\\.trycloudflare\\.com/gi, '<HOST>.trycloudflare.com');\n return s;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n // #421: accumulate stderr to attach as diagnostics on failure.\n // SECRET-HANDLING: lines are sanitized before inclusion in error messages.\n const stderrLines: string[] = [];\n\n /**\n * Format the last `n` sanitized stderr lines as a diagnostic appendix.\n * Returns an empty string when no lines have been collected.\n */\n const stderrTail = (n = 15): string => {\n if (stderrLines.length === 0) return '';\n const tail = stderrLines.slice(-n).map(sanitizeCloudflaredOutput).join('');\n return `\\ncloudflared 출력 (마지막 ${Math.min(n, stderrLines.length)}줄):\\n${tail}`;\n };\n\n const timer = setTimeout(() => {\n cleanup();\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.${stderrTail()}`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n cleanup();\n resolve({ url: found, stop });\n };\n\n // Accumulate stderr lines for diagnostics (#421). Named so it can be\n // removed from the listener list when cleanup() runs.\n const pushStderr = (line: string) => {\n stderrLines.push(line);\n };\n\n const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n tunnel.off('stderr', pushStderr);\n };\n\n // The library emits a parsed `url` event; we also scan raw stdout/stderr in\n // case the output format shifts.\n tunnel.once('url', onUrl);\n tunnel.on('stdout', onUrl);\n tunnel.on('stderr', onUrl);\n // Second stderr listener: accumulate all lines for error diagnostics.\n tunnel.on('stderr', pushStderr);\n tunnel.once('error', (err: Error) => {\n clearTimeout(timer);\n cleanup();\n stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\n cleanup();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared exited (code ${code ?? 'null'}) before reporting a tunnel URL.${stderrTail()}`,\n ),\n );\n });\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tunnel-B7U5k1xa.js","names":[],"sources":["../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * The `wss://` relay URL of the env-2 CDP tunnel, if `tunnel.cdp` is on. When\n * present the QR deep-link additionally carries `&debug=1&relay=<wss>` so the\n * framed PWA passes the in-app debug gate and attaches a Chii target — the\n * same single scan opens screen preview *and* CDP debugging.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, the deep-link carries `&navBarType=game`\n * so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the default) is the launcher's implicit default — not added to\n * keep the URL clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, the deep-link carries `&navBarTransparent=1` so the launcher\n * partner bar renders with a transparent background (content shows through).\n * `false` / omitted → not added (URL clean, back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (`granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, the deep-link carries\n * `&navBarTheme=<v>` so the launcher partner bar uses the matching foreground\n * colour. Omitted / other values → not added (URL clean, back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Options for {@link buildLauncherDeepLink}.\n */\nexport interface BuildLauncherDeepLinkOptions {\n /**\n * `wss://` relay URL for env-2 CDP wiring. When present the deep-link carries\n * `&debug=1&relay=<wss>`.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param, #498).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, adds `&navBarType=game` to the\n * deep-link so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the launcher's implicit default) is not added to keep the URL\n * clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, adds `&navBarTransparent=1` to the deep-link so the launcher\n * partner bar renders with a transparent background. Omitted when `false` /\n * undefined to keep the URL clean (back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, adds `&navBarTheme=<v>` to\n * the deep-link so the launcher partner bar uses the matching foreground colour.\n * Omitted when undefined / other values to keep the URL clean (back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n *\n * When `opts.relayWssUrl` is given (env-2 CDP wiring), the deep-link also carries\n * `&debug=1&relay=<wss>`; the launcher folds those onto the framed tunnel URL so\n * the in-app debug gate's Layer C (`debug=1` opt-in + `relay=<wss>`) is met and\n * a Chii target.js is injected into the live view.\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the launcher\n * partner bar shows the app name instead of the generic default (#498).\n *\n * When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the\n * launcher enters game nav chrome (floating capsule, no full bar) automatically\n * on scan. `'partner'` is the launcher's implicit default and is not added to\n * keep the URL clean (#584).\n *\n * When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended\n * so the launcher partner bar renders with a transparent background (#587).\n *\n * When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is\n * appended so the launcher partner bar uses the matching foreground colour (#587).\n *\n * Back-compat: the second argument may also be a plain string (`relayWssUrl`)\n * for callers that haven't migrated to the options object yet.\n */\nexport function buildLauncherDeepLink(\n tunnelUrl: string,\n optsOrRelay?: string | BuildLauncherDeepLinkOptions,\n): string {\n // Normalise the overloaded second argument.\n const opts: BuildLauncherDeepLinkOptions =\n typeof optsOrRelay === 'string' ? { relayWssUrl: optsOrRelay } : (optsOrRelay ?? {});\n\n const base = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n let url = base;\n if (opts.relayWssUrl) {\n url += `&debug=1&relay=${encodeURIComponent(opts.relayWssUrl)}`;\n }\n if (opts.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts.webViewType === 'game') {\n url += '&navBarType=game';\n }\n if (opts.navBarTransparent === true) {\n url += '&navBarTransparent=1';\n }\n if (opts.navBarTheme === 'light' || opts.navBarTheme === 'dark') {\n url += `&navBarTheme=${opts.navBarTheme}`;\n }\n return url;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url, {\n relayWssUrl: opts.relayWssUrl,\n name: opts.name,\n webViewType: opts.webViewType,\n navBarTransparent: opts.navBarTransparent,\n navBarTheme: opts.navBarTheme,\n });\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ...(opts.relayWssUrl\n ? [\n ' │ The same scan also attaches CDP — connect your AI host',\n ' │ to the relay and debug the live view on-device.',\n ]\n : []),\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\n/**\n * Heuristic: can this process open a GUI browser? Mirrors `canOpenBrowser` in\n * `src/mcp/tools.ts` but is re-declared here (not imported) so the tunnel path\n * does not statically pull the heavy MCP `tools.ts` module graph into the lazy\n * `import('./tunnel.js')` chunk. Kept in sync with the MCP copy.\n *\n * - macOS / Windows → assume yes (env-2 dev normally runs on the user's Mac).\n * - Linux → require `DISPLAY` or `WAYLAND_DISPLAY`.\n * - CI (`CI=true`/`CI=1`) → no.\n */\nfunction canOpenBrowser(): boolean {\n if (process.env.CI === 'true' || process.env.CI === '1') return false;\n const platform = process.platform;\n if (platform === 'darwin' || platform === 'win32') return true;\n if (platform === 'linux') {\n return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);\n }\n return false;\n}\n\n/** Handle returned by {@link startTunnelDashboard}. */\nexport interface TunnelDashboard {\n /** `http://127.0.0.1:<port>` — the local dashboard URL opened in the browser. */\n url: string;\n /** Tear down the local HTTP server. Idempotent via the underlying server. */\n close: () => Promise<void>;\n}\n\nexport interface StartTunnelDashboardOptions {\n /** The public `https://*.trycloudflare.com` app tunnel URL the launcher frames. */\n tunnelUrl: string;\n /** The `wss://` relay URL of the env-2 CDP tunnel. REQUIRED — the dashboard is a CDP-only UX. */\n relayWssUrl: string;\n /** Mirror of `tunnel.qr` — when `false` the dashboard is skipped (no browser open). */\n qr?: boolean;\n /**\n * Override the GUI/opt-out gate (testing only). When omitted the real\n * `canOpenBrowser()` + `AIT_AUTO_DEVTOOLS` checks decide.\n */\n shouldOpen?: () => boolean;\n /** Sink for the one-line \"opened in browser\" note (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n}\n\n/**\n * Env-2 UX parity with env 3/4 (issue #408): when CDP wiring is on and a GUI is\n * available, start the SAME `127.0.0.1` HTML dashboard (QR image + connect steps\n * + FAQ) that the MCP `start_attach` path serves, and auto-open it in the\n * browser. headless / opt-out falls back to the terminal ASCII QR (printed\n * separately by {@link printTunnelBanner}).\n *\n * Every part the install-graph invariant depends on (`qrcode`, the MCP HTTP\n * server, the opener) is reached only through dynamic `import()` here, inside\n * the already-lazy `tunnel.js` chunk — nothing is added to the common build\n * graph or the MCP-only install graph.\n *\n * TOTP encapsulation: the dashboard's `getDashboardState` closure mints a FRESH\n * TOTP `at=` code on every call via `generateTotp(secret, Date.now())` and folds\n * it into a fresh `buildLauncherAttachUrl(...)`. Because the QR is re-rendered on\n * each SSE push / page reload from this closure, the code a phone scans is always\n * within its 30 s window — no stale code is baked into static HTML.\n *\n * SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`\n * value/path are NEVER written to stdout/stderr/logs here. They live only inside\n * the attach URL (HTML body + `/qr.png` query, per qr-http-server's invariant).\n * The only thing opened/logged is `http://127.0.0.1:<port>` (local, safe).\n *\n * @returns the dashboard handle when it started (caller wires `close()` into the\n * tunnel cleanup), or `undefined` when skipped (no relay, `qr:false`, headless,\n * opt-out, or a start failure) — in which case ASCII QR fallback stands alone.\n */\nexport async function startTunnelDashboard(\n opts: StartTunnelDashboardOptions,\n): Promise<TunnelDashboard | undefined> {\n const log = opts.log ?? ((m: string) => console.log(m));\n\n // Gate: dashboard is a CDP-only UX (needs a relay to attach to).\n if (!opts.relayWssUrl) return undefined;\n // Opt-out via `tunnel.qr:false` (same toggle that suppresses the ASCII QR).\n if (opts.qr === false) return undefined;\n\n // GUI + AIT_AUTO_DEVTOOLS gate. Reuse the MCP opener's opt-out predicate so\n // the env-2 path honours the same `AIT_AUTO_DEVTOOLS=0` switch as env 3/4.\n const { isAutoDevtoolsDisabled } = await import('../mcp/devtools-opener.js');\n const gateOpen = opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser());\n if (!gateOpen()) return undefined;\n\n const { startQrHttpServer } = await import('../mcp/qr-http-server.js');\n const { buildLauncherAttachUrl } = await import('../mcp/deeplink.js');\n const { generateTotp } = await import('../mcp/totp.js');\n\n // getDashboardState — mints a fresh TOTP + attach URL on every call so the QR\n // the dashboard renders (on load and on each SSE push) is never expired.\n // SECRET-HANDLING: the secret is read from env AT CALL TIME (it was injected\n // by ensureRelaySecret in the same CDP block) and is used only to compute the\n // at= code folded into attachUrl. tunnel.up is always true here — the relay\n // tunnel is already up by the time this runs.\n const getDashboardState = () => {\n const secret = process.env.AIT_DEBUG_TOTP_SECRET;\n const totpCode = secret ? generateTotp(secret, Date.now()) : undefined;\n const attachUrl = buildLauncherAttachUrl(opts.tunnelUrl, opts.relayWssUrl, totpCode, {\n name: opts.name,\n });\n // pages: null — env 2(unplugin)는 데몬이 아니라 vite 플러그인 안이라\n // startChiiRelay 핸들이 connected target을 노출하지 않는다. 라이브 page 목록을\n // 알 수 없으므로 거짓 빈 목록 대신 \"연결된 Pages\" 섹션 자체를 숨긴다(#411).\n // env 3/4(debug-server.ts)는 router.active.listTargets()로 실제 목록을 채운다.\n // mode: 'relay-mobile' — 이 대시보드는 항상 환경 2(AITC Sandbox PWA) 전용이므로\n // /attach 카피가 launcher PWA 절차(sandbox family)로 분기된다(#468).\n // inspectorUrl: null — env 2에서는 unplugin relay가 connected target ID를 노출하지\n // 않아 buildChiiInspectorUrl에 필요한 targetId를 알 수 없다. target attach 후\n // target ID가 필요하므로 env 3/4에서만 non-null이 된다(#503).\n return {\n tunnel: { up: true, wssUrl: opts.relayWssUrl },\n pages: null,\n attachUrl,\n inspectorUrl: null,\n mode: 'relay-mobile' as const,\n };\n };\n\n let server: Awaited<ReturnType<typeof startQrHttpServer>>;\n try {\n server = await startQrHttpServer(getDashboardState);\n } catch {\n // SECRET-HANDLING: do not surface the error (could embed paths/hosts). The\n // ASCII QR printed by printTunnelBanner stays as the fallback.\n return undefined;\n }\n\n // TOTP periodic refresh timer — pushes a fresh at= code to SSE clients every\n // 20 s so a page left open never stales past the 90 s acceptance window (#448).\n // tunnel.ts always has relayWssUrl available here (gated above), so no\n // lastAttachParts guard is needed — getDashboardState mints a fresh TOTP on\n // every call unconditionally.\n // SECRET-HANDLING: callback is a plain trigger only — TOTP value and at= code\n // must never be logged or written to stdout.\n const TOTP_REFRESH_INTERVAL_MS = 20_000;\n let totpRefreshHandle: ReturnType<typeof setInterval> | null = setInterval(() => {\n server.notifyStateChange();\n }, TOTP_REFRESH_INTERVAL_MS);\n totpRefreshHandle.unref();\n\n const dashboardUrl = `http://127.0.0.1:${server.port}`;\n\n const { openUrlInBrowser } = await import('../mcp/devtools-opener.js');\n const opened = openUrlInBrowser(dashboardUrl);\n // SECRET-HANDLING: only the local 127.0.0.1 URL is logged — never the tunnel\n // host, relay wssUrl, or TOTP code.\n log(\n opened\n ? ` │ Opened a QR dashboard in your browser: ${dashboardUrl}`\n : ` │ Open this QR dashboard in your browser: ${dashboardUrl}`,\n );\n\n return {\n url: dashboardUrl,\n close: () => {\n if (totpRefreshHandle) {\n clearInterval(totpRefreshHandle);\n totpRefreshHandle = null;\n }\n return server.close();\n },\n };\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\n/**\n * Sanitize cloudflared stderr output for error diagnostics (#421).\n *\n * Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs\n * that carry those hostnames so tunnel host values never appear in error\n * messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.\n *\n * SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only\n * placeholder text is emitted.\n */\nexport function sanitizeCloudflaredOutput(line: string): string {\n // Full URL forms: https://xxx.trycloudflare.com/… and wss://xxx.trycloudflare.com/…\n let s = line.replace(/(?:https?|wss?):\\/\\/[a-z0-9-]+\\.trycloudflare\\.com(?:\\/[^\\s]*)*/gi, (m) =>\n m.replace(/[a-z0-9-]+\\.trycloudflare\\.com/i, '<HOST>.trycloudflare.com'),\n );\n // Bare hostname without scheme (e.g. printed in cloudflared JSON logs)\n s = s.replace(/[a-z0-9-]+\\.trycloudflare\\.com/gi, '<HOST>.trycloudflare.com');\n return s;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n // #421: accumulate stderr to attach as diagnostics on failure.\n // SECRET-HANDLING: lines are sanitized before inclusion in error messages.\n const stderrLines: string[] = [];\n\n /**\n * Format the last `n` sanitized stderr lines as a diagnostic appendix.\n * Returns an empty string when no lines have been collected.\n */\n const stderrTail = (n = 15): string => {\n if (stderrLines.length === 0) return '';\n const tail = stderrLines.slice(-n).map(sanitizeCloudflaredOutput).join('');\n return `\\ncloudflared 출력 (마지막 ${Math.min(n, stderrLines.length)}줄):\\n${tail}`;\n };\n\n const timer = setTimeout(() => {\n cleanup();\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.${stderrTail()}`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n cleanup();\n resolve({ url: found, stop });\n };\n\n // Accumulate stderr lines for diagnostics (#421). Named so it can be\n // removed from the listener list when cleanup() runs.\n const pushStderr = (line: string) => {\n stderrLines.push(line);\n };\n\n const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n tunnel.off('stderr', pushStderr);\n };\n\n // The library emits a parsed `url` event; we also scan raw stdout/stderr in\n // case the output format shifts.\n tunnel.once('url', onUrl);\n tunnel.on('stdout', onUrl);\n tunnel.on('stderr', onUrl);\n // Second stderr listener: accumulate all lines for error diagnostics.\n tunnel.on('stderr', pushStderr);\n tunnel.once('error', (err: Error) => {\n clearTimeout(timer);\n cleanup();\n stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\n cleanup();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared exited (code ${code ?? 'null'}) before reporting a tunnel URL.${stderrTail()}`,\n ),\n );\n });\n });\n}\n"],"mappings":"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"}
|
|
@@ -154,7 +154,7 @@ async function startTunnelDashboard(opts) {
|
|
|
154
154
|
if (opts.qr === false) return void 0;
|
|
155
155
|
const { isAutoDevtoolsDisabled } = await Promise.resolve().then(() => require("./devtools-opener-iv1OwfJN.cjs"));
|
|
156
156
|
if (!(opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser()))()) return void 0;
|
|
157
|
-
const { startQrHttpServer } = await Promise.resolve().then(() => require("./qr-http-server-
|
|
157
|
+
const { startQrHttpServer } = await Promise.resolve().then(() => require("./qr-http-server-BpTvfeku.cjs"));
|
|
158
158
|
const { buildLauncherAttachUrl } = await Promise.resolve().then(() => require("./deeplink-BzdbA1gV.cjs"));
|
|
159
159
|
const { generateTotp } = await Promise.resolve().then(() => require("./totp-Df252ZdA.cjs"));
|
|
160
160
|
const getDashboardState = () => {
|
|
@@ -290,4 +290,4 @@ exports.printTunnelBanner = printTunnelBanner;
|
|
|
290
290
|
exports.startQuickTunnel = startQuickTunnel;
|
|
291
291
|
exports.startTunnelDashboard = startTunnelDashboard;
|
|
292
292
|
|
|
293
|
-
//# sourceMappingURL=tunnel-
|
|
293
|
+
//# sourceMappingURL=tunnel-C-cgMnyY.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tunnel-Dj8Kf2QS.cjs","names":[],"sources":["../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * The `wss://` relay URL of the env-2 CDP tunnel, if `tunnel.cdp` is on. When\n * present the QR deep-link additionally carries `&debug=1&relay=<wss>` so the\n * framed PWA passes the in-app debug gate and attaches a Chii target — the\n * same single scan opens screen preview *and* CDP debugging.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, the deep-link carries `&navBarType=game`\n * so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the default) is the launcher's implicit default — not added to\n * keep the URL clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, the deep-link carries `&navBarTransparent=1` so the launcher\n * partner bar renders with a transparent background (content shows through).\n * `false` / omitted → not added (URL clean, back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (`granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, the deep-link carries\n * `&navBarTheme=<v>` so the launcher partner bar uses the matching foreground\n * colour. Omitted / other values → not added (URL clean, back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Options for {@link buildLauncherDeepLink}.\n */\nexport interface BuildLauncherDeepLinkOptions {\n /**\n * `wss://` relay URL for env-2 CDP wiring. When present the deep-link carries\n * `&debug=1&relay=<wss>`.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param, #498).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, adds `&navBarType=game` to the\n * deep-link so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the launcher's implicit default) is not added to keep the URL\n * clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, adds `&navBarTransparent=1` to the deep-link so the launcher\n * partner bar renders with a transparent background. Omitted when `false` /\n * undefined to keep the URL clean (back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, adds `&navBarTheme=<v>` to\n * the deep-link so the launcher partner bar uses the matching foreground colour.\n * Omitted when undefined / other values to keep the URL clean (back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n *\n * When `opts.relayWssUrl` is given (env-2 CDP wiring), the deep-link also carries\n * `&debug=1&relay=<wss>`; the launcher folds those onto the framed tunnel URL so\n * the in-app debug gate's Layer C (`debug=1` opt-in + `relay=<wss>`) is met and\n * a Chii target.js is injected into the live view.\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the launcher\n * partner bar shows the app name instead of the generic default (#498).\n *\n * When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the\n * launcher enters game nav chrome (floating capsule, no full bar) automatically\n * on scan. `'partner'` is the launcher's implicit default and is not added to\n * keep the URL clean (#584).\n *\n * When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended\n * so the launcher partner bar renders with a transparent background (#587).\n *\n * When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is\n * appended so the launcher partner bar uses the matching foreground colour (#587).\n *\n * Back-compat: the second argument may also be a plain string (`relayWssUrl`)\n * for callers that haven't migrated to the options object yet.\n */\nexport function buildLauncherDeepLink(\n tunnelUrl: string,\n optsOrRelay?: string | BuildLauncherDeepLinkOptions,\n): string {\n // Normalise the overloaded second argument.\n const opts: BuildLauncherDeepLinkOptions =\n typeof optsOrRelay === 'string' ? { relayWssUrl: optsOrRelay } : (optsOrRelay ?? {});\n\n const base = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n let url = base;\n if (opts.relayWssUrl) {\n url += `&debug=1&relay=${encodeURIComponent(opts.relayWssUrl)}`;\n }\n if (opts.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts.webViewType === 'game') {\n url += '&navBarType=game';\n }\n if (opts.navBarTransparent === true) {\n url += '&navBarTransparent=1';\n }\n if (opts.navBarTheme === 'light' || opts.navBarTheme === 'dark') {\n url += `&navBarTheme=${opts.navBarTheme}`;\n }\n return url;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url, {\n relayWssUrl: opts.relayWssUrl,\n name: opts.name,\n webViewType: opts.webViewType,\n navBarTransparent: opts.navBarTransparent,\n navBarTheme: opts.navBarTheme,\n });\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ...(opts.relayWssUrl\n ? [\n ' │ The same scan also attaches CDP — connect your AI host',\n ' │ to the relay and debug the live view on-device.',\n ]\n : []),\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\n/**\n * Heuristic: can this process open a GUI browser? Mirrors `canOpenBrowser` in\n * `src/mcp/tools.ts` but is re-declared here (not imported) so the tunnel path\n * does not statically pull the heavy MCP `tools.ts` module graph into the lazy\n * `import('./tunnel.js')` chunk. Kept in sync with the MCP copy.\n *\n * - macOS / Windows → assume yes (env-2 dev normally runs on the user's Mac).\n * - Linux → require `DISPLAY` or `WAYLAND_DISPLAY`.\n * - CI (`CI=true`/`CI=1`) → no.\n */\nfunction canOpenBrowser(): boolean {\n if (process.env.CI === 'true' || process.env.CI === '1') return false;\n const platform = process.platform;\n if (platform === 'darwin' || platform === 'win32') return true;\n if (platform === 'linux') {\n return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);\n }\n return false;\n}\n\n/** Handle returned by {@link startTunnelDashboard}. */\nexport interface TunnelDashboard {\n /** `http://127.0.0.1:<port>` — the local dashboard URL opened in the browser. */\n url: string;\n /** Tear down the local HTTP server. Idempotent via the underlying server. */\n close: () => Promise<void>;\n}\n\nexport interface StartTunnelDashboardOptions {\n /** The public `https://*.trycloudflare.com` app tunnel URL the launcher frames. */\n tunnelUrl: string;\n /** The `wss://` relay URL of the env-2 CDP tunnel. REQUIRED — the dashboard is a CDP-only UX. */\n relayWssUrl: string;\n /** Mirror of `tunnel.qr` — when `false` the dashboard is skipped (no browser open). */\n qr?: boolean;\n /**\n * Override the GUI/opt-out gate (testing only). When omitted the real\n * `canOpenBrowser()` + `AIT_AUTO_DEVTOOLS` checks decide.\n */\n shouldOpen?: () => boolean;\n /** Sink for the one-line \"opened in browser\" note (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n}\n\n/**\n * Env-2 UX parity with env 3/4 (issue #408): when CDP wiring is on and a GUI is\n * available, start the SAME `127.0.0.1` HTML dashboard (QR image + connect steps\n * + FAQ) that the MCP `start_attach` path serves, and auto-open it in the\n * browser. headless / opt-out falls back to the terminal ASCII QR (printed\n * separately by {@link printTunnelBanner}).\n *\n * Every part the install-graph invariant depends on (`qrcode`, the MCP HTTP\n * server, the opener) is reached only through dynamic `import()` here, inside\n * the already-lazy `tunnel.js` chunk — nothing is added to the common build\n * graph or the MCP-only install graph.\n *\n * TOTP encapsulation: the dashboard's `getDashboardState` closure mints a FRESH\n * TOTP `at=` code on every call via `generateTotp(secret, Date.now())` and folds\n * it into a fresh `buildLauncherAttachUrl(...)`. Because the QR is re-rendered on\n * each SSE push / page reload from this closure, the code a phone scans is always\n * within its 30 s window — no stale code is baked into static HTML.\n *\n * SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`\n * value/path are NEVER written to stdout/stderr/logs here. They live only inside\n * the attach URL (HTML body + `/qr.png` query, per qr-http-server's invariant).\n * The only thing opened/logged is `http://127.0.0.1:<port>` (local, safe).\n *\n * @returns the dashboard handle when it started (caller wires `close()` into the\n * tunnel cleanup), or `undefined` when skipped (no relay, `qr:false`, headless,\n * opt-out, or a start failure) — in which case ASCII QR fallback stands alone.\n */\nexport async function startTunnelDashboard(\n opts: StartTunnelDashboardOptions,\n): Promise<TunnelDashboard | undefined> {\n const log = opts.log ?? ((m: string) => console.log(m));\n\n // Gate: dashboard is a CDP-only UX (needs a relay to attach to).\n if (!opts.relayWssUrl) return undefined;\n // Opt-out via `tunnel.qr:false` (same toggle that suppresses the ASCII QR).\n if (opts.qr === false) return undefined;\n\n // GUI + AIT_AUTO_DEVTOOLS gate. Reuse the MCP opener's opt-out predicate so\n // the env-2 path honours the same `AIT_AUTO_DEVTOOLS=0` switch as env 3/4.\n const { isAutoDevtoolsDisabled } = await import('../mcp/devtools-opener.js');\n const gateOpen = opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser());\n if (!gateOpen()) return undefined;\n\n const { startQrHttpServer } = await import('../mcp/qr-http-server.js');\n const { buildLauncherAttachUrl } = await import('../mcp/deeplink.js');\n const { generateTotp } = await import('../mcp/totp.js');\n\n // getDashboardState — mints a fresh TOTP + attach URL on every call so the QR\n // the dashboard renders (on load and on each SSE push) is never expired.\n // SECRET-HANDLING: the secret is read from env AT CALL TIME (it was injected\n // by ensureRelaySecret in the same CDP block) and is used only to compute the\n // at= code folded into attachUrl. tunnel.up is always true here — the relay\n // tunnel is already up by the time this runs.\n const getDashboardState = () => {\n const secret = process.env.AIT_DEBUG_TOTP_SECRET;\n const totpCode = secret ? generateTotp(secret, Date.now()) : undefined;\n const attachUrl = buildLauncherAttachUrl(opts.tunnelUrl, opts.relayWssUrl, totpCode, {\n name: opts.name,\n });\n // pages: null — env 2(unplugin)는 데몬이 아니라 vite 플러그인 안이라\n // startChiiRelay 핸들이 connected target을 노출하지 않는다. 라이브 page 목록을\n // 알 수 없으므로 거짓 빈 목록 대신 \"연결된 Pages\" 섹션 자체를 숨긴다(#411).\n // env 3/4(debug-server.ts)는 router.active.listTargets()로 실제 목록을 채운다.\n // mode: 'relay-mobile' — 이 대시보드는 항상 환경 2(AITC Sandbox PWA) 전용이므로\n // /attach 카피가 launcher PWA 절차(sandbox family)로 분기된다(#468).\n // inspectorUrl: null — env 2에서는 unplugin relay가 connected target ID를 노출하지\n // 않아 buildChiiInspectorUrl에 필요한 targetId를 알 수 없다. target attach 후\n // target ID가 필요하므로 env 3/4에서만 non-null이 된다(#503).\n return {\n tunnel: { up: true, wssUrl: opts.relayWssUrl },\n pages: null,\n attachUrl,\n inspectorUrl: null,\n mode: 'relay-mobile' as const,\n };\n };\n\n let server: Awaited<ReturnType<typeof startQrHttpServer>>;\n try {\n server = await startQrHttpServer(getDashboardState);\n } catch {\n // SECRET-HANDLING: do not surface the error (could embed paths/hosts). The\n // ASCII QR printed by printTunnelBanner stays as the fallback.\n return undefined;\n }\n\n // TOTP periodic refresh timer — pushes a fresh at= code to SSE clients every\n // 20 s so a page left open never stales past the 90 s acceptance window (#448).\n // tunnel.ts always has relayWssUrl available here (gated above), so no\n // lastAttachParts guard is needed — getDashboardState mints a fresh TOTP on\n // every call unconditionally.\n // SECRET-HANDLING: callback is a plain trigger only — TOTP value and at= code\n // must never be logged or written to stdout.\n const TOTP_REFRESH_INTERVAL_MS = 20_000;\n let totpRefreshHandle: ReturnType<typeof setInterval> | null = setInterval(() => {\n server.notifyStateChange();\n }, TOTP_REFRESH_INTERVAL_MS);\n totpRefreshHandle.unref();\n\n const dashboardUrl = `http://127.0.0.1:${server.port}`;\n\n const { openUrlInBrowser } = await import('../mcp/devtools-opener.js');\n const opened = openUrlInBrowser(dashboardUrl);\n // SECRET-HANDLING: only the local 127.0.0.1 URL is logged — never the tunnel\n // host, relay wssUrl, or TOTP code.\n log(\n opened\n ? ` │ Opened a QR dashboard in your browser: ${dashboardUrl}`\n : ` │ Open this QR dashboard in your browser: ${dashboardUrl}`,\n );\n\n return {\n url: dashboardUrl,\n close: () => {\n if (totpRefreshHandle) {\n clearInterval(totpRefreshHandle);\n totpRefreshHandle = null;\n }\n return server.close();\n },\n };\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\n/**\n * Sanitize cloudflared stderr output for error diagnostics (#421).\n *\n * Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs\n * that carry those hostnames so tunnel host values never appear in error\n * messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.\n *\n * SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only\n * placeholder text is emitted.\n */\nexport function sanitizeCloudflaredOutput(line: string): string {\n // Full URL forms: https://xxx.trycloudflare.com/… and wss://xxx.trycloudflare.com/…\n let s = line.replace(/(?:https?|wss?):\\/\\/[a-z0-9-]+\\.trycloudflare\\.com(?:\\/[^\\s]*)*/gi, (m) =>\n m.replace(/[a-z0-9-]+\\.trycloudflare\\.com/i, '<HOST>.trycloudflare.com'),\n );\n // Bare hostname without scheme (e.g. printed in cloudflared JSON logs)\n s = s.replace(/[a-z0-9-]+\\.trycloudflare\\.com/gi, '<HOST>.trycloudflare.com');\n return s;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n // #421: accumulate stderr to attach as diagnostics on failure.\n // SECRET-HANDLING: lines are sanitized before inclusion in error messages.\n const stderrLines: string[] = [];\n\n /**\n * Format the last `n` sanitized stderr lines as a diagnostic appendix.\n * Returns an empty string when no lines have been collected.\n */\n const stderrTail = (n = 15): string => {\n if (stderrLines.length === 0) return '';\n const tail = stderrLines.slice(-n).map(sanitizeCloudflaredOutput).join('');\n return `\\ncloudflared 출력 (마지막 ${Math.min(n, stderrLines.length)}줄):\\n${tail}`;\n };\n\n const timer = setTimeout(() => {\n cleanup();\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.${stderrTail()}`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n cleanup();\n resolve({ url: found, stop });\n };\n\n // Accumulate stderr lines for diagnostics (#421). Named so it can be\n // removed from the listener list when cleanup() runs.\n const pushStderr = (line: string) => {\n stderrLines.push(line);\n };\n\n const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n tunnel.off('stderr', pushStderr);\n };\n\n // The library emits a parsed `url` event; we also scan raw stdout/stderr in\n // case the output format shifts.\n tunnel.once('url', onUrl);\n tunnel.on('stdout', onUrl);\n tunnel.on('stderr', onUrl);\n // Second stderr listener: accumulate all lines for error diagnostics.\n tunnel.on('stderr', pushStderr);\n tunnel.once('error', (err: Error) => {\n clearTimeout(timer);\n cleanup();\n stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\n cleanup();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared exited (code ${code ?? 'null'}) before reporting a tunnel URL.${stderrTail()}`,\n ),\n );\n });\n });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"tunnel-C-cgMnyY.cjs","names":[],"sources":["../src/unplugin/tunnel.ts"],"sourcesContent":["/**\n * Cloudflare quick-tunnel helper for the devtools unplugin.\n *\n * Loaded lazily (`await import('./tunnel.js')`) only when the `tunnel` option is\n * on, so `cloudflared` / `qrcode-terminal` are never pulled in for the common\n * case. This is the one place in `@ait-co/devtools` that depends on Node-only\n * APIs (`child_process` via the `cloudflared` wrapper) — keep it thin and out of\n * jsdom unit tests; the spawn path is verified by hand / e2e (same spirit as the\n * \"web 모드는 e2e\" rule in CLAUDE.md). The pure helpers below\n * (`parseTrycloudflareUrl`, `printTunnelBanner`) are unit-tested.\n */\n\nimport { existsSync } from 'node:fs';\nimport { mkdir } from 'node:fs/promises';\nimport { dirname } from 'node:path';\n\n/** Matches the public URL cloudflared prints for an unauthenticated quick tunnel. */\nconst TRYCLOUDFLARE_RE = /https:\\/\\/[a-z0-9-]+\\.trycloudflare\\.com/i;\n\n/**\n * Extract the `https://<sub>.trycloudflare.com` URL from a line of cloudflared\n * output, or `null` if the line doesn't contain one. Pulled out as a pure\n * function so it can be unit-tested without spawning anything.\n */\nexport function parseTrycloudflareUrl(line: string): string | null {\n const m = line.match(TRYCLOUDFLARE_RE);\n return m ? m[0] : null;\n}\n\nexport interface PrintTunnelBannerOptions {\n /** Print an ASCII QR encoding the tunnel URL (default: true). */\n qr?: boolean;\n /** Sink for the banner text (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * The `wss://` relay URL of the env-2 CDP tunnel, if `tunnel.cdp` is on. When\n * present the QR deep-link additionally carries `&debug=1&relay=<wss>` so the\n * framed PWA passes the in-app debug gate and attaches a Chii target — the\n * same single scan opens screen preview *and* CDP debugging.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, the deep-link carries `&navBarType=game`\n * so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the default) is the launcher's implicit default — not added to\n * keep the URL clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, the deep-link carries `&navBarTransparent=1` so the launcher\n * partner bar renders with a transparent background (content shows through).\n * `false` / omitted → not added (URL clean, back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (`granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, the deep-link carries\n * `&navBarTheme=<v>` so the launcher partner bar uses the matching foreground\n * colour. Omitted / other values → not added (URL clean, back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\nconst LAUNCHER_URL = 'https://devtools.aitc.dev/launcher/';\n\n/**\n * Options for {@link buildLauncherDeepLink}.\n */\nexport interface BuildLauncherDeepLinkOptions {\n /**\n * `wss://` relay URL for env-2 CDP wiring. When present the deep-link carries\n * `&debug=1&relay=<wss>`.\n */\n relayWssUrl?: string;\n /**\n * Human-readable app name shown in the partner nav bar (`name=` param, #498).\n * Blank / whitespace-only values are not added.\n */\n name?: string;\n /**\n * The miniapp's webViewType. When `'game'`, adds `&navBarType=game` to the\n * deep-link so the launcher enters game nav chrome automatically on scan (#584).\n * `'partner'` (the launcher's implicit default) is not added to keep the URL\n * clean.\n */\n webViewType?: 'partner' | 'game';\n /**\n * Whether the miniapp's navigationBar has `transparentBackground: true`\n * (granite.config `navigationBar.transparentBackground`, SDK 2.8.0, #587).\n * When `true`, adds `&navBarTransparent=1` to the deep-link so the launcher\n * partner bar renders with a transparent background. Omitted when `false` /\n * undefined to keep the URL clean (back-compat).\n */\n navBarTransparent?: boolean;\n /**\n * The miniapp's navigationBar theme (granite.config `navigationBar.theme`,\n * SDK 2.8.0, #587). When `'light'` or `'dark'`, adds `&navBarTheme=<v>` to\n * the deep-link so the launcher partner bar uses the matching foreground colour.\n * Omitted when undefined / other values to keep the URL clean (back-compat).\n */\n navBarTheme?: 'light' | 'dark';\n}\n\n/**\n * Build the deep-link URL that QR codes encode: when the launcher PWA is\n * already on the phone's home screen, scanning this opens it directly into the\n * live view for `tunnelUrl` (the launcher consumes `?url=` and clears it).\n * Plain-text raw URL is no longer enough — the launcher gates its setup UI to\n * the installed PWA, so a raw tunnel URL opened in a normal browser tab would\n * land on a \"please install\" screen.\n *\n * When `opts.relayWssUrl` is given (env-2 CDP wiring), the deep-link also carries\n * `&debug=1&relay=<wss>`; the launcher folds those onto the framed tunnel URL so\n * the in-app debug gate's Layer C (`debug=1` opt-in + `relay=<wss>`) is met and\n * a Chii target.js is injected into the live view.\n *\n * When `opts.name` is given (non-blank), it is added as `&name=` so the launcher\n * partner bar shows the app name instead of the generic default (#498).\n *\n * When `opts.webViewType` is `'game'`, `&navBarType=game` is appended so the\n * launcher enters game nav chrome (floating capsule, no full bar) automatically\n * on scan. `'partner'` is the launcher's implicit default and is not added to\n * keep the URL clean (#584).\n *\n * When `opts.navBarTransparent` is `true`, `&navBarTransparent=1` is appended\n * so the launcher partner bar renders with a transparent background (#587).\n *\n * When `opts.navBarTheme` is `'light'` or `'dark'`, `&navBarTheme=<v>` is\n * appended so the launcher partner bar uses the matching foreground colour (#587).\n *\n * Back-compat: the second argument may also be a plain string (`relayWssUrl`)\n * for callers that haven't migrated to the options object yet.\n */\nexport function buildLauncherDeepLink(\n tunnelUrl: string,\n optsOrRelay?: string | BuildLauncherDeepLinkOptions,\n): string {\n // Normalise the overloaded second argument.\n const opts: BuildLauncherDeepLinkOptions =\n typeof optsOrRelay === 'string' ? { relayWssUrl: optsOrRelay } : (optsOrRelay ?? {});\n\n const base = `${LAUNCHER_URL}?url=${encodeURIComponent(tunnelUrl)}`;\n let url = base;\n if (opts.relayWssUrl) {\n url += `&debug=1&relay=${encodeURIComponent(opts.relayWssUrl)}`;\n }\n if (opts.name !== undefined && opts.name.trim() !== '') {\n url += `&name=${encodeURIComponent(opts.name.trim())}`;\n }\n if (opts.webViewType === 'game') {\n url += '&navBarType=game';\n }\n if (opts.navBarTransparent === true) {\n url += '&navBarTransparent=1';\n }\n if (opts.navBarTheme === 'light' || opts.navBarTheme === 'dark') {\n url += `&navBarTheme=${opts.navBarTheme}`;\n }\n return url;\n}\n\n/**\n * Print the terminal banner announcing the live tunnel: the public URL, an ASCII\n * QR encoding a launcher deep-link, and a one-line note that quick tunnels are\n * ephemeral, unauthenticated and not for production. Pure w.r.t. side effects\n * other than the injected `log` sink and `qrcode-terminal` — unit-tested.\n */\nexport async function printTunnelBanner(\n url: string,\n opts: PrintTunnelBannerOptions = {},\n): Promise<void> {\n const log = opts.log ?? ((m: string) => console.log(m));\n const deepLink = buildLauncherDeepLink(url, {\n relayWssUrl: opts.relayWssUrl,\n name: opts.name,\n webViewType: opts.webViewType,\n navBarTransparent: opts.navBarTransparent,\n navBarTheme: opts.navBarTheme,\n });\n const lines: string[] = [\n '',\n ' ┌─ @ait-co/devtools · live tunnel ────────────────────────────',\n ` │ ${url}`,\n ' │',\n ` │ Install the launcher PWA once: ${LAUNCHER_URL}`,\n ' │ Then scan the QR below — it opens the launcher directly',\n ' │ into this tunnel URL (no manual paste needed).',\n ...(opts.relayWssUrl\n ? [\n ' │ The same scan also attaches CDP — connect your AI host',\n ' │ to the relay and debug the live view on-device.',\n ]\n : []),\n ' │ Quick tunnels are unauthenticated, change every run, and are',\n ' │ not for production use.',\n ' └──────────────────────────────────────────────────────────────',\n '',\n ];\n log(lines.join('\\n'));\n\n if (opts.qr !== false) {\n // qrcode-terminal is only pulled in on this code path (ambient types live\n // in src/qrcode-terminal.d.ts).\n const qrcode = (await import('qrcode-terminal')).default;\n await new Promise<void>((resolve) => {\n qrcode.generate(deepLink, { small: true }, (out) => {\n log(out);\n resolve();\n });\n });\n }\n}\n\n/**\n * Heuristic: can this process open a GUI browser? Mirrors `canOpenBrowser` in\n * `src/mcp/tools.ts` but is re-declared here (not imported) so the tunnel path\n * does not statically pull the heavy MCP `tools.ts` module graph into the lazy\n * `import('./tunnel.js')` chunk. Kept in sync with the MCP copy.\n *\n * - macOS / Windows → assume yes (env-2 dev normally runs on the user's Mac).\n * - Linux → require `DISPLAY` or `WAYLAND_DISPLAY`.\n * - CI (`CI=true`/`CI=1`) → no.\n */\nfunction canOpenBrowser(): boolean {\n if (process.env.CI === 'true' || process.env.CI === '1') return false;\n const platform = process.platform;\n if (platform === 'darwin' || platform === 'win32') return true;\n if (platform === 'linux') {\n return Boolean(process.env.DISPLAY ?? process.env.WAYLAND_DISPLAY);\n }\n return false;\n}\n\n/** Handle returned by {@link startTunnelDashboard}. */\nexport interface TunnelDashboard {\n /** `http://127.0.0.1:<port>` — the local dashboard URL opened in the browser. */\n url: string;\n /** Tear down the local HTTP server. Idempotent via the underlying server. */\n close: () => Promise<void>;\n}\n\nexport interface StartTunnelDashboardOptions {\n /** The public `https://*.trycloudflare.com` app tunnel URL the launcher frames. */\n tunnelUrl: string;\n /** The `wss://` relay URL of the env-2 CDP tunnel. REQUIRED — the dashboard is a CDP-only UX. */\n relayWssUrl: string;\n /** Mirror of `tunnel.qr` — when `false` the dashboard is skipped (no browser open). */\n qr?: boolean;\n /**\n * Override the GUI/opt-out gate (testing only). When omitted the real\n * `canOpenBrowser()` + `AIT_AUTO_DEVTOOLS` checks decide.\n */\n shouldOpen?: () => boolean;\n /** Sink for the one-line \"opened in browser\" note (default: `console.log`). Injected for testing. */\n log?: (msg: string) => void;\n /**\n * Human-readable app name to embed as `name=` in the launcher deep-link (#498).\n * When provided (non-blank), the launcher partner bar shows this name instead of\n * the generic default.\n */\n name?: string;\n}\n\n/**\n * Env-2 UX parity with env 3/4 (issue #408): when CDP wiring is on and a GUI is\n * available, start the SAME `127.0.0.1` HTML dashboard (QR image + connect steps\n * + FAQ) that the MCP `start_attach` path serves, and auto-open it in the\n * browser. headless / opt-out falls back to the terminal ASCII QR (printed\n * separately by {@link printTunnelBanner}).\n *\n * Every part the install-graph invariant depends on (`qrcode`, the MCP HTTP\n * server, the opener) is reached only through dynamic `import()` here, inside\n * the already-lazy `tunnel.js` chunk — nothing is added to the common build\n * graph or the MCP-only install graph.\n *\n * TOTP encapsulation: the dashboard's `getDashboardState` closure mints a FRESH\n * TOTP `at=` code on every call via `generateTotp(secret, Date.now())` and folds\n * it into a fresh `buildLauncherAttachUrl(...)`. Because the QR is re-rendered on\n * each SSE push / page reload from this closure, the code a phone scans is always\n * within its 30 s window — no stale code is baked into static HTML.\n *\n * SECRET-HANDLING: the tunnel host, relay wssUrl, TOTP code, and `.ait_relay`\n * value/path are NEVER written to stdout/stderr/logs here. They live only inside\n * the attach URL (HTML body + `/qr.png` query, per qr-http-server's invariant).\n * The only thing opened/logged is `http://127.0.0.1:<port>` (local, safe).\n *\n * @returns the dashboard handle when it started (caller wires `close()` into the\n * tunnel cleanup), or `undefined` when skipped (no relay, `qr:false`, headless,\n * opt-out, or a start failure) — in which case ASCII QR fallback stands alone.\n */\nexport async function startTunnelDashboard(\n opts: StartTunnelDashboardOptions,\n): Promise<TunnelDashboard | undefined> {\n const log = opts.log ?? ((m: string) => console.log(m));\n\n // Gate: dashboard is a CDP-only UX (needs a relay to attach to).\n if (!opts.relayWssUrl) return undefined;\n // Opt-out via `tunnel.qr:false` (same toggle that suppresses the ASCII QR).\n if (opts.qr === false) return undefined;\n\n // GUI + AIT_AUTO_DEVTOOLS gate. Reuse the MCP opener's opt-out predicate so\n // the env-2 path honours the same `AIT_AUTO_DEVTOOLS=0` switch as env 3/4.\n const { isAutoDevtoolsDisabled } = await import('../mcp/devtools-opener.js');\n const gateOpen = opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser());\n if (!gateOpen()) return undefined;\n\n const { startQrHttpServer } = await import('../mcp/qr-http-server.js');\n const { buildLauncherAttachUrl } = await import('../mcp/deeplink.js');\n const { generateTotp } = await import('../mcp/totp.js');\n\n // getDashboardState — mints a fresh TOTP + attach URL on every call so the QR\n // the dashboard renders (on load and on each SSE push) is never expired.\n // SECRET-HANDLING: the secret is read from env AT CALL TIME (it was injected\n // by ensureRelaySecret in the same CDP block) and is used only to compute the\n // at= code folded into attachUrl. tunnel.up is always true here — the relay\n // tunnel is already up by the time this runs.\n const getDashboardState = () => {\n const secret = process.env.AIT_DEBUG_TOTP_SECRET;\n const totpCode = secret ? generateTotp(secret, Date.now()) : undefined;\n const attachUrl = buildLauncherAttachUrl(opts.tunnelUrl, opts.relayWssUrl, totpCode, {\n name: opts.name,\n });\n // pages: null — env 2(unplugin)는 데몬이 아니라 vite 플러그인 안이라\n // startChiiRelay 핸들이 connected target을 노출하지 않는다. 라이브 page 목록을\n // 알 수 없으므로 거짓 빈 목록 대신 \"연결된 Pages\" 섹션 자체를 숨긴다(#411).\n // env 3/4(debug-server.ts)는 router.active.listTargets()로 실제 목록을 채운다.\n // mode: 'relay-mobile' — 이 대시보드는 항상 환경 2(AITC Sandbox PWA) 전용이므로\n // /attach 카피가 launcher PWA 절차(sandbox family)로 분기된다(#468).\n // inspectorUrl: null — env 2에서는 unplugin relay가 connected target ID를 노출하지\n // 않아 buildChiiInspectorUrl에 필요한 targetId를 알 수 없다. target attach 후\n // target ID가 필요하므로 env 3/4에서만 non-null이 된다(#503).\n return {\n tunnel: { up: true, wssUrl: opts.relayWssUrl },\n pages: null,\n attachUrl,\n inspectorUrl: null,\n mode: 'relay-mobile' as const,\n };\n };\n\n let server: Awaited<ReturnType<typeof startQrHttpServer>>;\n try {\n server = await startQrHttpServer(getDashboardState);\n } catch {\n // SECRET-HANDLING: do not surface the error (could embed paths/hosts). The\n // ASCII QR printed by printTunnelBanner stays as the fallback.\n return undefined;\n }\n\n // TOTP periodic refresh timer — pushes a fresh at= code to SSE clients every\n // 20 s so a page left open never stales past the 90 s acceptance window (#448).\n // tunnel.ts always has relayWssUrl available here (gated above), so no\n // lastAttachParts guard is needed — getDashboardState mints a fresh TOTP on\n // every call unconditionally.\n // SECRET-HANDLING: callback is a plain trigger only — TOTP value and at= code\n // must never be logged or written to stdout.\n const TOTP_REFRESH_INTERVAL_MS = 20_000;\n let totpRefreshHandle: ReturnType<typeof setInterval> | null = setInterval(() => {\n server.notifyStateChange();\n }, TOTP_REFRESH_INTERVAL_MS);\n totpRefreshHandle.unref();\n\n const dashboardUrl = `http://127.0.0.1:${server.port}`;\n\n const { openUrlInBrowser } = await import('../mcp/devtools-opener.js');\n const opened = openUrlInBrowser(dashboardUrl);\n // SECRET-HANDLING: only the local 127.0.0.1 URL is logged — never the tunnel\n // host, relay wssUrl, or TOTP code.\n log(\n opened\n ? ` │ Opened a QR dashboard in your browser: ${dashboardUrl}`\n : ` │ Open this QR dashboard in your browser: ${dashboardUrl}`,\n );\n\n return {\n url: dashboardUrl,\n close: () => {\n if (totpRefreshHandle) {\n clearInterval(totpRefreshHandle);\n totpRefreshHandle = null;\n }\n return server.close();\n },\n };\n}\n\nexport interface QuickTunnel {\n /** The public `https://*.trycloudflare.com` URL. */\n url: string;\n /** Stop the underlying `cloudflared` process. Idempotent. */\n stop: () => void;\n}\n\n/**\n * Sanitize cloudflared stderr output for error diagnostics (#421).\n *\n * Masks `*.trycloudflare.com` hostnames and full `https://` / `wss://` URLs\n * that carry those hostnames so tunnel host values never appear in error\n * messages. Diagnostic content (error codes, reasons, JSON blobs) is preserved.\n *\n * SECRET-HANDLING: tunnel host is SECRET-class per harness policy — only\n * placeholder text is emitted.\n */\nexport function sanitizeCloudflaredOutput(line: string): string {\n // Full URL forms: https://xxx.trycloudflare.com/… and wss://xxx.trycloudflare.com/…\n let s = line.replace(/(?:https?|wss?):\\/\\/[a-z0-9-]+\\.trycloudflare\\.com(?:\\/[^\\s]*)*/gi, (m) =>\n m.replace(/[a-z0-9-]+\\.trycloudflare\\.com/i, '<HOST>.trycloudflare.com'),\n );\n // Bare hostname without scheme (e.g. printed in cloudflared JSON logs)\n s = s.replace(/[a-z0-9-]+\\.trycloudflare\\.com/gi, '<HOST>.trycloudflare.com');\n return s;\n}\n\nconst URL_TIMEOUT_MS = 20_000;\n\n/**\n * Start an unauthenticated Cloudflare quick tunnel to `http://localhost:<port>`\n * and resolve once the public URL is known. Downloads the `cloudflared` binary\n * on first use if it is not already installed. Rejects with a friendly error if\n * no URL appears within {@link URL_TIMEOUT_MS}.\n */\nexport async function startQuickTunnel(port: number): Promise<QuickTunnel> {\n const cloudflared = await import('cloudflared');\n const { bin, install, Tunnel } = cloudflared;\n\n if (!existsSync(bin)) {\n await mkdir(dirname(bin), { recursive: true });\n await install(bin);\n }\n\n const tunnel = Tunnel.quick(`http://localhost:${port}`);\n let stopped = false;\n const stop = () => {\n if (stopped) return;\n stopped = true;\n try {\n tunnel.stop();\n } catch {\n // process may already be gone\n }\n };\n\n return new Promise<QuickTunnel>((resolve, reject) => {\n // #421: accumulate stderr to attach as diagnostics on failure.\n // SECRET-HANDLING: lines are sanitized before inclusion in error messages.\n const stderrLines: string[] = [];\n\n /**\n * Format the last `n` sanitized stderr lines as a diagnostic appendix.\n * Returns an empty string when no lines have been collected.\n */\n const stderrTail = (n = 15): string => {\n if (stderrLines.length === 0) return '';\n const tail = stderrLines.slice(-n).map(sanitizeCloudflaredOutput).join('');\n return `\\ncloudflared 출력 (마지막 ${Math.min(n, stderrLines.length)}줄):\\n${tail}`;\n };\n\n const timer = setTimeout(() => {\n cleanup();\n stop();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared did not report a tunnel URL within ${\n URL_TIMEOUT_MS / 1000\n }s. Check your network connection, or run \\`cloudflared tunnel --url http://localhost:${port}\\` manually.${stderrTail()}`,\n ),\n );\n }, URL_TIMEOUT_MS);\n\n const onUrl = (line: string) => {\n const found = parseTrycloudflareUrl(line);\n if (!found) return;\n clearTimeout(timer);\n // Stop scanning further output once we have the URL.\n cleanup();\n resolve({ url: found, stop });\n };\n\n // Accumulate stderr lines for diagnostics (#421). Named so it can be\n // removed from the listener list when cleanup() runs.\n const pushStderr = (line: string) => {\n stderrLines.push(line);\n };\n\n const cleanup = () => {\n tunnel.off('stdout', onUrl);\n tunnel.off('stderr', onUrl);\n tunnel.off('stderr', pushStderr);\n };\n\n // The library emits a parsed `url` event; we also scan raw stdout/stderr in\n // case the output format shifts.\n tunnel.once('url', onUrl);\n tunnel.on('stdout', onUrl);\n tunnel.on('stderr', onUrl);\n // Second stderr listener: accumulate all lines for error diagnostics.\n tunnel.on('stderr', pushStderr);\n tunnel.once('error', (err: Error) => {\n clearTimeout(timer);\n cleanup();\n stop();\n reject(err);\n });\n tunnel.once('exit', (code: number | null) => {\n if (stopped) return;\n clearTimeout(timer);\n cleanup();\n reject(\n new Error(\n `[@ait-co/devtools] cloudflared exited (code ${code ?? 'null'}) before reporting a tunnel URL.${stderrTail()}`,\n ),\n );\n });\n });\n}\n"],"mappings":"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"}
|
package/dist/unplugin/index.cjs
CHANGED
|
@@ -248,7 +248,7 @@ const aitDevtoolsPlugin = (0, unplugin.createUnplugin)((options) => {
|
|
|
248
248
|
console.warn("[@ait-co/devtools] tunnel: could not determine the dev server port; skipping.");
|
|
249
249
|
return;
|
|
250
250
|
}
|
|
251
|
-
Promise.resolve().then(() => require("../tunnel-
|
|
251
|
+
Promise.resolve().then(() => require("../tunnel-C-cgMnyY.cjs")).then(async ({ startQuickTunnel, printTunnelBanner, startTunnelDashboard }) => {
|
|
252
252
|
const t = await startQuickTunnel(port);
|
|
253
253
|
tunnel = t;
|
|
254
254
|
let relayWssUrl;
|
package/dist/unplugin/index.js
CHANGED
|
@@ -244,7 +244,7 @@ const aitDevtoolsPlugin = createUnplugin((options) => {
|
|
|
244
244
|
console.warn("[@ait-co/devtools] tunnel: could not determine the dev server port; skipping.");
|
|
245
245
|
return;
|
|
246
246
|
}
|
|
247
|
-
import("../tunnel-
|
|
247
|
+
import("../tunnel-B7U5k1xa.js").then(async ({ startQuickTunnel, printTunnelBanner, startTunnelDashboard }) => {
|
|
248
248
|
const t = await startQuickTunnel(port);
|
|
249
249
|
tunnel = t;
|
|
250
250
|
let relayWssUrl;
|
package/dist/unplugin/tunnel.cjs
CHANGED
|
@@ -155,7 +155,7 @@ async function startTunnelDashboard(opts) {
|
|
|
155
155
|
if (opts.qr === false) return void 0;
|
|
156
156
|
const { isAutoDevtoolsDisabled } = await Promise.resolve().then(() => require("../devtools-opener-BDY0w3_0.cjs"));
|
|
157
157
|
if (!(opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser()))()) return void 0;
|
|
158
|
-
const { startQrHttpServer } = await Promise.resolve().then(() => require("../qr-http-server-
|
|
158
|
+
const { startQrHttpServer } = await Promise.resolve().then(() => require("../qr-http-server-CrVKno9V.cjs"));
|
|
159
159
|
const { buildLauncherAttachUrl } = await Promise.resolve().then(() => require("../deeplink-DCScMYcp.cjs"));
|
|
160
160
|
const { generateTotp } = await Promise.resolve().then(() => require("../totp-BjtoQNfu.cjs"));
|
|
161
161
|
const getDashboardState = () => {
|
package/dist/unplugin/tunnel.js
CHANGED
|
@@ -154,7 +154,7 @@ async function startTunnelDashboard(opts) {
|
|
|
154
154
|
if (opts.qr === false) return void 0;
|
|
155
155
|
const { isAutoDevtoolsDisabled } = await import("../devtools-opener-BTl5A6Cd.js");
|
|
156
156
|
if (!(opts.shouldOpen ?? (() => !isAutoDevtoolsDisabled() && canOpenBrowser()))()) return void 0;
|
|
157
|
-
const { startQrHttpServer } = await import("../qr-http-server-
|
|
157
|
+
const { startQrHttpServer } = await import("../qr-http-server-UBV2qUEd.js");
|
|
158
158
|
const { buildLauncherAttachUrl } = await import("../deeplink-BpO9qc-D.js");
|
|
159
159
|
const { generateTotp } = await import("../totp-95OAa20j.js");
|
|
160
160
|
const getDashboardState = () => {
|
package/package.json
CHANGED