jh-web-gateway 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/infra/chrome-cdp.ts","../src/infra/config.ts","../src/core/auth-capture.ts","../src/infra/gateway-auth.ts","../src/infra/types.ts","../src/core/client.ts","../src/server.ts","../src/routes/models.ts","../src/routes/health.ts","../src/routes/chat-completions.ts","../src/core/message-builder.ts","../src/core/stream-translator.ts","../src/core/tool-parser.ts","../src/infra/logger.ts","../src/core/request-queue.ts","../src/core/page-pool.ts","../src/core/token-refresher.ts","../src/infra/chrome-manager.ts"],"sourcesContent":["import { chromium } from \"playwright-core\";\nimport type { Browser, Page } from \"playwright-core\";\n\nconst DEFAULT_TIMEOUT_MS = 5000;\nconst JH_URL = \"https://chat.ai.jh.edu\";\n\n/**\n * Fetch /json/version from Chrome's CDP HTTP endpoint and extract webSocketDebuggerUrl.\n * Throws a descriptive error if Chrome is unreachable or the response is malformed.\n */\nexport async function getChromeWebSocketUrl(\n cdpUrl: string,\n timeoutMs: number = DEFAULT_TIMEOUT_MS\n): Promise<string> {\n const controller = new AbortController();\n const timer = setTimeout(() => controller.abort(), timeoutMs);\n\n let raw: string;\n try {\n const res = await fetch(`${cdpUrl}/json/version`, {\n signal: controller.signal,\n });\n raw = await res.text();\n } catch {\n throw new Error(\n `Chrome is not running or remote debugging is not enabled at ${cdpUrl}`\n );\n } finally {\n clearTimeout(timer);\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(`Unexpected CDP response format: ${raw}`);\n }\n\n const wsUrl =\n parsed !== null &&\n typeof parsed === \"object\" &&\n \"webSocketDebuggerUrl\" in parsed\n ? (parsed as Record<string, unknown>).webSocketDebuggerUrl\n : undefined;\n\n if (typeof wsUrl !== \"string\" || wsUrl.length === 0) {\n throw new Error(`Unexpected CDP response format: ${raw}`);\n }\n\n return wsUrl;\n}\n\n/**\n * Connect to Chrome via Playwright CDP.\n * Returns the Browser and the first existing page (or a new one).\n */\nexport async function connectToChrome(\n cdpUrl: string\n): Promise<{ browser: Browser; page: Page }> {\n const wsUrl = await getChromeWebSocketUrl(cdpUrl);\n const browser = await chromium.connectOverCDP(wsUrl);\n\n const contexts = browser.contexts();\n const existingPage =\n contexts.length > 0 ? contexts[0].pages()[0] : undefined;\n\n const page =\n existingPage ??\n (await (contexts.length > 0\n ? contexts[0].newPage()\n : (await browser.newContext()).newPage()));\n\n return { browser, page };\n}\n\n/**\n * Check whether an existing browser page is still on chat.ai.jh.edu.\n * This is a **read-only** check — it does NOT navigate or open new tabs.\n * Returns true if at least one page URL contains \"chat.ai.jh.edu\",\n * false if no such page exists or all JH pages have redirected away.\n */\nexport async function checkBrowserLoginState(browser: Browser): Promise<boolean> {\n for (const context of browser.contexts()) {\n for (const page of context.pages()) {\n if (page.url().includes(\"chat.ai.jh.edu\")) {\n return true;\n }\n }\n }\n return false;\n}\n\n/**\n * Find an existing chat.ai.jh.edu page across all contexts, or open a new one.\n */\nexport async function findOrOpenJhPage(browser: Browser): Promise<Page> {\n for (const context of browser.contexts()) {\n for (const page of context.pages()) {\n if (page.url().includes(\"chat.ai.jh.edu\")) {\n return page;\n }\n }\n }\n\n // No existing JH page found — open one\n const contexts = browser.contexts();\n const context =\n contexts.length > 0 ? contexts[0] : await browser.newContext();\n const page = await context.newPage();\n await page.goto(JH_URL, { waitUntil: \"domcontentloaded\", timeout: 30_000 });\n return page;\n}\n","import { readFile, writeFile, mkdir, chmod } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { GatewayConfig } from \"./types.js\";\n\n// ── Paths ─────────────────────────────────────────────────────────────────────\n\nexport function getConfigPath(): string {\n return join(homedir(), \".jh-gateway\", \"config.json\");\n}\n\nfunction getConfigDir(): string {\n return join(homedir(), \".jh-gateway\");\n}\n\n// ── Defaults ──────────────────────────────────────────────────────────────────\n\nexport function getDefaultConfig(): GatewayConfig {\n return {\n cdpUrl: \"http://127.0.0.1:9222\",\n port: 8741,\n defaultModel: \"claude-opus-4.5\",\n defaultEndpoint: \"AnthropicClaude\",\n credentials: null,\n auth: { mode: \"none\", token: null },\n maxQueueWaitMs: 120000,\n };\n}\n\n// ── Validation ────────────────────────────────────────────────────────────────\n\nexport function validateConfig(raw: unknown): GatewayConfig {\n if (typeof raw !== \"object\" || raw === null) {\n throw new Error(\"Config validation error: config must be a JSON object\");\n }\n\n const c = raw as Record<string, unknown>;\n\n // cdpUrl\n if (typeof c.cdpUrl !== \"string\" || !/^https?:\\/\\//.test(c.cdpUrl)) {\n throw new Error(\n \"Config validation error: cdpUrl must be a valid URL starting with http:// or https://\"\n );\n }\n\n // port\n if (\n typeof c.port !== \"number\" ||\n !Number.isInteger(c.port) ||\n c.port < 1 ||\n c.port > 65535\n ) {\n throw new Error(\n \"Config validation error: port must be between 1 and 65535\"\n );\n }\n\n // defaultModel\n if (typeof c.defaultModel !== \"string\" || c.defaultModel.trim() === \"\") {\n throw new Error(\n \"Config validation error: defaultModel must be a non-empty string\"\n );\n }\n\n // defaultEndpoint\n if (\n typeof c.defaultEndpoint !== \"string\" ||\n c.defaultEndpoint.trim() === \"\"\n ) {\n throw new Error(\n \"Config validation error: defaultEndpoint must be a non-empty string\"\n );\n }\n\n // credentials\n if (c.credentials !== null && c.credentials !== undefined) {\n if (typeof c.credentials !== \"object\" || Array.isArray(c.credentials)) {\n throw new Error(\n \"Config validation error: credentials must be null or an object with bearerToken, cookie, and userAgent strings\"\n );\n }\n const creds = c.credentials as Record<string, unknown>;\n for (const field of [\"bearerToken\", \"cookie\", \"userAgent\"] as const) {\n if (typeof creds[field] !== \"string\") {\n throw new Error(\n `Config validation error: credentials.${field} must be a string`\n );\n }\n }\n }\n\n // auth\n if (typeof c.auth !== \"object\" || c.auth === null || Array.isArray(c.auth)) {\n throw new Error(\n \"Config validation error: auth must be an object with mode and token fields\"\n );\n }\n const auth = c.auth as Record<string, unknown>;\n if (\n auth.mode !== \"none\" &&\n auth.mode !== \"bearer\" &&\n auth.mode !== \"basic\"\n ) {\n throw new Error(\n 'Config validation error: auth.mode must be \"none\", \"bearer\", or \"basic\"'\n );\n }\n if (auth.token !== null && typeof auth.token !== \"string\") {\n throw new Error(\n \"Config validation error: auth.token must be null or a string\"\n );\n }\n\n // maxQueueWaitMs\n if (typeof c.maxQueueWaitMs !== \"number\" || c.maxQueueWaitMs <= 0) {\n throw new Error(\n \"Config validation error: maxQueueWaitMs must be a positive number\"\n );\n }\n\n return {\n cdpUrl: c.cdpUrl,\n port: c.port,\n defaultModel: c.defaultModel,\n defaultEndpoint: c.defaultEndpoint,\n credentials:\n c.credentials != null\n ? {\n bearerToken: (c.credentials as Record<string, unknown>).bearerToken as string,\n cookie: (c.credentials as Record<string, unknown>).cookie as string,\n userAgent: (c.credentials as Record<string, unknown>).userAgent as string,\n expiresAt: typeof (c.credentials as Record<string, unknown>).expiresAt === \"number\"\n ? ((c.credentials as Record<string, unknown>).expiresAt as number)\n : 0,\n }\n : null,\n auth: {\n mode: auth.mode,\n token: (auth.token) ?? null,\n },\n maxQueueWaitMs: c.maxQueueWaitMs,\n };\n}\n\n// ── I/O ───────────────────────────────────────────────────────────────────────\n\nexport async function loadConfig(): Promise<GatewayConfig> {\n const configPath = getConfigPath();\n\n let raw: string;\n try {\n raw = await readFile(configPath, \"utf8\");\n } catch (err: unknown) {\n // File or directory missing — create with defaults\n if (isNodeError(err) && (err.code === \"ENOENT\" || err.code === \"ENOTDIR\")) {\n const defaults = getDefaultConfig();\n await saveConfig(defaults);\n return defaults;\n }\n throw err;\n }\n\n let parsed: unknown;\n try {\n parsed = JSON.parse(raw);\n } catch {\n throw new Error(\n `Config validation error: config file at ${configPath} contains malformed JSON`\n );\n }\n\n const validated = validateConfig(parsed);\n await enforceConfigPermissions(configPath, getConfigDir());\n return validated;\n}\n\nexport async function saveConfig(config: GatewayConfig): Promise<void> {\n const configPath = getConfigPath();\n const configDir = getConfigDir();\n await mkdir(configDir, { recursive: true, mode: 0o700 });\n await writeFile(configPath, JSON.stringify(config, null, 2), {\n encoding: \"utf8\",\n mode: 0o600,\n });\n await enforceConfigPermissions(configPath, configDir);\n}\n\nexport async function updateConfig(\n partial: Partial<GatewayConfig>\n): Promise<void> {\n const current = await loadConfig();\n\n // Deep merge for nested `auth` object; shallow merge for everything else\n const updated: GatewayConfig = { ...current, ...partial };\n if (partial.auth !== undefined) {\n updated.auth = { ...current.auth, ...partial.auth };\n }\n\n await saveConfig(updated);\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction isNodeError(err: unknown): err is NodeJS.ErrnoException {\n return err instanceof Error && \"code\" in err;\n}\n\nasync function enforceConfigPermissions(\n configPath: string,\n configDir: string\n): Promise<void> {\n await chmod(configDir, 0o700).catch((err: unknown) => {\n // Best-effort: some filesystems/platforms may not support chmod semantics.\n const message = err instanceof Error ? err.message : String(err);\n console.warn(`[config] Could not enforce secure directory permissions: ${message}`);\n });\n await chmod(configPath, 0o600).catch((err: unknown) => {\n // Best-effort: some filesystems/platforms may not support chmod semantics.\n const message = err instanceof Error ? err.message : String(err);\n console.warn(`[config] Could not enforce secure file permissions: ${message}`);\n });\n}\n","import type { Browser, BrowserContext, Page } from \"playwright-core\";\nimport { connectToChrome } from \"../infra/chrome-cdp.js\";\nimport { updateConfig } from \"../infra/config.js\";\nimport type { CapturedCredentials } from \"../infra/types.js\";\n\nconst JH_HOST = \"chat.ai.jh.edu\";\n\nfunction isJhUrl(url: string): boolean {\n try {\n return new URL(url).hostname === JH_HOST;\n } catch {\n return false;\n }\n}\n\n/**\n * Decode the JWT `exp` claim from the middle (payload) segment.\n * Returns 0 if decoding fails for any reason.\n */\nexport function getTokenExpiry(token: string): number {\n try {\n const parts = token.split(\".\");\n if (parts.length < 2) return 0;\n\n // Base64url → base64 → decode\n const payload = parts[1]\n .replace(/-/g, \"+\")\n .replace(/_/g, \"/\");\n\n const json = Buffer.from(payload, \"base64\").toString(\"utf8\");\n const parsed = JSON.parse(json) as Record<string, unknown>;\n\n const exp = parsed[\"exp\"];\n if (typeof exp !== \"number\") return 0;\n return exp;\n } catch {\n return 0;\n }\n}\n\n/**\n * Connect to Chrome via CDP, intercept outgoing requests to chat.ai.jh.edu,\n * capture the Bearer token, cookies, and user agent, then persist to config.\n *\n * Throws a descriptive message if no token is captured within `timeoutMs`.\n */\nexport async function captureCredentials(\n cdpUrl: string,\n timeoutMs: number = 120_000\n): Promise<CapturedCredentials> {\n const { browser } = await connectToChrome(cdpUrl);\n const browserTyped = browser as Browser;\n\n // Find an existing JH page, or create a blank page (don't navigate yet).\n let page: Page | undefined;\n for (const ctx of browserTyped.contexts()) {\n for (const p of ctx.pages()) {\n if (p.url().includes(JH_HOST)) {\n page = p;\n break;\n }\n }\n if (page) break;\n }\n\n if (!page) {\n const contexts = browserTyped.contexts();\n const context = contexts.length > 0 ? contexts[0] : await browserTyped.newContext();\n page = await context.newPage();\n }\n\n const targetPage = page;\n const routePattern = \"**/*\";\n\n return new Promise<CapturedCredentials>((resolve, reject) => {\n let settled = false;\n let cleanedUp = false;\n\n const routeHandler = async (route: import(\"playwright-core\").Route) => {\n const request = route.request();\n const headers = await request.headers();\n\n const authHeader =\n headers[\"authorization\"] ?? headers[\"Authorization\"] ?? \"\";\n\n if (\n !settled &&\n authHeader.startsWith(\"Bearer \") &&\n isJhUrl(request.url())\n ) {\n try {\n const bearerToken = authHeader.slice(\"Bearer \".length).trim();\n\n // Collect cookies from the browser context.\n const rawCookies = await targetPage.context().cookies();\n const cookie = rawCookies\n .map((c) => `${c.name}=${c.value}`)\n .join(\"; \");\n\n // Capture the user agent from the page.\n const userAgent = await targetPage.evaluate(\n () => navigator.userAgent\n );\n\n const expiresAt = getTokenExpiry(bearerToken);\n\n const captured: CapturedCredentials = {\n bearerToken,\n cookie,\n userAgent,\n expiresAt,\n };\n\n // Persist to config store (fire-and-forget; errors surface via reject).\n await updateConfig({ credentials: captured });\n\n settleSuccess(captured);\n } catch (err) {\n settleFailure(err);\n }\n }\n\n // Always continue so the browser request is not blocked.\n await route.continue().catch((err: unknown) => {\n // Best-effort: request may already be handled/aborted during teardown.\n const message = err instanceof Error ? err.message : String(err);\n console.warn(`[auth-capture] Route continuation failed: ${message}`);\n });\n };\n\n const cleanup = async (): Promise<void> => {\n if (cleanedUp) return;\n cleanedUp = true;\n clearTimeout(timer);\n await targetPage.unroute(routePattern, routeHandler).catch((err: unknown) => {\n // Best-effort: page may be closing or route already removed.\n const message = err instanceof Error ? err.message : String(err);\n console.warn(`[auth-capture] Route cleanup failed: ${message}`);\n });\n };\n\n const settleSuccess = (captured: CapturedCredentials): void => {\n if (settled) return;\n settled = true;\n void cleanup().finally(() => resolve(captured));\n };\n\n const settleFailure = (err: unknown): void => {\n if (settled) return;\n settled = true;\n const error = err instanceof Error ? err : new Error(String(err));\n void cleanup().finally(() => reject(error));\n };\n\n const timer = setTimeout(() => {\n settleFailure(\n new Error(\n `Credential capture timed out after ${timeoutMs / 1000}s. ` +\n \"Please log in to chat.ai.jh.edu and send a message to trigger authentication.\"\n )\n );\n }, timeoutMs);\n\n // Register route intercept FIRST, then navigate so we don't miss\n // Bearer-carrying requests from the initial page load.\n targetPage\n .route(routePattern, routeHandler)\n .then(() => {\n // Route registered — now navigate to trigger auth requests.\n if (!isJhUrl(targetPage.url())) {\n targetPage.goto(`https://${JH_HOST}`, { waitUntil: \"commit\" }).catch(() => {});\n } else {\n // Already on JH — reload to trigger fresh API calls.\n targetPage.reload({ waitUntil: \"commit\" }).catch(() => {});\n }\n })\n .catch((err: unknown) => {\n settleFailure(err);\n });\n });\n}\n\n/**\n * Proactively capture credentials for background token refresh.\n *\n * Unlike captureCredentials() which waits passively for user interaction,\n * this opens a dedicated browser tab, navigates to the JH URL to trigger\n * the logged-in SPA's authenticated API calls, and captures the first\n * Bearer token seen. The dedicated tab is always closed when done.\n *\n * Throws if the JH session has expired (no Bearer calls appear) or on timeout.\n */\nexport async function captureCredentialsActive(\n cdpUrl: string,\n timeoutMs: number = 30_000\n): Promise<CapturedCredentials> {\n const { browser } = await connectToChrome(cdpUrl);\n const browserTyped = browser as Browser;\n const contexts = browserTyped.contexts();\n const context: BrowserContext =\n contexts.length > 0 ? contexts[0] : await browserTyped.newContext();\n const page = await context.newPage();\n\n try {\n return await new Promise<CapturedCredentials>((resolve, reject) => {\n let settled = false;\n\n const timer = setTimeout(() => {\n if (settled) return;\n settled = true;\n reject(\n new Error(\n `Active credential capture timed out after ${timeoutMs / 1000}s. ` +\n \"The JH session may have expired — restart without --headless to re-login.\"\n )\n );\n }, timeoutMs);\n\n page\n .route(\"**/*\", async (route) => {\n const request = route.request();\n const headers = await request.headers();\n const authHeader =\n headers[\"authorization\"] ?? headers[\"Authorization\"] ?? \"\";\n\n if (\n !settled &&\n authHeader.startsWith(\"Bearer \") &&\n isJhUrl(request.url())\n ) {\n settled = true;\n clearTimeout(timer);\n\n try {\n const bearerToken = authHeader.slice(\"Bearer \".length).trim();\n const rawCookies = await page.context().cookies();\n const cookie = rawCookies\n .map((c) => `${c.name}=${c.value}`)\n .join(\"; \");\n const userAgent = await page.evaluate(\n () => navigator.userAgent\n );\n const expiresAt = getTokenExpiry(bearerToken);\n const captured: CapturedCredentials = {\n bearerToken,\n cookie,\n userAgent,\n expiresAt,\n };\n await updateConfig({ credentials: captured });\n resolve(captured);\n } catch (err) {\n reject(err);\n }\n }\n\n await route.continue().catch(() => {});\n })\n .then(() => {\n // Route registered — navigate to JH to trigger the SPA's auth API calls\n page\n .goto(`https://${JH_HOST}`, { waitUntil: \"commit\" })\n .catch(() => {});\n })\n .catch((err: unknown) => {\n if (!settled) {\n settled = true;\n clearTimeout(timer);\n reject(err);\n }\n });\n });\n } finally {\n await page.close().catch(() => {});\n }\n}\n","import { randomBytes } from \"node:crypto\";\nimport { timingSafeEqual } from \"node:crypto\";\nimport type { MiddlewareHandler } from \"hono\";\nimport type { GatewayConfig } from \"./types.js\";\n\n/** Generate a random gateway API key prefixed with \"jh-local-\". */\nexport function generateApiKey(): string {\n return `jh-local-${randomBytes(16).toString(\"hex\")}`;\n}\n\n/**\n * Hono middleware that validates the Authorization header against the config.\n *\n * - mode \"none\": all requests pass through\n * - mode \"bearer\": requires `Authorization: Bearer <token>`\n * - mode \"basic\": requires `Authorization: Basic <base64(\"gateway:<token>\")>`\n */\nexport function authMiddleware(config: GatewayConfig): MiddlewareHandler {\n return async (c, next) => {\n const { mode, token } = config.auth;\n\n if (mode === \"none\") {\n return next();\n }\n\n const authHeader = c.req.header(\"authorization\");\n if (!authHeader) {\n return c.json(\n {\n error: {\n message: \"Missing Authorization header\",\n type: \"authentication_error\",\n code: \"missing_auth\",\n param: null,\n },\n },\n 401,\n );\n }\n\n if (mode === \"bearer\") {\n const expected = `Bearer ${token}`;\n if (!safeEquals(authHeader, expected)) {\n return c.json(\n {\n error: {\n message: \"Invalid bearer token\",\n type: \"authentication_error\",\n code: \"invalid_auth\",\n param: null,\n },\n },\n 401,\n );\n }\n } else if (mode === \"basic\") {\n // Expect Basic base64(\"gateway:<token>\")\n const expected = `Basic ${Buffer.from(`gateway:${token}`).toString(\"base64\")}`;\n if (!safeEquals(authHeader, expected)) {\n return c.json(\n {\n error: {\n message: \"Invalid basic credentials\",\n type: \"authentication_error\",\n code: \"invalid_auth\",\n param: null,\n },\n },\n 401,\n );\n }\n }\n\n return next();\n };\n}\n\nfunction safeEquals(actual: string, expected: string): boolean {\n const actualBuffer = Buffer.from(actual);\n const expectedBuffer = Buffer.from(expected);\n if (actualBuffer.length !== expectedBuffer.length) return false;\n return timingSafeEqual(actualBuffer, expectedBuffer);\n}\n","// Shared type definitions for jh-web-gateway\n\n// ── Credentials & Config ──────────────────────────────────────────────────────\n\nexport interface GatewayCredentials {\n bearerToken: string;\n cookie: string;\n userAgent: string;\n /** JWT exp claim (unix seconds), optional for backward compatibility */\n expiresAt?: number;\n}\n\nexport interface GatewayConfig {\n /** default: \"http://127.0.0.1:9222\" */\n cdpUrl: string;\n /** default: 8741 */\n port: number;\n /** default: \"claude-opus-4.5\" */\n defaultModel: string;\n /** default: \"AnthropicClaude\" */\n defaultEndpoint: string;\n credentials: GatewayCredentials | null;\n auth: {\n mode: \"none\" | \"bearer\" | \"basic\";\n token: string | null;\n };\n /** default: 120000 */\n maxQueueWaitMs: number;\n}\n\n// ── OpenAI Wire Types ─────────────────────────────────────────────────────────\n\nexport interface ToolCall {\n id: string;\n type: \"function\";\n index?: number;\n function: {\n name: string;\n arguments: string;\n };\n}\n\nexport interface ToolCallDelta {\n index: number;\n id?: string;\n type?: \"function\";\n function?: {\n name?: string;\n arguments?: string;\n };\n}\n\nexport interface OpenAIMessage {\n role: \"system\" | \"user\" | \"assistant\" | \"tool\";\n content: string | null;\n tool_call_id?: string;\n tool_calls?: ToolCall[];\n}\n\nexport interface OpenAITool {\n type: \"function\";\n function: {\n name: string;\n description?: string;\n parameters?: object;\n };\n}\n\nexport interface OpenAIChunk {\n id: string;\n object: \"chat.completion.chunk\";\n created: number;\n model: string;\n choices: [{\n index: number;\n delta: {\n role?: string;\n content?: string;\n tool_calls?: ToolCallDelta[];\n };\n finish_reason: string | null;\n }];\n}\n\nexport interface OpenAICompletion {\n id: string;\n object: \"chat.completion\";\n created: number;\n model: string;\n choices: [{\n index: number;\n message: {\n role: string;\n content: string | null;\n tool_calls?: ToolCall[];\n };\n finish_reason: string;\n }];\n usage: {\n prompt_tokens: number;\n completion_tokens: number;\n total_tokens: number;\n };\n}\n\nexport interface OpenAIErrorResponse {\n error: {\n message: string;\n type: string;\n code: string;\n param: string | null;\n };\n}\n\n// ── Tool Parsing ──────────────────────────────────────────────────────────────\n\nexport interface ParsedToolCall {\n id: string;\n name: string;\n /** valid JSON string */\n arguments: string;\n}\n\nexport interface ParsedContent {\n /** text outside tags */\n text: string;\n toolCalls: ParsedToolCall[];\n /** extracted think content, stripped from text */\n thinking: string | null;\n}\n\n// ── Logging ───────────────────────────────────────────────────────────────────\n\nexport interface RequestLogEntry {\n timestamp: string;\n method: string;\n path: string;\n model: string | null;\n statusCode: number;\n latencyMs: number;\n estimatedTokens: {\n prompt: number;\n completion: number;\n };\n}\n\n// ── Auth Capture ──────────────────────────────────────────────────────────────\n\nexport interface CapturedCredentials {\n bearerToken: string;\n cookie: string;\n userAgent: string;\n /** decoded from JWT exp claim */\n expiresAt: number;\n}\n\n// ── Browser Client ────────────────────────────────────────────────────────────\n\nexport interface ChatRequest {\n model: string;\n /** flattened from message-builder */\n prompt: string;\n conversationId?: string;\n parentMessageId?: string;\n}\n\nexport interface ChatResponse {\n rawSseText: string;\n conversationId: string;\n parentMessageId: string;\n}\n\n// ── Message Builder ───────────────────────────────────────────────────────────\n\nexport interface BuiltPrompt {\n /** flattened conversation text */\n prompt: string;\n /** extracted system message */\n systemPrompt?: string;\n}\n\n// ── Model → Endpoint Mapping ──────────────────────────────────────────────────\n\n/** Maps model IDs to JH platform endpoint paths */\nexport const MODEL_ENDPOINT_MAP: Record<string, string> = {\n \"claude-opus-4.5\": \"AnthropicClaude\",\n \"claude-sonnet-4.5\": \"AnthropicClaude\",\n \"claude-haiku-4.5\": \"AnthropicClaude\",\n \"gpt-4.1\": \"AzureOpenAI\",\n \"o3\": \"AzureOpenAI\",\n \"o3-mini\": \"AzureOpenAI\",\n \"gpt-5\": \"AzureOpenAI\",\n \"gpt-5.1\": \"AzureOpenAI\",\n \"gpt-5.2\": \"AzureOpenAI\",\n \"llama3-3-70b-instruct\": \"MetaLlama\",\n} as const;\n\n/** Maps model IDs to the display label expected by the JH platform */\nexport const MODEL_DISPLAY_LABEL_MAP: Record<string, string> = {\n \"claude-opus-4.5\": \"Claude\",\n \"claude-sonnet-4.5\": \"Claude\",\n \"claude-haiku-4.5\": \"Claude\",\n \"gpt-4.1\": \"ChatGPT\",\n \"o3\": \"ChatGPT\",\n \"o3-mini\": \"ChatGPT\",\n \"gpt-5\": \"ChatGPT\",\n \"gpt-5.1\": \"ChatGPT\",\n \"gpt-5.2\": \"ChatGPT\",\n \"llama3-3-70b-instruct\": \"MetaLlama\",\n} as const;\n","import type { Page } from \"playwright-core\";\nimport type {\n GatewayCredentials,\n ChatRequest,\n ChatResponse,\n} from \"../infra/types.js\";\nimport { MODEL_ENDPOINT_MAP, MODEL_DISPLAY_LABEL_MAP } from \"../infra/types.js\";\nimport { captureCredentials } from \"./auth-capture.js\";\n\nconst JH_API_BASE = \"https://chat.ai.jh.edu/api\";\nconst JH_DEFAULT_GREETING = \"Hello! How can I help you today?\";\nconst NULL_UUID = \"00000000-0000-0000-0000-000000000000\";\n\n/**\n * Check if a JWT bearer token has expired.\n * Returns true if the current time exceeds the `exp` claim.\n */\nexport function isTokenExpired(token: string): boolean {\n try {\n const parts = token.split(\".\");\n if (parts.length < 2) return true;\n const payload = parts[1].replace(/-/g, \"+\").replace(/_/g, \"/\");\n const json = Buffer.from(payload, \"base64\").toString(\"utf8\");\n const parsed = JSON.parse(json) as Record<string, unknown>;\n const exp = parsed[\"exp\"];\n if (typeof exp !== \"number\") return true;\n return Date.now() / 1000 > exp;\n } catch {\n return true;\n }\n}\n\n/**\n * Execute a chat request via in-browser fetch (Cloudflare bypass).\n * Auto-retries up to 2 times if the stream fetch fails (JH platform flakiness).\n */\nexport async function sendChatRequest(\n page: Page,\n credentials: GatewayCredentials,\n request: ChatRequest,\n options?: {\n cdpUrl?: string;\n onCredentialsRefreshed?: (creds: GatewayCredentials) => void;\n },\n): Promise<ChatResponse> {\n const MAX_ATTEMPTS = 3;\n for (let attempt = 1; attempt <= MAX_ATTEMPTS; attempt++) {\n try {\n return await sendChatRequestInner(page, credentials, request, options, false);\n } catch (err) {\n const error = err as Error & { statusCode?: number };\n // Only retry on 404 (stream not found) — other errors are real\n if (error.statusCode === 404 && attempt < MAX_ATTEMPTS) {\n console.log(`[gateway] Stream not found, retrying (attempt ${attempt + 1}/${MAX_ATTEMPTS})...`);\n // Brief pause before retry\n await new Promise((r) => setTimeout(r, 500));\n continue;\n }\n throw err;\n }\n }\n // Should never reach here\n throw new Error(\"Unexpected: all retry attempts exhausted\");\n}\n\nasync function sendChatRequestInner(\n page: Page,\n credentials: GatewayCredentials,\n request: ChatRequest,\n options: {\n cdpUrl?: string;\n onCredentialsRefreshed?: (creds: GatewayCredentials) => void;\n } | undefined,\n isRetry: boolean,\n): Promise<ChatResponse> {\n if (!isRetry && isTokenExpired(credentials.bearerToken)) {\n throw Object.assign(\n new Error(\"Bearer token has expired. Run `jh-gateway auth` to capture fresh credentials.\"),\n { statusCode: 401 },\n );\n }\n\n const endpoint = MODEL_ENDPOINT_MAP[request.model];\n if (!endpoint) {\n throw Object.assign(new Error(`Model '${request.model}' is not supported`), { statusCode: 400 });\n }\n\n const conversationId = request.conversationId ?? crypto.randomUUID();\n const parentMessageId = request.parentMessageId ?? NULL_UUID;\n const messageId = crypto.randomUUID();\n\n const body = {\n text: request.prompt,\n sender: \"User\",\n clientTimestamp: new Date().toISOString(),\n isCreatedByUser: true,\n parentMessageId,\n conversationId,\n messageId,\n error: false,\n endpoint,\n endpointType: \"custom\",\n model: request.model,\n resendFiles: true,\n greeting: JH_DEFAULT_GREETING,\n key: \"never\",\n modelDisplayLabel: MODEL_DISPLAY_LABEL_MAP[request.model] ?? endpoint,\n isTemporary: true,\n isRegenerate: false,\n isContinued: false,\n ephemeralAgent: {\n execute_code: false,\n web_search: false,\n file_search: false,\n artifacts: false,\n mcp: [],\n },\n };\n\n // Strategy: intercept the stream GET at the network level via page.route().\n // The page's JS fires the GET immediately after POST, but the stream may not\n // be ready yet (server needs time to set it up). We intercept the request,\n // retry with backoff until the stream is available, then fulfill the response\n // to both the page and our code.\n\n type Result = { error: boolean; status: number; statusText: string; body: string };\n\n let sseResolve: (v: Result) => void;\n const ssePromise = new Promise<Result>((res) => { sseResolve = res; });\n let sseResolved = false;\n\n const streamPattern = \"**/api/agents/chat/stream/*\";\n\n const routeHandler = async (route: import(\"playwright-core\").Route) => {\n console.log(`[gateway] Route handler intercepted: ${route.request().url()}`);\n const url = route.request().url();\n\n // Don't use route.fetch() for retries — each call consumes the stream.\n // Instead, poll with Node.js fetch until the stream is ready, then fulfill once.\n // Aggressive early polling — stream is usually ready within 100-300ms\n const delays = [0, 50, 100, 150, 250, 400, 700, 1200, 2000, 3500];\n let lastStatus = 0;\n let lastBody = \"\";\n let lastHeaders: Record<string, string> = {};\n\n // Extract headers from the original request to forward them\n const reqHeaders = route.request().headers();\n\n for (let i = 0; i < delays.length; i++) {\n if (delays[i] > 0) {\n await new Promise((r) => setTimeout(r, delays[i]));\n }\n try {\n // Use Node.js global fetch — NOT route.fetch() — to avoid consuming the stream\n const response = await fetch(url, {\n method: \"GET\",\n headers: {\n Accept: reqHeaders[\"accept\"] ?? \"text/event-stream\",\n Authorization: reqHeaders[\"authorization\"] ?? \"\",\n Cookie: reqHeaders[\"cookie\"] ?? \"\",\n Referer: reqHeaders[\"referer\"] ?? \"\",\n \"User-Agent\": reqHeaders[\"user-agent\"] ?? \"\",\n },\n });\n\n lastStatus = response.status;\n lastBody = await response.text();\n lastHeaders = {};\n response.headers.forEach((v, k) => { lastHeaders[k] = v; });\n\n if (lastStatus === 200) {\n console.log(`[gateway] Stream ready on attempt ${i + 1}, body length: ${lastBody.length}`);\n if (!sseResolved) {\n sseResolved = true;\n sseResolve({ error: false, status: 200, statusText: \"OK\", body: lastBody });\n }\n // Fulfill the intercepted route with the data we got\n await route.fulfill({ status: 200, headers: lastHeaders, body: lastBody });\n return;\n }\n\n if (lastStatus !== 404) {\n console.log(`[gateway] Stream non-404 error: ${lastStatus}, body: ${lastBody.slice(0, 200)}`);\n break;\n }\n } catch (err) {\n lastBody = String(err);\n lastStatus = 500;\n break;\n }\n }\n\n console.log(`[gateway] Stream fetch failed after ${delays.length} attempts, last status: ${lastStatus}`);\n if (!sseResolved) {\n sseResolved = true;\n sseResolve({ error: true, status: lastStatus, statusText: \"stream fetch failed\", body: lastBody });\n }\n try {\n await route.fulfill({ status: lastStatus, headers: lastHeaders, body: lastBody });\n } catch {\n try { await route.continue(); } catch { /* ignore */ }\n }\n };\n\n await page.route(streamPattern, routeHandler);\n\n let result!: Result;\n\n try {\n // POST to start the chat\n const postResult = await page.evaluate(\n async ({\n apiBase,\n bearerToken,\n endpointPath,\n requestBody,\n }: {\n apiBase: string;\n bearerToken: string;\n endpointPath: string;\n requestBody: Record<string, unknown>;\n }) => {\n try {\n const res = await fetch(`${apiBase}/agents/chat/${endpointPath}`, {\n method: \"POST\",\n headers: {\n \"Content-Type\": \"application/json\",\n Accept: \"text/event-stream\",\n Authorization: `Bearer ${bearerToken}`,\n },\n body: JSON.stringify(requestBody),\n });\n\n if (!res.ok) {\n return {\n error: true,\n status: res.status,\n statusText: res.statusText,\n body: (await res.text()).slice(0, 2000),\n contentType: \"\",\n };\n }\n\n const ct = res.headers.get(\"content-type\") ?? \"\";\n\n if (ct.includes(\"text/event-stream\")) {\n const reader = res.body?.getReader();\n if (!reader) return { error: true, status: 500, statusText: \"No body\", body: \"No SSE body\", contentType: ct };\n const decoder = new TextDecoder();\n let text = \"\";\n while (true) {\n const { done, value } = await reader.read();\n if (done) break;\n text += decoder.decode(value, { stream: true });\n }\n return { error: false, status: 200, statusText: \"OK\", body: text, contentType: ct };\n }\n\n const bodyText = await res.text();\n console.log(`[gateway] POST response content-type: ${ct}, body: ${bodyText.slice(0, 500)}`);\n return { error: false, status: 200, statusText: \"OK\", body: bodyText, contentType: ct };\n } catch (err) {\n return { error: true, status: 500, statusText: \"fetch error\", body: String(err), contentType: \"\" };\n }\n },\n {\n apiBase: JH_API_BASE,\n bearerToken: credentials.bearerToken,\n endpointPath: endpoint,\n requestBody: body as unknown as Record<string, unknown>,\n },\n );\n\n if (postResult.error) {\n result = { error: true, status: postResult.status, statusText: postResult.statusText ?? \"\", body: postResult.body };\n } else if (postResult.contentType.includes(\"text/event-stream\")) {\n result = { error: false, status: 200, statusText: \"OK\", body: postResult.body };\n } else {\n // POST returned JSON with streamId — the page's JS will GET the stream,\n // and our route handler will intercept it with retry logic.\n // Also fire our own GET as a fallback in case the page doesn't.\n let streamId: string | undefined;\n try {\n streamId = (JSON.parse(postResult.body) as { streamId?: string }).streamId;\n } catch { /* not JSON */ }\n\n if (!streamId) {\n result = { error: false, status: 200, statusText: \"OK\", body: postResult.body };\n } else {\n // Fire our own GET as fallback (the route handler intercepts it too)\n const streamUrl = `${JH_API_BASE}/agents/chat/stream/${streamId}`;\n page.evaluate(\n async ({ url, token }: { url: string; token: string }) => {\n await new Promise((r) => setTimeout(r, 10));\n try {\n await fetch(url, {\n method: \"GET\",\n headers: { Accept: \"text/event-stream\", Authorization: `Bearer ${token}` },\n });\n } catch { /* route handler captures it */ }\n },\n { url: streamUrl, token: credentials.bearerToken },\n ).catch(() => { /* ignore */ });\n\n // Wait for the route handler to resolve\n const timeout = new Promise<Result>((res) =>\n setTimeout(() => {\n if (!sseResolved) {\n sseResolved = true;\n res({ error: true, status: 408, statusText: \"timeout\", body: \"Stream capture timed out after 120s\" });\n }\n }, 120_000),\n );\n\n result = await Promise.race([ssePromise, timeout]);\n }\n }\n\n } finally {\n // Always clean up the route handler to prevent leaked interceptors on pooled pages\n await page.unroute(streamPattern, routeHandler).catch(() => {});\n }\n\n if (result.error) {\n const status = result.status as number;\n const responseBody = result.body as string;\n\n if (status === 401 && !isRetry) {\n const cdpUrl = options?.cdpUrl ?? \"http://127.0.0.1:9222\";\n try {\n await page.reload({ waitUntil: \"commit\" });\n const fresh = await captureCredentials(cdpUrl, 30_000);\n const newCreds: GatewayCredentials = {\n bearerToken: fresh.bearerToken,\n cookie: fresh.cookie,\n userAgent: fresh.userAgent,\n };\n options?.onCredentialsRefreshed?.(newCreds);\n return sendChatRequestInner(page, newCreds, request, options, true);\n } catch {\n throw Object.assign(\n new Error(\"JH platform returned 401 and automatic re-authentication failed. Run `jh-gateway auth` to capture fresh credentials.\"),\n { statusCode: 401 },\n );\n }\n }\n\n if (status === 401) {\n throw Object.assign(\n new Error(\"JH platform returned 401 after re-authentication attempt. Run `jh-gateway auth` to capture fresh credentials.\"),\n { statusCode: 401 },\n );\n }\n\n if (status === 403) {\n throw Object.assign(\n new Error(\"JH platform returned 403 — Cloudflare session has expired. Please open chat.ai.jh.edu in your browser, complete any challenge, then run `jh-gateway auth`.\"),\n { statusCode: 403 },\n );\n }\n\n throw Object.assign(new Error(`JH platform returned ${status}: ${responseBody}`), { statusCode: status });\n }\n\n console.log(`[gateway] Final result: error=${result.error}, status=${result.status}, body length=${result.body.length}`);\n\n const rawSseText = result.body as string;\n const { newConversationId, newParentMessageId } =\n extractConversationState(rawSseText, conversationId, messageId);\n\n return { rawSseText, conversationId: newConversationId, parentMessageId: newParentMessageId };\n}\n\nfunction extractConversationState(\n rawSse: string,\n fallbackConversationId: string | null,\n fallbackParentMessageId: string,\n): { newConversationId: string; newParentMessageId: string } {\n let newConversationId = fallbackConversationId ?? \"\";\n let newParentMessageId = fallbackParentMessageId;\n\n const blocks = rawSse.split(\"\\n\\n\");\n for (const block of blocks) {\n const trimmed = block.trim();\n if (!trimmed) continue;\n let event = \"\";\n let data = \"\";\n for (const line of trimmed.split(\"\\n\")) {\n if (line.startsWith(\"event: \")) event = line.slice(7).trim();\n else if (line.startsWith(\"data: \")) data = line.slice(6);\n else if (line.startsWith(\"data:\")) data = line.slice(5);\n }\n if (event === \"message\" && data) {\n try {\n const parsed = JSON.parse(data);\n if (parsed?.isCreatedByUser === false) {\n if (parsed.conversationId) newConversationId = parsed.conversationId;\n if (parsed.messageId) newParentMessageId = parsed.messageId;\n }\n // Also check nested message format\n const msg = parsed?.message;\n if (msg?.isCreatedByUser === false) {\n if (msg.conversationId) newConversationId = msg.conversationId;\n if (msg.messageId) newParentMessageId = msg.messageId;\n }\n } catch { /* skip */ }\n }\n }\n\n return { newConversationId, newParentMessageId };\n}\n","import { Hono } from \"hono\";\nimport { serve } from \"@hono/node-server\";\nimport { modelsRouter } from \"./routes/models.js\";\nimport { healthRouter } from \"./routes/health.js\";\nimport { chatCompletionsRouter } from \"./routes/chat-completions.js\";\nimport { authMiddleware } from \"./infra/gateway-auth.js\";\nimport { Logger } from \"./infra/logger.js\";\nimport type { PagePool } from \"./core/page-pool.js\";\nimport type { GatewayConfig, GatewayCredentials, RequestLogEntry } from \"./infra/types.js\";\nimport type { ReauthLock } from \"./core/reauth-lock.js\";\n\nexport interface ServerDeps {\n getPool: () => PagePool | null;\n getCredentials: () => GatewayConfig[\"credentials\"];\n reauthLock?: ReauthLock;\n setCredentials?: (creds: GatewayCredentials) => void;\n}\n\nexport function createServer(config: GatewayConfig, deps?: ServerDeps): Hono {\n const app = new Hono();\n const startTime = Date.now();\n const logger = new Logger();\n\n // Auth middleware on /v1/* routes\n app.use(\"/v1/*\", authMiddleware(config));\n\n // Request logging middleware — logs every API request with latency + token estimates\n app.use(\"*\", async (c, next) => {\n const start = Date.now();\n await next();\n const latencyMs = Date.now() - start;\n\n // Extract model from request body for chat completions (best-effort)\n let model: string | null = null;\n if (c.req.method === \"POST\" && c.req.path.includes(\"chat/completions\")) {\n try {\n // Body already consumed by route; read model from response or stored value\n const bodyText = await c.req.raw.clone().text();\n const parsed = JSON.parse(bodyText);\n model = typeof parsed?.model === \"string\" ? parsed.model : null;\n } catch {\n // Body may not be re-readable; that's fine\n }\n }\n\n // Approximate token estimates from response size (1 token ≈ 4 chars)\n const resBody = c.res?.headers?.get(\"content-length\");\n const resSize = resBody ? parseInt(resBody, 10) : 0;\n\n const entry: RequestLogEntry = {\n timestamp: new Date().toISOString(),\n method: c.req.method,\n path: c.req.path,\n model,\n statusCode: c.res.status,\n latencyMs,\n estimatedTokens: {\n prompt: 0,\n completion: Math.max(0, Math.ceil(resSize / 4)),\n },\n };\n\n // Fire-and-forget — don't block the response\n logger.log(entry).catch(() => { });\n });\n\n // Mount routes\n app.route(\"/v1/models\", modelsRouter(config));\n app.route(\"/health\", healthRouter(config, startTime, deps ? { getCredentials: deps.getCredentials } : undefined));\n\n if (deps) {\n app.route(\n \"/v1/chat/completions\",\n chatCompletionsRouter(config, {\n getPool: deps.getPool,\n getCredentials: deps.getCredentials,\n reauthLock: deps.reauthLock,\n setCredentials: deps.setCredentials,\n }),\n );\n }\n\n // Global error handler — OpenAI error format\n app.onError((err, c) => {\n return c.json(\n {\n error: {\n message: err.message || \"Internal server error\",\n type: \"server_error\",\n code: \"internal_error\",\n param: null,\n },\n },\n 500,\n );\n });\n\n // 404 handler — OpenAI error format\n app.notFound((c) => {\n return c.json(\n {\n error: {\n message: `Route ${c.req.method} ${c.req.path} not found`,\n type: \"invalid_request_error\",\n code: \"route_not_found\",\n param: null,\n },\n },\n 404,\n );\n });\n\n return app;\n}\n\nexport interface ServerHandle {\n close: () => Promise<void>;\n}\n\nexport async function startServer(\n config: GatewayConfig,\n deps: ServerDeps & { browser?: { close(): Promise<void> } },\n): Promise<ServerHandle> {\n const hostname = \"127.0.0.1\";\n const app = createServer(config, deps);\n const port = config.port;\n\n const server = await new Promise<ReturnType<typeof serve>>((resolve, reject) => {\n const s = serve({ fetch: app.fetch, port, hostname }, () => {\n s.removeListener(\"error\", onError);\n resolve(s);\n });\n const onError = (err: Error) => reject(err);\n s.once(\"error\", onError);\n });\n\n console.log(` Gateway listening on http://${hostname}:${port}`);\n\n let shuttingDown = false;\n const DRAIN_TIMEOUT_MS = 10_000;\n\n const shutdown = async () => {\n if (shuttingDown) { return; }\n shuttingDown = true;\n console.log(\"\\nShutting down gracefully...\");\n\n // Stop accepting new connections and drain in-flight requests\n await new Promise<void>((resolve) => {\n const timer = setTimeout(() => {\n console.log(\"Drain timeout reached, forcing close.\");\n resolve();\n }, DRAIN_TIMEOUT_MS);\n\n server.close(() => {\n clearTimeout(timer);\n resolve();\n });\n });\n\n // Close Chrome CDP connection if available\n if (deps.browser) {\n try {\n await deps.browser.close();\n } catch {\n // Browser may already be disconnected\n }\n }\n\n console.log(\"Shutdown complete.\");\n };\n\n return { close: shutdown };\n}\n","import { Hono } from \"hono\";\nimport { MODEL_ENDPOINT_MAP } from \"../infra/types.js\";\nimport type { GatewayConfig } from \"../infra/types.js\";\n\nconst MODEL_LIST = Object.keys(MODEL_ENDPOINT_MAP).map((id) => ({\n id,\n object: \"model\" as const,\n created: 1700000000,\n owned_by: \"jh-web\",\n}));\n\nconst MODEL_SET = new Set(Object.keys(MODEL_ENDPOINT_MAP));\n\nexport function modelsRouter(_config: GatewayConfig): Hono {\n const app = new Hono();\n\n app.get(\"/\", (c) => {\n return c.json({ object: \"list\", data: MODEL_LIST });\n });\n\n app.get(\"/:id\", (c) => {\n const id = c.req.param(\"id\");\n if (!MODEL_SET.has(id)) {\n return c.json(\n {\n error: {\n message: `Model '${id}' not found`,\n type: \"invalid_request_error\",\n code: \"model_not_found\",\n param: \"id\",\n },\n },\n 404\n );\n }\n return c.json({ id, object: \"model\", created: 1700000000, owned_by: \"jh-web\" });\n });\n\n return app;\n}\n","import { Hono } from \"hono\";\nimport { getTokenExpiry } from \"../core/auth-capture.js\";\nimport type { GatewayConfig, GatewayCredentials } from \"../infra/types.js\";\n\nexport function healthRouter(\n config: GatewayConfig,\n startTime: number,\n deps?: { getCredentials?: () => GatewayCredentials | null },\n): Hono {\n const app = new Hono();\n\n app.get(\"/\", (c) => {\n const uptime = (Date.now() - startTime) / 1000;\n\n // Prefer live credentials from CredentialHolder over the static config snapshot\n const liveCreds = deps?.getCredentials?.() ?? config.credentials;\n const tokenExpiry =\n liveCreds?.bearerToken\n ? getTokenExpiry(liveCreds.bearerToken) || null\n : null;\n\n const tokenExpired =\n tokenExpiry !== null && Date.now() / 1000 > tokenExpiry;\n\n return c.json({\n status: \"ok\",\n uptime,\n tokenExpiry,\n tokenExpired,\n cdpUrl: config.cdpUrl,\n });\n });\n\n return app;\n}\n","import { Hono } from \"hono\";\nimport { stream as honoStream } from \"hono/streaming\";\nimport type { GatewayConfig, GatewayCredentials, OpenAIMessage, OpenAITool } from \"../infra/types.js\";\nimport { MODEL_ENDPOINT_MAP } from \"../infra/types.js\";\nimport { buildPrompt } from \"../core/message-builder.js\";\nimport { sendChatRequest } from \"../core/client.js\";\nimport { translateToStream, translateToCompletion } from \"../core/stream-translator.js\";\nimport type { PagePool } from \"../core/page-pool.js\";\nimport type { ReauthLock } from \"../core/reauth-lock.js\";\nimport { captureCredentials } from \"../core/auth-capture.js\";\nimport { randomBytes } from \"node:crypto\";\n\nconst MODEL_SET = new Set(Object.keys(MODEL_ENDPOINT_MAP));\n\ninterface ChatCompletionsDeps {\n getPool: () => PagePool | null;\n getCredentials: () => GatewayConfig[\"credentials\"];\n reauthLock?: ReauthLock;\n setCredentials?: (creds: GatewayCredentials) => void;\n}\n\nexport function chatCompletionsRouter(\n _config: GatewayConfig,\n deps: ChatCompletionsDeps,\n): Hono {\n const app = new Hono();\n\n app.post(\"/\", async (c) => {\n // Parse request body\n let body: Record<string, unknown>;\n try {\n body = await c.req.json();\n } catch {\n return c.json(\n {\n error: {\n message: \"Request body must be valid JSON\",\n type: \"invalid_request_error\",\n code: \"invalid_json\",\n param: null,\n },\n },\n 400,\n );\n }\n\n // Validate required fields\n const model = body.model;\n if (typeof model !== \"string\" || !model) {\n return c.json(\n {\n error: {\n message: \"Missing required field: model\",\n type: \"invalid_request_error\",\n code: \"missing_field\",\n param: \"model\",\n },\n },\n 400,\n );\n }\n\n if (!MODEL_SET.has(model)) {\n return c.json(\n {\n error: {\n message: `Model '${model}' is not supported. Available models: ${[...MODEL_SET].join(\", \")}`,\n type: \"invalid_request_error\",\n code: \"model_not_found\",\n param: \"model\",\n },\n },\n 400,\n );\n }\n\n const messages = body.messages;\n if (!Array.isArray(messages) || messages.length === 0) {\n return c.json(\n {\n error: {\n message: \"Missing or empty required field: messages\",\n type: \"invalid_request_error\",\n code: \"missing_field\",\n param: \"messages\",\n },\n },\n 400,\n );\n }\n\n const shouldStream = body.stream === true;\n const tools = (body.tools as OpenAITool[] | undefined) ?? undefined;\n const toolChoice = body.tool_choice as\n | string\n | { type: string; function: { name: string } }\n | undefined;\n\n // Check page pool availability\n const pool = deps.getPool();\n if (!pool) {\n return c.json(\n {\n error: {\n message: \"Chrome browser is not connected. Run `jh-gateway setup` or `jh-gateway auth`.\",\n type: \"service_unavailable\",\n code: \"chrome_disconnected\",\n param: null,\n },\n },\n 503,\n );\n }\n\n const credentials = deps.getCredentials();\n if (!credentials) {\n return c.json(\n {\n error: {\n message: \"No credentials available. Run `jh-gateway auth` to capture credentials.\",\n type: \"authentication_error\",\n code: \"no_credentials\",\n param: null,\n },\n },\n 401,\n );\n }\n\n const completionId = `chatcmpl-${randomBytes(12).toString(\"hex\")}`;\n\n // Parallelize prompt building and page acquisition — they're independent\n const [built, acquired] = await Promise.all([\n Promise.resolve(buildPrompt(messages as OpenAIMessage[], tools, toolChoice)),\n pool.acquire(),\n ]);\n const { page, queue, release } = acquired;\n const stats = pool.stats;\n console.log(`[chat] Acquired page (pool: ${stats.busy}/${stats.total} busy)`);\n\n // Combine systemPrompt + prompt so tool definitions and system messages\n // reach the model (JH API only has a single `text` field).\n const fullPrompt = built.systemPrompt\n ? `${built.systemPrompt}\\n\\n${built.prompt}`\n : built.prompt;\n\n try {\n // Enqueue and execute via browser client\n const response = await queue.enqueue(() =>\n sendChatRequest(page, credentials, {\n model,\n prompt: fullPrompt,\n }),\n );\n\n if (shouldStream) {\n // Streaming SSE response\n const chunks = translateToStream(response.rawSseText, model, completionId);\n\n return honoStream(c, async (stream) => {\n c.header(\"Content-Type\", \"text/event-stream\");\n c.header(\"Cache-Control\", \"no-cache\");\n c.header(\"Connection\", \"keep-alive\");\n\n try {\n for (const chunk of chunks) {\n await stream.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n }\n } catch (streamErr: unknown) {\n // Mid-stream error — emit SSE error event before [DONE]\n const sErr = streamErr as Error;\n const errorEvent = {\n error: {\n message: sErr.message || \"An error occurred during streaming\",\n type: \"server_error\",\n code: \"stream_error\",\n param: null,\n },\n };\n await stream.write(`data: ${JSON.stringify(errorEvent)}\\n\\n`);\n } finally {\n release();\n }\n await stream.write(\"data: [DONE]\\n\\n\");\n });\n }\n\n // Non-streaming JSON response\n release();\n const completion = translateToCompletion(response.rawSseText, model, completionId);\n return c.json(completion);\n } catch (err: unknown) {\n release();\n const error = err as Error & { statusCode?: number };\n const statusCode = error.statusCode ?? 500;\n\n // ── 401 retry via ReauthLock ──────────────────────────────────────\n if (statusCode === 401 && deps.reauthLock) {\n try {\n const freshCreds = await deps.reauthLock.acquire(async () => {\n const captured = await captureCredentials(_config.cdpUrl);\n return {\n bearerToken: captured.bearerToken,\n cookie: captured.cookie,\n userAgent: captured.userAgent,\n };\n });\n\n // Update the credential holder so future requests use fresh creds\n deps.setCredentials?.(freshCreds);\n\n // Re-acquire a page and retry exactly once\n const retry = await pool.acquire();\n try {\n const retryResponse = await retry.queue.enqueue(() =>\n sendChatRequest(retry.page, freshCreds, {\n model: model as string,\n prompt: fullPrompt,\n }),\n );\n\n if (shouldStream) {\n const chunks = translateToStream(retryResponse.rawSseText, model as string, completionId);\n return honoStream(c, async (stream) => {\n c.header(\"Content-Type\", \"text/event-stream\");\n c.header(\"Cache-Control\", \"no-cache\");\n c.header(\"Connection\", \"keep-alive\");\n try {\n for (const chunk of chunks) {\n await stream.write(`data: ${JSON.stringify(chunk)}\\n\\n`);\n }\n } catch (streamErr: unknown) {\n const sErr = streamErr as Error;\n const errorEvent = {\n error: {\n message: sErr.message || \"An error occurred during streaming\",\n type: \"server_error\",\n code: \"stream_error\",\n param: null,\n },\n };\n await stream.write(`data: ${JSON.stringify(errorEvent)}\\n\\n`);\n } finally {\n retry.release();\n }\n await stream.write(\"data: [DONE]\\n\\n\");\n });\n }\n\n retry.release();\n const completion = translateToCompletion(retryResponse.rawSseText, model as string, completionId);\n return c.json(completion);\n } catch {\n retry.release();\n // Retry failed — fall through to return 401\n }\n } catch {\n // Re-capture itself failed — fall through to return 401\n }\n\n // If we reach here, either re-capture or retry failed → 401\n const reauthErrorBody = {\n error: {\n message: \"Authentication failed after automatic re-capture attempt.\",\n type: \"authentication_error\",\n code: \"upstream_error\",\n param: null,\n },\n };\n\n if (shouldStream) {\n return honoStream(c, async (stream) => {\n c.header(\"Content-Type\", \"text/event-stream\");\n c.header(\"Cache-Control\", \"no-cache\");\n c.header(\"Connection\", \"keep-alive\");\n await stream.write(`data: ${JSON.stringify(reauthErrorBody)}\\n\\n`);\n await stream.write(\"data: [DONE]\\n\\n\");\n });\n }\n\n return c.json(reauthErrorBody, 401);\n }\n\n // ── Standard error response ───────────────────────────────────────\n const typeMap: Record<number, string> = {\n 400: \"invalid_request_error\",\n 401: \"authentication_error\",\n 403: \"permission_error\",\n 429: \"rate_limit_error\",\n 503: \"service_unavailable\",\n };\n\n const errorBody = {\n error: {\n message: error.message || \"Internal server error\",\n type: typeMap[statusCode] ?? \"server_error\",\n code: statusCode === 429 ? \"queue_overflow\" : \"upstream_error\",\n param: null,\n },\n };\n\n // If streaming was requested, emit error as SSE event\n if (shouldStream) {\n return honoStream(c, async (stream) => {\n c.header(\"Content-Type\", \"text/event-stream\");\n c.header(\"Cache-Control\", \"no-cache\");\n c.header(\"Connection\", \"keep-alive\");\n await stream.write(`data: ${JSON.stringify(errorBody)}\\n\\n`);\n await stream.write(\"data: [DONE]\\n\\n\");\n });\n }\n\n return c.json(\n errorBody,\n statusCode as 400 | 401 | 403 | 429 | 500 | 503,\n );\n }\n });\n\n return app;\n}\n","import type { OpenAIMessage, OpenAITool, BuiltPrompt } from \"../infra/types.js\";\n\n/**\n * Format tool definitions as XML for injection into the system prompt.\n */\nexport function formatToolDefinitionsXml(tools: OpenAITool[]): string {\n const defs = tools.map((t) => {\n const fn = t.function;\n let xml = `<tool name=\"${fn.name}\"`;\n if (fn.description) {\n xml += ` description=\"${escapeXmlAttr(fn.description)}\"`;\n }\n xml += \">\";\n if (fn.parameters) {\n xml += `\\n <parameters>${JSON.stringify(fn.parameters)}</parameters>`;\n }\n xml += \"\\n</tool>\";\n return xml;\n });\n\n return `<tools>\\n${defs.join(\"\\n\")}\\n</tools>`;\n}\n\n/**\n * Format a tool result message as `<tool_response>` XML.\n */\nexport function formatToolResponse(toolCallId: string, content: string): string {\n return `<tool_response id=\"${toolCallId}\">${content}</tool_response>`;\n}\n\n/**\n * Convert OpenAI messages + tools into JH prompt format.\n *\n * Rules:\n * 1. system → prepended as system prompt section\n * 2. user → \"Human: {content}\"\n * 3. assistant → \"Assistant: {content}\" (including tool_calls as XML)\n * 4. tool → \"<tool_response id=\"{tool_call_id}\">{content}</tool_response>\"\n * 5. tools array → XML tool definitions injected into system prompt\n * 6. tool_choice: \"required\" → append instruction\n * 7. tool_choice: { function: { name } } → append specific tool instruction\n */\nexport function buildPrompt(\n messages: OpenAIMessage[],\n tools?: OpenAITool[],\n toolChoice?: string | { type: string; function: { name: string } },\n): BuiltPrompt {\n let systemPrompt: string | undefined;\n const parts: string[] = [];\n\n for (const msg of messages) {\n switch (msg.role) {\n case \"system\":\n // Accumulate system messages (rare to have multiple, but handle it)\n systemPrompt = systemPrompt\n ? `${systemPrompt}\\n${msg.content ?? \"\"}`\n : (msg.content ?? \"\");\n break;\n\n case \"user\":\n parts.push(`${msg.content ?? \"\"}`);\n break;\n\n case \"assistant\": {\n let content = msg.content ?? \"\";\n // If assistant message includes tool_calls, append them as XML\n if (msg.tool_calls && msg.tool_calls.length > 0) {\n const toolXml = msg.tool_calls\n .map(\n (tc) =>\n `<tool_call id=\"${tc.id}\" name=\"${tc.function.name}\">${tc.function.arguments}</tool_call>`,\n )\n .join(\"\");\n content = content ? `${content}\\n${toolXml}` : toolXml;\n }\n parts.push(`Assistant: ${content}`);\n break;\n }\n\n case \"tool\":\n parts.push(formatToolResponse(msg.tool_call_id ?? \"unknown\", msg.content ?? \"\"));\n break;\n }\n }\n\n // Inject tool definitions into system prompt if tools are provided\n if (tools && tools.length > 0) {\n const toolsXml = formatToolDefinitionsXml(tools);\n const toolInstructions =\n \"You have access to the following tools. To use a tool, respond with \" +\n '<tool_call id=\"call_ID\" name=\"TOOL_NAME\">ARGUMENTS_JSON</tool_call>';\n const injection = `${toolInstructions}\\n\\n${toolsXml}`;\n systemPrompt = systemPrompt ? `${systemPrompt}\\n\\n${injection}` : injection;\n }\n\n // Append tool_choice instructions\n if (toolChoice) {\n let instruction = \"\";\n if (toolChoice === \"required\") {\n instruction = \"You MUST use a tool in your response.\";\n } else if (typeof toolChoice === \"object\" && toolChoice.function?.name) {\n instruction = `You MUST use the tool \"${toolChoice.function.name}\" in your response.`;\n }\n if (instruction) {\n parts.push(instruction);\n }\n }\n\n return {\n prompt: parts.join(\"\\n\\n\"),\n systemPrompt,\n };\n}\n\n// ── Helpers ───────────────────────────────────────────────────────────────────\n\nfunction escapeXmlAttr(s: string): string {\n return s.replace(/&/g, \"&amp;\").replace(/\"/g, \"&quot;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n","import { randomBytes } from \"node:crypto\";\nimport type { OpenAIChunk, OpenAICompletion, ToolCallDelta } from \"../infra/types.js\";\nimport { parseToolsAndThinking, toOpenAIToolCalls, StreamingToolBuffer } from \"./tool-parser.js\";\n\n// ── SSE Parsing ───────────────────────────────────────────────────────────────\n\ninterface SseEvent {\n event: string;\n data: string;\n}\n\n/** Parse raw SSE text into individual events. */\nfunction parseSseEvents(rawSse: string): SseEvent[] {\n const events: SseEvent[] = [];\n const blocks = rawSse.split(\"\\n\\n\");\n\n for (const block of blocks) {\n const trimmed = block.trim();\n if (!trimmed) continue;\n\n let event = \"\";\n let data = \"\";\n\n for (const line of trimmed.split(\"\\n\")) {\n if (line.startsWith(\"event: \")) {\n event = line.slice(7).trim();\n } else if (line.startsWith(\"data: \")) {\n data = line.slice(6);\n } else if (line.startsWith(\"data:\")) {\n data = line.slice(5);\n }\n }\n\n if (event || data) {\n events.push({ event, data });\n }\n }\n\n return events;\n}\n\n/**\n * Resolve the effective event type from a JH SSE event.\n *\n * JH wraps all events in `event: message` and puts the real type\n * in the JSON payload's `event` field. We also support the flat format\n * (event: on_message_delta) for test compatibility.\n */\nfunction resolveEventType(ev: SseEvent): { type: string; parsed: Record<string, unknown> | null } {\n // Flat format: event line already has the real type\n if (ev.event && ev.event !== \"message\") {\n try {\n return { type: ev.event, parsed: JSON.parse(ev.data) };\n } catch {\n return { type: ev.event, parsed: null };\n }\n }\n\n // JH wrapped format: event: message, real type in JSON .event field\n if (ev.data) {\n try {\n const parsed = JSON.parse(ev.data) as Record<string, unknown>;\n const jsonEvent = typeof parsed.event === \"string\" ? parsed.event : null;\n return { type: jsonEvent ?? ev.event, parsed };\n } catch {\n return { type: ev.event, parsed: null };\n }\n }\n\n return { type: ev.event, parsed: null };\n}\n\n/** Check if an event is a user echo (skip). */\nfunction isUserEcho(parsed: Record<string, unknown> | null): boolean {\n if (!parsed) return false;\n // Direct format: { isCreatedByUser: true }\n if (parsed.isCreatedByUser === true) return true;\n // Nested format: { message: { isCreatedByUser: true } }\n const msg = parsed.message as Record<string, unknown> | undefined;\n if (msg?.isCreatedByUser === true || msg?.sender === \"User\") return true;\n return false;\n}\n\n/**\n * Extract text delta from an on_message_delta event payload.\n * Supports both flat format (delta at top level) and nested format (inside .data).\n */\nfunction extractDeltaText(parsed: Record<string, unknown> | null): string | null {\n if (!parsed) return null;\n\n // Try flat format: { delta: { content: [...] } }\n let content = (parsed.delta as Record<string, unknown>)?.content;\n\n // Try nested format: { data: { delta: { content: [...] } } }\n if (!content) {\n const dataObj = parsed.data as Record<string, unknown> | undefined;\n content = (dataObj?.delta as Record<string, unknown>)?.content;\n }\n\n if (!Array.isArray(content)) return null;\n\n const texts: string[] = [];\n for (const item of content) {\n if (item?.type === \"text\" && typeof item.text === \"string\") {\n texts.push(item.text);\n }\n }\n return texts.length > 0 ? texts.join(\"\") : null;\n}\n\n/**\n * Extract accumulated text from a final message event.\n * JH sends { message: { text: \"full accumulated text\", isCreatedByUser: false } }\n */\nfunction extractMessageText(parsed: Record<string, unknown> | null): string | null {\n if (!parsed) return null;\n const msg = parsed.message as Record<string, unknown> | undefined;\n if (!msg) return null;\n if (msg.isCreatedByUser === true || msg.sender === \"User\") return null;\n if (typeof msg.text === \"string\" && msg.text) return msg.text;\n return null;\n}\n\n// ── Public API ────────────────────────────────────────────────────────────────\n\n/** Generate a unique completion ID. */\nfunction generateCompletionId(): string {\n return `chatcmpl-${randomBytes(12).toString(\"hex\")}`;\n}\n/**\n * Extract text content directly from JH SSE events.\n * Supports both flat SSE format and JH's wrapped format.\n */\nexport function extractContentFromJhSse(rawSse: string): string {\n const events = parseSseEvents(rawSse);\n const parts: string[] = [];\n let lastMessageText = \"\";\n\n for (const ev of events) {\n const { type, parsed } = resolveEventType(ev);\n if (isUserEcho(parsed)) continue;\n if (type === \"on_run_step\") continue;\n\n if (type === \"on_message_delta\") {\n const delta = extractDeltaText(parsed);\n if (delta !== null) {\n parts.push(delta);\n }\n }\n\n // Also handle final message events with accumulated text\n if (type === \"message\" || !type) {\n const msgText = extractMessageText(parsed);\n if (msgText && msgText.length > lastMessageText.length) {\n lastMessageText = msgText;\n }\n }\n }\n\n // If we got deltas, use those. Otherwise fall back to accumulated message text.\n const deltaText = parts.join(\"\");\n return deltaText || lastMessageText;\n}\n\n/**\n * Parse JH SSE text and return OpenAI SSE chunks.\n * Skips user echoes and metadata events.\n * Uses StreamingToolBuffer to detect <tool_call> XML and emit proper\n * tool_calls delta objects instead of leaking raw XML as content.\n */\nexport function translateToStream(\n rawSse: string,\n model: string,\n completionId?: string,\n): OpenAIChunk[] {\n const id = completionId ?? generateCompletionId();\n const created = Math.floor(Date.now() / 1000);\n const events = parseSseEvents(rawSse);\n const chunks: OpenAIChunk[] = [];\n let lastMessageText = \"\";\n const toolBuf = new StreamingToolBuffer();\n let toolCallIndex = 0;\n let hasToolCalls = false;\n\n // First chunk: role announcement\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model,\n choices: [{ index: 0, delta: { role: \"assistant\" }, finish_reason: null }],\n });\n\n let gotDeltas = false;\n\n /** Push a raw text fragment through the tool buffer and emit chunks. */\n function processDelta(rawDelta: string): void {\n const { text, completedCalls } = toolBuf.push(rawDelta);\n\n // Emit safe text (outside any tool_call tag) as content\n if (text) {\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model,\n choices: [{ index: 0, delta: { content: text }, finish_reason: null }],\n });\n }\n\n // Emit completed tool calls as proper tool_calls deltas\n for (const call of completedCalls) {\n hasToolCalls = true;\n const toolDelta: ToolCallDelta = {\n index: toolCallIndex++,\n id: call.id,\n type: \"function\",\n function: {\n name: call.name,\n arguments: call.arguments,\n },\n };\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model,\n choices: [{ index: 0, delta: { tool_calls: [toolDelta] }, finish_reason: null }],\n });\n }\n }\n\n for (const ev of events) {\n const { type, parsed } = resolveEventType(ev);\n if (isUserEcho(parsed)) continue;\n if (type === \"on_run_step\") continue;\n\n if (type === \"on_message_delta\") {\n const delta = extractDeltaText(parsed);\n if (delta !== null) {\n gotDeltas = true;\n processDelta(delta);\n }\n }\n\n // Track accumulated message text as fallback\n if (type === \"message\" || !type) {\n const msgText = extractMessageText(parsed);\n if (msgText && msgText.length > lastMessageText.length) {\n const delta = msgText.slice(lastMessageText.length);\n lastMessageText = msgText;\n if (!gotDeltas && delta) {\n processDelta(delta);\n }\n }\n }\n }\n\n // Flush any remaining buffered content (partial/malformed tags become text)\n const flushed = toolBuf.flush();\n if (flushed) {\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model,\n choices: [{ index: 0, delta: { content: flushed }, finish_reason: null }],\n });\n }\n\n // Final chunk: finish_reason \"tool_calls\" if any tool calls were emitted, else \"stop\"\n chunks.push({\n id,\n object: \"chat.completion.chunk\",\n created,\n model,\n choices: [{ index: 0, delta: {}, finish_reason: hasToolCalls ? \"tool_calls\" : \"stop\" }],\n });\n\n return chunks;\n}\n\n/**\n * Parse JH SSE text and return a single OpenAI completion JSON.\n * Collects full response text, parses tool calls, estimates token usage.\n */\nexport function translateToCompletion(\n rawSse: string,\n model: string,\n completionId?: string,\n): OpenAICompletion {\n const id = completionId ?? generateCompletionId();\n const created = Math.floor(Date.now() / 1000);\n\n const fullText = extractContentFromJhSse(rawSse);\n const parsed = parseToolsAndThinking(fullText);\n\n // Approximate token estimates (1 token ≈ 4 chars)\n const promptTokens = 0;\n const completionTokens = Math.max(1, Math.ceil(fullText.length / 4));\n\n const message: OpenAICompletion[\"choices\"][0][\"message\"] = {\n role: \"assistant\",\n content: parsed.text || null,\n };\n\n if (parsed.toolCalls.length > 0) {\n message.tool_calls = toOpenAIToolCalls(parsed.toolCalls);\n }\n\n return {\n id,\n object: \"chat.completion\",\n created,\n model,\n choices: [{ index: 0, message, finish_reason: parsed.toolCalls.length > 0 ? \"tool_calls\" : \"stop\" }],\n usage: {\n prompt_tokens: promptTokens,\n completion_tokens: completionTokens,\n total_tokens: promptTokens + completionTokens,\n },\n };\n}\n","import type { ParsedToolCall, ParsedContent, ToolCall } from \"../infra/types.js\";\n\n// ── Regex patterns ────────────────────────────────────────────────────────────\n\nconst TOOL_CALL_RE =\n /<tool_call\\s+id=\"([^\"]*)\"\\s+name=\"([^\"]*)\">([\\s\\S]*?)<\\/tool_call>/g;\n\nconst THINK_RE = /<think>([\\s\\S]*?)<\\/think>/g;\n\n// Detects an opening <tool_call that never closes — used for malformed detection\nconst PARTIAL_TOOL_CALL_RE = /<tool_call\\b[^>]*>(?:(?!<\\/tool_call>)[\\s\\S])*$/;\n\n/**\n * Parse response text, extracting `<tool_call>` and `<think>` tags.\n * Malformed XML is emitted as raw text (never throws).\n */\nexport function parseToolsAndThinking(text: string): ParsedContent {\n const toolCalls: ParsedToolCall[] = [];\n let thinking: string | null = null;\n\n // Extract think tags first, accumulate thinking content\n const thinkMatches = [...text.matchAll(THINK_RE)];\n if (thinkMatches.length > 0) {\n thinking = thinkMatches.map((m) => m[1]).join(\"\\n\");\n }\n\n // Remove think tags from text for further processing\n let remaining = text.replace(THINK_RE, \"\");\n\n // Extract well-formed tool_call tags\n const toolMatches = [...remaining.matchAll(TOOL_CALL_RE)];\n for (const match of toolMatches) {\n const id = match[1];\n const name = match[2];\n const rawArgs = match[3].trim();\n\n // Validate JSON arguments; if invalid, keep as-is (still valid JSON string)\n let args: string;\n try {\n JSON.parse(rawArgs);\n args = rawArgs;\n } catch {\n // Malformed JSON in arguments — wrap as string to keep valid JSON\n args = JSON.stringify(rawArgs);\n }\n\n toolCalls.push({ id, name, arguments: args });\n }\n\n // Remove well-formed tool_call tags from remaining text\n remaining = remaining.replace(TOOL_CALL_RE, \"\");\n\n // Any leftover partial/malformed <tool_call> tags stay as raw text\n const cleanedText = remaining.trim();\n\n return {\n text: cleanedText,\n toolCalls,\n thinking,\n };\n}\n\n/**\n * Convert ParsedToolCall[] to OpenAI tool_calls format with incrementing index.\n */\nexport function toOpenAIToolCalls(calls: ParsedToolCall[]): ToolCall[] {\n return calls.map((call, index) => ({\n id: call.id,\n type: \"function\" as const,\n index,\n function: {\n name: call.name,\n arguments: call.arguments,\n },\n }));\n}\n\n/**\n * Reconstruct XML from parsed tool calls (for round-trip testing).\n */\nexport function toToolCallXml(calls: ParsedToolCall[]): string {\n return calls\n .map((c) => `<tool_call id=\"${c.id}\" name=\"${c.name}\">${c.arguments}</tool_call>`)\n .join(\"\");\n}\n\n// ── Streaming buffer support ──────────────────────────────────────────────────\n\n/**\n * Streaming XML buffer that holds partial `<tool_call>` content until a\n * complete closing tag boundary is found.\n *\n * Feed chunks via `push()`. Completed tool calls are returned; any buffered\n * partial content is held until the closing tag arrives.\n */\nexport class StreamingToolBuffer {\n private buffer = \"\";\n\n /**\n * Push a text chunk. Returns an object with:\n * - `text`: safe-to-emit text (outside any partial tag)\n * - `completedCalls`: fully parsed tool calls from this chunk\n */\n push(chunk: string): { text: string; completedCalls: ParsedToolCall[] } {\n this.buffer += chunk;\n\n const completedCalls: ParsedToolCall[] = [];\n let safeText = \"\";\n\n // Extract all complete tool_call tags\n let match: RegExpExecArray | null;\n const re = new RegExp(TOOL_CALL_RE.source, \"g\");\n\n let lastIndex = 0;\n while ((match = re.exec(this.buffer)) !== null) {\n // Text before this match is safe to emit\n safeText += this.buffer.slice(lastIndex, match.index);\n\n const id = match[1];\n const name = match[2];\n const rawArgs = match[3].trim();\n\n let args: string;\n try {\n JSON.parse(rawArgs);\n args = rawArgs;\n } catch {\n args = JSON.stringify(rawArgs);\n }\n\n completedCalls.push({ id, name, arguments: args });\n lastIndex = match.index + match[0].length;\n }\n\n // Remaining buffer after all complete matches\n const remainder = this.buffer.slice(lastIndex);\n\n // Check if remainder contains a partial opening tag\n if (PARTIAL_TOOL_CALL_RE.test(remainder)) {\n // Hold the partial tag in the buffer; emit text before it\n const partialStart = remainder.search(/<tool_call\\b/);\n if (partialStart > 0) {\n safeText += remainder.slice(0, partialStart);\n }\n this.buffer = partialStart >= 0 ? remainder.slice(partialStart) : remainder;\n } else {\n // No partial tag — emit everything, clear buffer\n safeText += remainder;\n this.buffer = \"\";\n }\n\n return { text: safeText, completedCalls };\n }\n\n /** Flush any remaining buffer content as raw text (end of stream). */\n flush(): string {\n const remaining = this.buffer;\n this.buffer = \"\";\n return remaining;\n }\n}\n","import { appendFile, readFile, readdir, mkdir } from \"node:fs/promises\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport type { RequestLogEntry } from \"./types.js\";\n\nconst DEFAULT_LOG_DIR = join(homedir(), \".jh-gateway\", \"logs\");\n\n/** Format a Date as YYYY-MM-DD for daily log file rotation. */\nfunction dateStamp(d: Date = new Date()): string {\n return d.toISOString().slice(0, 10);\n}\n\nexport class Logger {\n private logDir: string;\n private dirCreated = false;\n\n constructor(logDir?: string) {\n this.logDir = logDir ?? DEFAULT_LOG_DIR;\n }\n\n /** Ensure the log directory exists (lazy, once per instance). */\n private async ensureDir(): Promise<void> {\n if (this.dirCreated) {return;}\n await mkdir(this.logDir, { recursive: true });\n this.dirCreated = true;\n }\n\n /** Append a log entry to today's JSONL file. */\n async log(entry: RequestLogEntry): Promise<void> {\n await this.ensureDir();\n const file = join(this.logDir, `${dateStamp()}.jsonl`);\n await appendFile(file, JSON.stringify(entry) + \"\\n\", \"utf8\");\n }\n\n /** Query log entries. Reads from a specific date or the most recent file. */\n async query(options: { date?: string; limit?: number } = {}): Promise<RequestLogEntry[]> {\n await this.ensureDir();\n const limit = options.limit ?? 100;\n\n if (options.date) {\n return this.readLogFile(join(this.logDir, `${options.date}.jsonl`), limit);\n }\n\n // Read from most recent files until we have enough entries\n let names: string[];\n try {\n names = await readdir(this.logDir);\n } catch {\n return [];\n }\n\n const jsonlFiles = names\n .filter((n) => n.endsWith(\".jsonl\"))\n .toSorted()\n .toReversed();\n\n const entries: RequestLogEntry[] = [];\n for (const name of jsonlFiles) {\n if (entries.length >= limit) {break;}\n const batch = await this.readLogFile(join(this.logDir, name), limit - entries.length);\n entries.push(...batch);\n }\n\n return entries.slice(0, limit);\n }\n\n private async readLogFile(filePath: string, limit: number): Promise<RequestLogEntry[]> {\n let raw: string;\n try {\n raw = await readFile(filePath, \"utf8\");\n } catch {\n return [];\n }\n\n const entries: RequestLogEntry[] = [];\n const lines = raw.split(\"\\n\");\n // Read from end for most-recent-first\n for (let i = lines.length - 1; i >= 0 && entries.length < limit; i--) {\n const line = lines[i].trim();\n if (!line) {continue;}\n try {\n entries.push(JSON.parse(line) as RequestLogEntry);\n } catch {\n // Skip malformed lines\n }\n }\n return entries;\n }\n}\n","/**\n * FIFO promise queue that serializes async tasks so only one executes at a time.\n * Rejects with a 429-style error when queue wait exceeds maxWaitMs.\n */\nexport class RequestQueue {\n private queue: Array<() => void> = [];\n private running = false;\n private maxWaitMs: number;\n\n constructor(maxWaitMs: number = 120_000) {\n this.maxWaitMs = maxWaitMs;\n }\n\n /** Current number of tasks waiting in the queue (not including the active one). */\n get pending(): number {\n return this.queue.length;\n }\n\n /** Enqueue a task. Resolves when the task completes. Rejects on timeout or task error. */\n async enqueue<T>(task: () => Promise<T>): Promise<T> {\n // Wait for our turn\n await this.waitForTurn();\n\n try {\n return await task();\n } finally {\n // Adaptive cooldown: only delay if there are queued requests waiting.\n // The JH platform needs a brief pause between generations; 100ms is\n // enough for cleanup while keeping throughput high.\n if (this.queue.length > 0) {\n await new Promise((r) => setTimeout(r, 100));\n }\n this.release();\n }\n }\n\n private waitForTurn(): Promise<void> {\n if (!this.running) {\n this.running = true;\n return Promise.resolve();\n }\n\n return new Promise<void>((resolve, reject) => {\n const timer = setTimeout(() => {\n // Remove ourselves from the queue\n const idx = this.queue.indexOf(release);\n if (idx !== -1) this.queue.splice(idx, 1);\n reject(\n Object.assign(\n new Error(\n `Request queue wait exceeded ${this.maxWaitMs}ms — server is overloaded`,\n ),\n { statusCode: 429 },\n ),\n );\n }, this.maxWaitMs);\n\n const release = () => {\n clearTimeout(timer);\n resolve();\n };\n\n this.queue.push(release);\n });\n }\n\n private release(): void {\n const next = this.queue.shift();\n if (next) {\n next();\n } else {\n this.running = false;\n }\n }\n}\n","/**\n * Pool of browser pages for concurrent request handling.\n * Each page has its own RequestQueue (concurrency=1), but with N pages\n * we can handle N concurrent requests to the upstream.\n */\nimport type { Page, Browser } from \"playwright-core\";\nimport { RequestQueue } from \"./request-queue.js\";\n\nexport interface PooledPage {\n page: Page;\n queue: RequestQueue;\n inUse: boolean;\n}\n\nexport class PagePool {\n private pages: PooledPage[] = [];\n private browser: Browser | null = null;\n private targetUrl: string;\n private maxPages: number;\n private maxWaitMs: number;\n private initPromise: Promise<void> | null = null;\n private pagesCreating = 0;\n private warmedUp = false;\n private disconnected = false;\n\n constructor(options: {\n targetUrl?: string;\n maxPages?: number;\n maxWaitMs?: number;\n } = {}) {\n this.targetUrl = options.targetUrl ?? \"https://chat.ai.jh.edu\";\n this.maxPages = options.maxPages ?? 1;\n this.maxWaitMs = options.maxWaitMs ?? 120_000;\n }\n\n /** Initialize the pool with an existing browser connection and seed page */\n async init(browser: Browser, seedPage: Page): Promise<void> {\n if (this.initPromise) return this.initPromise;\n\n this.initPromise = this._doInit(browser, seedPage);\n return this.initPromise;\n }\n\n private async _doInit(browser: Browser, seedPage: Page): Promise<void> {\n this.browser = browser;\n this.disconnected = false;\n\n // Detect CDP disconnection so we stop handing out dead pages\n browser.on(\"disconnected\", () => {\n console.warn(\"[PagePool] Browser disconnected — all pages are now invalid\");\n this.disconnected = true;\n });\n\n // Add the seed page as the first pooled page\n this.pages.push({\n page: seedPage,\n queue: new RequestQueue(this.maxWaitMs),\n inUse: false,\n });\n\n console.log(`[PagePool] Initialized with 1 page, will scale up to ${this.maxPages}`);\n }\n\n /** Get pool statistics */\n get stats(): { total: number; busy: number; available: number } {\n const busy = this.pages.filter(p => p.inUse).length;\n return {\n total: this.pages.length,\n busy,\n available: this.pages.length - busy,\n };\n }\n\n /**\n * Acquire a page for use. Creates new pages on-demand up to maxPages.\n * Note: We intentionally don't lock here — allowing multiple requests to\n * grab the same page and queue on it is actually faster than creating new pages.\n *\n * On first init (before any request succeeds), page scaling is disabled to\n * avoid opening new Chrome tabs that may redirect through SSO and hang.\n * Call `markWarmedUp()` after the first successful request to enable scaling.\n */\n async acquire(): Promise<{ page: Page; queue: RequestQueue; release: () => void }> {\n if (this.disconnected) {\n throw Object.assign(\n new Error(\"Browser has disconnected. Restart the gateway to reconnect.\"),\n { statusCode: 503 },\n );\n }\n\n // Evict dead/navigated-away pages before selecting\n this.evictDeadPages();\n\n // First, try to find an available (non-busy) page\n let pooled = this.pages.find(p => !p.inUse);\n\n // Only scale up if the pool is warmed up (first request has succeeded).\n // Before warm-up, queue on the seed page to avoid opening new tabs that\n // may redirect through SSO on a fresh session and hang visibly.\n if (!pooled && this.warmedUp && (this.pages.length + this.pagesCreating) < this.maxPages && this.browser) {\n this.pagesCreating++;\n try {\n pooled = await this.createPage();\n } finally {\n this.pagesCreating--;\n }\n }\n\n // If still no page available, pick the one with the smallest queue\n if (!pooled) {\n if (this.pages.length === 0) {\n throw Object.assign(\n new Error(\"No healthy browser pages available. Restart the gateway.\"),\n { statusCode: 503 },\n );\n }\n pooled = this.pages.reduce((a, b) =>\n a.queue.pending <= b.queue.pending ? a : b\n );\n }\n\n pooled.inUse = true;\n const p = pooled;\n\n return {\n page: p.page,\n queue: p.queue,\n release: () => {\n p.inUse = false;\n // Auto warm-up after the first successful release\n if (!this.warmedUp) {\n this.warmedUp = true;\n console.log(`[PagePool] Warm-up complete — page scaling enabled (max ${this.maxPages})`);\n // Pre-warm a second page in the background if maxPages > 1\n if (this.maxPages > 1 && this.pages.length < this.maxPages && this.browser) {\n this.preWarmPage();\n }\n }\n },\n };\n }\n\n /** Mark the pool as warmed up, enabling page scaling. */\n markWarmedUp(): void {\n if (!this.warmedUp) {\n this.warmedUp = true;\n console.log(`[PagePool] Warm-up complete — page scaling enabled (max ${this.maxPages})`);\n }\n }\n\n private async createPage(): Promise<PooledPage> {\n if (!this.browser) {\n throw new Error(\"PagePool not initialized\");\n }\n\n console.log(`[PagePool] Creating new page (${this.pages.length + 1}/${this.maxPages})...`);\n\n const context = this.browser.contexts()[0];\n if (!context) {\n throw new Error(\"No browser context available\");\n }\n\n const page = await context.newPage();\n\n try {\n // Navigate to the target URL\n await page.goto(this.targetUrl, { waitUntil: \"domcontentloaded\", timeout: 30_000 });\n\n // Verify we actually landed on the target domain and weren't redirected\n // to a login/auth page (goto() follows redirects silently).\n const finalUrl = page.url();\n if (!finalUrl.includes(\"chat.ai.jh.edu\")) {\n throw new Error(\n `New page redirected away from target: ${finalUrl} — ` +\n \"browser session may have expired, restart without --headless to re-login.\"\n );\n }\n\n console.log(`[PagePool] New page ready: ${finalUrl}`);\n\n const pooled: PooledPage = {\n page,\n queue: new RequestQueue(this.maxWaitMs),\n inUse: false,\n };\n\n this.pages.push(pooled);\n return pooled;\n } catch (err) {\n // Always close the tab on failure to avoid leaving orphaned browser tabs\n // that the user would have to close manually.\n await page.close().catch(() => {});\n throw err;\n }\n }\n\n /** Evict pages that have crashed or navigated away from JH */\n private evictDeadPages(): void {\n const before = this.pages.length;\n this.pages = this.pages.filter((p) => {\n try {\n // page.isClosed() is synchronous and safe\n if (p.page.isClosed()) return false;\n // Check the page is still on JH domain\n const url = p.page.url();\n if (!url.includes(\"chat.ai.jh.edu\")) {\n console.warn(`[PagePool] Evicting page — navigated away: ${url}`);\n p.page.close().catch(() => {});\n return false;\n }\n return true;\n } catch {\n // page reference is dead\n return false;\n }\n });\n const evicted = before - this.pages.length;\n if (evicted > 0) {\n console.warn(`[PagePool] Evicted ${evicted} dead/stale page(s)`);\n }\n }\n\n /** Pre-warm a new page in the background (fire-and-forget) */\n private preWarmPage(): void {\n this.pagesCreating++;\n this.createPage()\n .then(() => console.log(\"[PagePool] Pre-warmed a new page\"))\n .catch((err) => console.warn(`[PagePool] Pre-warm failed: ${(err as Error).message}`))\n .finally(() => { this.pagesCreating--; });\n }\n\n /** Close all pages except the seed page */\n async drain(): Promise<void> {\n // Keep the first page (seed), close the rest\n const toClose = this.pages.slice(1);\n this.pages = this.pages.slice(0, 1);\n\n for (const p of toClose) {\n try {\n await p.page.close();\n } catch {\n // Page may already be closed\n }\n }\n\n console.log(`[PagePool] Drained ${toClose.length} pages`);\n }\n}\n","import type { GatewayCredentials } from \"../infra/types.js\";\nimport { captureCredentialsActive } from \"./auth-capture.js\";\nimport { updateConfig } from \"../infra/config.js\";\n\n/**\n * Thread-safe holder for the current gateway credentials.\n * After the first `set()`, `get()` will never return `null`.\n */\nexport class CredentialHolder {\n private creds: GatewayCredentials | null = null;\n\n /** Returns the current credentials, or `null` if none have been set yet. */\n get(): GatewayCredentials | null {\n return this.creds;\n }\n\n /** Atomically replaces the stored credentials. */\n set(creds: GatewayCredentials): void {\n this.creds = creds;\n }\n}\n\n/**\n * Returns true if the token should be refreshed.\n *\n * @param nowMs - Current time in milliseconds (e.g. Date.now())\n * @param expiresAt - JWT exp claim in unix seconds\n * @param thresholdMs - Refresh threshold in milliseconds\n */\nexport function shouldRefresh(\n nowMs: number,\n expiresAt: number,\n thresholdMs: number,\n): boolean {\n return expiresAt * 1000 - nowMs < thresholdMs;\n}\n\nexport interface TokenRefresherOptions {\n checkIntervalMs?: number; // default: 60_000\n refreshBeforeExpiryMs?: number; // default: 300_000 (5 min)\n maxRetries?: number; // default: 3\n}\n\nconst BACKOFF_DELAYS = [5_000, 15_000, 30_000];\n\n/**\n * Background service that monitors JWT expiry and proactively\n * re-captures credentials before they expire.\n */\nexport class TokenRefresher {\n private credentialHolder: CredentialHolder;\n private cdpUrl: string;\n private checkIntervalMs: number;\n private refreshBeforeExpiryMs: number;\n private maxRetries: number;\n private intervalId: ReturnType<typeof setInterval> | null = null;\n\n constructor(\n credentialHolder: CredentialHolder,\n cdpUrl: string,\n options?: TokenRefresherOptions,\n ) {\n this.credentialHolder = credentialHolder;\n this.cdpUrl = cdpUrl;\n this.checkIntervalMs = options?.checkIntervalMs ?? 60_000;\n this.refreshBeforeExpiryMs = options?.refreshBeforeExpiryMs ?? 300_000;\n this.maxRetries = options?.maxRetries ?? 3;\n }\n\n /** Start the background check interval. */\n start(): void {\n if (this.intervalId !== null) return;\n this.intervalId = setInterval(() => {\n void this.checkAndRefresh();\n }, this.checkIntervalMs);\n }\n\n /** Stop the background check interval. */\n stop(): void {\n if (this.intervalId !== null) {\n clearInterval(this.intervalId);\n this.intervalId = null;\n }\n }\n\n /** Check if a refresh is needed and perform it. Returns true if refreshed. */\n async checkAndRefresh(): Promise<boolean> {\n const creds = this.credentialHolder.get();\n if (!creds || !creds.expiresAt) {\n return false;\n }\n\n if (!shouldRefresh(Date.now(), creds.expiresAt, this.refreshBeforeExpiryMs)) {\n return false;\n }\n\n // Attempt refresh with retries and backoff\n for (let attempt = 0; attempt < this.maxRetries; attempt++) {\n try {\n const newCreds = await captureCredentialsActive(this.cdpUrl);\n const gatewayCreds: GatewayCredentials = {\n bearerToken: newCreds.bearerToken,\n cookie: newCreds.cookie,\n userAgent: newCreds.userAgent,\n expiresAt: newCreds.expiresAt,\n };\n\n this.credentialHolder.set(gatewayCreds);\n await updateConfig({ credentials: gatewayCreds });\n\n const expiryDate = new Date(newCreds.expiresAt * 1000).toISOString();\n console.log(`[TokenRefresher] Credentials refreshed successfully. New expiry: ${expiryDate}`);\n return true;\n } catch (err) {\n const delay = BACKOFF_DELAYS[attempt] ?? BACKOFF_DELAYS[BACKOFF_DELAYS.length - 1];\n if (attempt < this.maxRetries - 1) {\n console.warn(\n `[TokenRefresher] Refresh attempt ${attempt + 1}/${this.maxRetries} failed, retrying in ${delay / 1000}s...`,\n );\n await this.sleep(delay);\n } else {\n const msg = err instanceof Error ? err.message : String(err);\n console.warn(\n `\\n⚠️ [TokenRefresher] All ${this.maxRetries} refresh attempts failed. ${msg}\\n` +\n ` Continuing with current credentials. If requests start failing with 401,\\n` +\n ` restart with \\`jh-gateway start\\` (without --headless) to re-login.\\n`,\n );\n }\n }\n }\n\n return false;\n }\n\n private sleep(ms: number): Promise<void> {\n return new Promise((resolve) => setTimeout(resolve, ms));\n }\n}\n","import { existsSync } from \"node:fs\";\nimport { execSync, spawn } from \"node:child_process\";\nimport { homedir } from \"node:os\";\nimport { join } from \"node:path\";\nimport { chromium } from \"playwright-core\";\nimport type { Browser } from \"playwright-core\";\nimport type { ChildProcess } from \"node:child_process\";\nimport { getChromeWebSocketUrl } from \"./chrome-cdp.js\";\n\n// ── Interfaces ────────────────────────────────────────────────────────────────\n\nexport interface ChromeManagerOptions {\n /** CDP remote debugging port. Default: 9222 */\n cdpPort?: number;\n /** Launch Chrome in headless mode. Default: false */\n headless?: boolean;\n /** Chrome user data directory. Default: ~/.jh-gateway/chrome-profile */\n userDataDir?: string;\n}\n\nexport interface ChromeManagerState {\n browser: Browser;\n /** true if ChromeManager spawned the process */\n selfLaunched: boolean;\n /** Only set when selfLaunched is true */\n process?: ChildProcess;\n}\n\n// ── Chrome path constants ─────────────────────────────────────────────────────\n\nconst MACOS_CHROME_PATH =\n \"/Applications/Google Chrome.app/Contents/MacOS/Google Chrome\";\n\nconst WINDOWS_CHROME_PATHS = [\n \"C:\\\\Program Files\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\",\n \"C:\\\\Program Files (x86)\\\\Google\\\\Chrome\\\\Application\\\\chrome.exe\",\n];\n\nconst LINUX_CHROME_CANDIDATES = [\n \"google-chrome\",\n \"google-chrome-stable\",\n \"chromium-browser\",\n \"chromium\",\n];\n\n// ── ChromeManager ─────────────────────────────────────────────────────────────\n\nexport class ChromeManager {\n private readonly cdpPort: number;\n private readonly headless: boolean;\n private readonly userDataDir: string;\n\n constructor(options?: ChromeManagerOptions) {\n this.cdpPort = options?.cdpPort ?? 9222;\n this.headless = options?.headless ?? false;\n this.userDataDir =\n options?.userDataDir ?? join(homedir(), \".jh-gateway\", \"chrome-profile\");\n }\n\n /**\n * Try connecting to an existing Chrome instance at the configured CDP port.\n * If no instance is running, launch a new Chrome process and connect to it.\n */\n async connect(): Promise<ChromeManagerState> {\n const cdpUrl = `http://127.0.0.1:${this.cdpPort}`;\n\n // ── Phase 1: Try connecting to an existing Chrome instance ────────\n try {\n const wsUrl = await getChromeWebSocketUrl(cdpUrl);\n const browser = await chromium.connectOverCDP(wsUrl);\n return { browser, selfLaunched: false };\n } catch {\n // No existing Chrome — fall through to launch\n }\n\n // ── Phase 2: Find Chrome and launch it ───────────────────────────\n const chromePath = ChromeManager.findChromePath();\n if (!chromePath) {\n throw new Error(\n `Chrome executable not found. Please install Google Chrome or Chromium.\\n` +\n `Expected locations for ${process.platform}:\\n` +\n (process.platform === \"darwin\"\n ? ` - ${MACOS_CHROME_PATH}\\n`\n : process.platform === \"linux\"\n ? LINUX_CHROME_CANDIDATES.map((c) => ` - ${c} (via PATH)`).join(\"\\n\") + \"\\n\"\n : WINDOWS_CHROME_PATHS.map((p) => ` - ${p}`).join(\"\\n\") + \"\\n\"),\n );\n }\n\n const args = [\n `--remote-debugging-port=${this.cdpPort}`,\n `--user-data-dir=${this.userDataDir}`,\n \"--no-first-run\",\n \"--no-default-browser-check\",\n ];\n if (this.headless) {\n args.push(\"--headless=new\");\n }\n\n const child = spawn(chromePath, args, {\n detached: false,\n stdio: \"ignore\",\n });\n\n // ── Phase 3: Wait for CDP to become available ─────────────────────\n await this.waitForCdp(cdpUrl);\n\n // ── Phase 4: Connect via Playwright ───────────────────────────────\n const wsUrl = await getChromeWebSocketUrl(cdpUrl);\n const browser = await chromium.connectOverCDP(wsUrl);\n\n return { browser, selfLaunched: true, process: child };\n }\n\n /**\n * Poll the CDP /json/version endpoint until it responds, or timeout.\n */\n private async waitForCdp(\n cdpUrl: string,\n timeoutMs: number = 15_000,\n intervalMs: number = 250,\n ): Promise<void> {\n const deadline = Date.now() + timeoutMs;\n\n while (Date.now() < deadline) {\n try {\n const res = await fetch(`${cdpUrl}/json/version`);\n if (res.ok) return;\n } catch {\n // Not ready yet\n }\n await new Promise((r) => setTimeout(r, intervalMs));\n }\n\n throw new Error(\n `Chrome did not become available at ${cdpUrl} within ${timeoutMs / 1000}s`,\n );\n }\n\n /**\n * Terminate managed Chrome (only if selfLaunched).\n * Tries to close the browser connection gracefully first, then kills the process.\n */\n async shutdown(state: ChromeManagerState): Promise<void> {\n if (!state.selfLaunched || !state.process) {\n return;\n }\n\n try {\n await state.browser.close();\n } catch {\n // Browser may already be disconnected — ignore\n }\n\n try {\n state.process.kill(\"SIGTERM\");\n } catch {\n // Process may already be dead — ignore\n }\n }\n\n /**\n * Gracefully detach from Chrome without killing it.\n * Hides the window, then disconnects the Playwright CDP session.\n * Chrome keeps running in the background so the next gateway start\n * can reconnect to the same instance (preserving session/cookies).\n */\n async disconnect(state: ChromeManagerState): Promise<void> {\n await this.hideWindow(state);\n // Do NOT call browser.close() — Playwright sends Browser.close over CDP\n // which kills the Chrome process. We just drop the connection and let\n // Chrome keep running in the background.\n }\n\n /**\n * Attempt to relaunch and reconnect to Chrome within 30s.\n * Returns a fresh ChromeManagerState.\n */\n async reconnect(): Promise<ChromeManagerState> {\n const RECONNECT_TIMEOUT_MS = 30_000;\n const deadline = Date.now() + RECONNECT_TIMEOUT_MS;\n\n while (Date.now() < deadline) {\n try {\n return await this.connect();\n } catch {\n await new Promise((r) => setTimeout(r, 1_000));\n }\n }\n\n throw new Error(\n `Failed to reconnect to Chrome within ${RECONNECT_TIMEOUT_MS / 1000}s`,\n );\n }\n\n /**\n * Show / unhide the Chrome window.\n * On macOS: uses osascript to set the process visible again.\n * On other platforms: restores the window via CDP.\n * Best-effort — errors are silently caught.\n */\n async showWindow(state: ChromeManagerState): Promise<void> {\n if (process.platform === \"darwin\") {\n try {\n const pid = state.process?.pid;\n if (pid !== undefined) {\n execSync(\n `osascript -e 'tell application \"System Events\" to set visible of (first process whose unix id is ${pid}) to true'`,\n { stdio: \"pipe\" },\n );\n } else {\n execSync(\n `osascript -e 'tell application \"System Events\" to set visible of process \"Google Chrome\" to true'`,\n { stdio: \"pipe\" },\n );\n }\n return;\n } catch {\n // Fall back to CDP restore\n }\n }\n try {\n const page = state.browser.contexts()[0]?.pages()[0];\n if (!page) return;\n\n const cdpSession = await page.context().newCDPSession(page);\n const { windowId } = (await cdpSession.send(\n \"Browser.getWindowForTarget\",\n )) as { windowId: number };\n await cdpSession.send(\"Browser.setWindowBounds\", {\n windowId,\n bounds: { windowState: \"normal\" },\n });\n } catch {\n // Best-effort — silently ignore errors\n }\n }\n\n /**\n * Hide the Chrome window.\n * On macOS: uses osascript to fully hide the window (equivalent to Cmd+H),\n * so it does not appear in the Dock as a minimized tile.\n * On other platforms: falls back to CDP minimize.\n * Best-effort — errors are silently caught.\n */\n async hideWindow(state: ChromeManagerState): Promise<void> {\n if (process.platform === \"darwin\") {\n try {\n const pid = state.process?.pid;\n if (pid !== undefined) {\n execSync(\n `osascript -e 'tell application \"System Events\" to set visible of (first process whose unix id is ${pid}) to false'`,\n { stdio: \"pipe\" },\n );\n } else {\n execSync(\n `osascript -e 'tell application \"System Events\" to set visible of process \"Google Chrome\" to false'`,\n { stdio: \"pipe\" },\n );\n }\n return;\n } catch {\n // Fall back to CDP minimize\n }\n }\n try {\n const page = state.browser.contexts()[0]?.pages()[0];\n if (!page) return;\n\n const cdpSession = await page.context().newCDPSession(page);\n const { windowId } = (await cdpSession.send(\n \"Browser.getWindowForTarget\",\n )) as { windowId: number };\n await cdpSession.send(\"Browser.setWindowBounds\", {\n windowId,\n bounds: { windowState: \"minimized\" },\n });\n } catch {\n // Best-effort — silently ignore errors\n }\n }\n\n /**\n * Detect the Chrome/Chromium executable path for the current OS.\n * Returns the absolute path string, or `null` if no installation is found.\n */\n static findChromePath(): string | null {\n const platform = process.platform;\n\n if (platform === \"darwin\") {\n return existsSync(MACOS_CHROME_PATH) ? MACOS_CHROME_PATH : null;\n }\n\n if (platform === \"linux\") {\n for (const candidate of LINUX_CHROME_CANDIDATES) {\n try {\n const resolved = execSync(`which ${candidate}`, {\n encoding: \"utf8\",\n stdio: [\"pipe\", \"pipe\", \"pipe\"],\n }).trim();\n if (resolved) {\n return resolved;\n }\n } catch {\n // candidate not found, try next\n }\n }\n return null;\n }\n\n if (platform === \"win32\") {\n for (const winPath of WINDOWS_CHROME_PATHS) {\n if (existsSync(winPath)) {\n return winPath;\n }\n }\n return null;\n }\n\n // Unsupported platform\n return null;\n }\n}\n"],"mappings":"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","names":["context","page","Hono","Hono","Hono","Hono","randomBytes","randomBytes","randomBytes","MODEL_SET","Hono","readFile","mkdir","homedir","join","Hono","p","homedir","join","chromium","join","homedir","wsUrl","browser","chromium"]}
package/dist/cli.js CHANGED
@@ -17,7 +17,7 @@ import {
17
17
  shouldRefresh,
18
18
  startServer,
19
19
  updateConfig
20
- } from "./chunk-TNKXXCTQ.js";
20
+ } from "./chunk-Y2NMKJOG.js";
21
21
 
