diff --git a/src/browser/cdp.ts b/src/browser/cdp.ts index b3d2f2046..fce55048b 100644 --- a/src/browser/cdp.ts +++ b/src/browser/cdp.ts @@ -1,4 +1,33 @@ -import { fetchJson, isLoopbackHost, withCdpSocket } from "./cdp.helpers.js"; +import { isLoopbackHost, withCdpSocket } from "./cdp.helpers.js"; + +export function getHeadersWithAuth( + url: string, + headers: Record = {}, +) { + try { + const parsed = new URL(url); + if (parsed.username || parsed.password) { + const auth = Buffer.from( + `${parsed.username}:${parsed.password}`, + ).toString("base64"); + return { ...headers, Authorization: `Basic ${auth}` }; + } + } catch (_e) { + // ignore + } + return headers; +} + +export async function fetchJson(url: string): Promise { + const res = await fetch(url, { headers: getHeadersWithAuth(url) }); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); + return (await res.json()) as Promise; +} + +export async function fetchOk(url: string): Promise { + const res = await fetch(url, { headers: getHeadersWithAuth(url) }); + if (!res.ok) throw new Error(`${res.status} ${res.statusText}`); +} export function normalizeCdpWsUrl(wsUrl: string, cdpUrl: string): string { const ws = new URL(wsUrl); @@ -69,7 +98,9 @@ export async function createTargetViaCdp(opts: { url: string; }): Promise<{ targetId: string }> { const base = opts.cdpUrl.replace(/\/$/, ""); - const version = await fetchJson<{ webSocketDebuggerUrl?: string }>(`${base}/json/version`, 1500); + const version = await fetchJson<{ webSocketDebuggerUrl?: string }>( + `${base}/json/version`, + ); const wsUrlRaw = String(version?.webSocketDebuggerUrl ?? "").trim(); const wsUrl = wsUrlRaw ? normalizeCdpWsUrl(wsUrlRaw, opts.cdpUrl) : ""; if (!wsUrl) throw new Error("CDP /json/version missing webSocketDebuggerUrl"); diff --git a/src/browser/chrome.ts b/src/browser/chrome.ts index 6667d18c9..b7c6a7ec3 100644 --- a/src/browser/chrome.ts +++ b/src/browser/chrome.ts @@ -7,7 +7,7 @@ import WebSocket from "ws"; import { ensurePortAvailable } from "../infra/ports.js"; import { createSubsystemLogger } from "../logging.js"; import { CONFIG_DIR } from "../utils.js"; -import { normalizeCdpWsUrl } from "./cdp.js"; +import { getHeadersWithAuth, normalizeCdpWsUrl } from "./cdp.js"; import { type BrowserExecutable, resolveBrowserExecutableForPlatform, @@ -74,6 +74,7 @@ async function fetchChromeVersion(cdpUrl: string, timeoutMs = 500): Promise { + const { browser } = await connectBrowser(cdpUrl); + return browser; +} + async function connectBrowser(cdpUrl: string): Promise { const normalized = normalizeCdpUrl(cdpUrl); if (cached?.cdpUrl === normalized) return cached; diff --git a/src/browser/server-context.ts b/src/browser/server-context.ts index 5ed460d10..314b89b22 100644 --- a/src/browser/server-context.ts +++ b/src/browser/server-context.ts @@ -1,6 +1,10 @@ import fs from "node:fs"; -import { createTargetViaCdp, normalizeCdpWsUrl } from "./cdp.js"; +import { + createTargetViaCdp, + getHeadersWithAuth, + normalizeCdpWsUrl, +} from "./cdp.js"; import { isChromeCdpReady, isChromeReachable, @@ -10,6 +14,7 @@ import { } from "./chrome.js"; import type { ResolvedBrowserProfile } from "./config.js"; import { resolveProfile } from "./config.js"; +import { getConnectedBrowser } from "./pw-session.js"; import type { BrowserRouteContext, BrowserTab, @@ -50,7 +55,11 @@ async function fetchJson(url: string, timeoutMs = 1500, init?: RequestInit): const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), timeoutMs); try { - const res = await fetch(url, { ...init, signal: ctrl.signal }); + const headers = getHeadersWithAuth( + url, + (init?.headers as Record) || {}, + ); + const res = await fetch(url, { ...init, headers, signal: ctrl.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); return (await res.json()) as T; } finally { @@ -62,7 +71,11 @@ async function fetchOk(url: string, timeoutMs = 1500, init?: RequestInit): Promi const ctrl = new AbortController(); const t = setTimeout(() => ctrl.abort(), timeoutMs); try { - const res = await fetch(url, { ...init, signal: ctrl.signal }); + const headers = getHeadersWithAuth( + url, + (init?.headers as Record) || {}, + ); + const res = await fetch(url, { ...init, headers, signal: ctrl.signal }); if (!res.ok) throw new Error(`HTTP ${res.status}`); } finally { clearTimeout(t); @@ -97,7 +110,40 @@ function createProfileContext( profileState.running = running; }; + const listTabsViaPlaywright = async (): Promise => { + const browser = await getConnectedBrowser(profile.cdpUrl); + const contexts = browser.contexts(); + const pages = contexts.flatMap((c) => c.pages()); + // Note: Playwright pages don't sync title instantly, returning URL is safer for list + return pages.map((p) => ({ + targetId: p.url(), + title: "", + url: p.url(), + type: "page", + })); + }; + + const openTabViaPlaywright = async (url?: string): Promise => { + const browser = await getConnectedBrowser(profile.cdpUrl); + // Reuse context or create new + const context = browser.contexts()[0] || (await browser.newContext()); + const page = await context.newPage(); + if (url) await page.goto(url); + return { targetId: page.url(), title: "", url: page.url(), type: "page" }; + }; + + const closeTabViaPlaywright = async (targetId: string) => { + const browser = await getConnectedBrowser(profile.cdpUrl); + const pages = browser.contexts().flatMap((c) => c.pages()); + // Find page by URL match (simple strategy) + const page = pages.find((p) => p.url() === targetId); + if (page) await page.close(); + }; + const listTabs = async (): Promise => { + if (!profile.cdpIsLoopback) { + return await listTabsViaPlaywright(); + } const raw = await fetchJson< Array<{ id?: string; @@ -119,6 +165,9 @@ function createProfileContext( }; const openTab = async (url: string): Promise => { + if (!profile.cdpIsLoopback) { + return await openTabViaPlaywright(url); + } const createdViaCdp = await createTargetViaCdp({ cdpUrl: profile.cdpUrl, url, @@ -342,6 +391,9 @@ function createProfileContext( }; const closeTab = async (targetId: string): Promise => { + if (!profile.cdpIsLoopback) { + return await closeTabViaPlaywright(targetId); + } const base = profile.cdpUrl.replace(/\/$/, ""); const tabs = await listTabs(); const resolved = resolveTargetIdFromTabs(targetId, tabs);