22
22
  // src/cli/setup.ts
23
23
  import * as p from "@clack/prompts";
@@ -187,7 +187,7 @@ async function runServe(options) {
187
187
  await updateConfig({ port: options.port });
188
188
  config.port = options.port;
189
189
  }
190
- const maxPages = options.pages ?? 3;
190
+ const maxPages = options.pages ?? 1;
191
191
  let pool = null;
192
192
  let browser;
193
193
  try {
@@ -369,7 +369,7 @@ async function runStart(options) {
369
369
  await updateConfig({ port: options.port });
370
370
  config.port = options.port;
371
371
  }
372
- const maxPages = options.pages ?? 3;
372
+ const maxPages = options.pages ?? 1;
373
373
  const cdpPort = parseInt(new URL(config.cdpUrl).port, 10) || 9222;
374
374
  const chromeManager = new ChromeManager({
375
375
  cdpPort,
@@ -519,9 +519,9 @@ Commands:
519
519
  Options:
520
520
  start --headless Launch Chrome in headless mode
521
521
  start --port <n> Override the configured port
522
- start --pages <n> Max concurrent browser pages (default: 3)
522
+ start --pages <n> Max concurrent browser pages (default: 1)
523
523
  serve --port <n> Override the configured port
524
- serve --pages <n> Max concurrent browser pages (default: 3)
524
+ serve --pages <n> Max concurrent browser pages (default: 1)
525
525
  logs --limit <n> Number of log entries to show (default: 50)
526
526
  --version, -v Print version and exit
527
527
  --help Show this help message
@@ -529,7 +529,7 @@ Options:
529
529
  }
530
530
  async function main() {
531
531
  if (command === "--version" || command === "-v") {
532
- console.log("2.1.0");
532
+ console.log("2.2.0");
533
533
  process.exit(0);
534
534
  }
535
535
  if (command === "--help" || command === "-h") {
@@ -537,7 +537,7 @@ async function main() {
537
537
  process.exit(0);
538
538
  }
539
539
  if (!command || command === "tui") {
540
- const { launchTui } = await import("./tui-YGSNFLO7.js");
540
+ const { launchTui } = await import("./tui-GRDJWXQL.js");
541
541
  await launchTui();
542
542
  return;
543
543
  }