UNPKG

openclaw

Version:

Multi-channel AI gateway with extensible messaging integrations

711 lines (710 loc) 28.8 kB
import { a as addTimerTimeoutGraceMs, f as clampTimerTimeoutMs, j as resolveTimerTimeoutMs } from "./number-coercion-CJQ8TR--.js"; import { _ as uniqueStrings } from "./string-normalization-WNUDCpXX.js"; import { i as isLoopbackHost } from "./net-DTe7AQiu.js"; import { i as hasProxyEnvConfigured } from "./proxy-env-B9aW4MXJ.js"; import { n as registerManagedProxyBrowserCdpBypass } from "./proxy-lifecycle-Dv0tHumt.js"; import { _ as resolvePinnedHostnameWithPolicy, t as SsrFBlockedError } from "./ssrf-CvPEXMGn.js"; import { r as fetchWithSsrFGuard } from "./fetch-guard-BttkNCLm.js"; import "./number-runtime-DBLVDypr.js"; import "./string-coerce-runtime-CEGJWkQ_.js"; import "./ssrf-runtime-BOGN5pUi.js"; import "./browser-config-CB1dJM4Y.js"; import "./sdk-security-runtime-D1sDHmUD.js"; import "./ssrf-runtime-internal-BMEiAk0I.js"; import WebSocket from "ws"; import http from "node:http"; import https from "node:https"; //#region extensions/browser/src/browser/cdp-proxy-bypass.ts /** * Proxy bypass for CDP (Chrome DevTools Protocol) localhost connections. * * When HTTP_PROXY / HTTPS_PROXY / ALL_PROXY environment variables are set, * CDP connections to localhost/127.0.0.1 can be incorrectly routed through * the proxy, causing browser control to fail. * * @see https://github.com/nicepkg/openclaw/issues/31219 */ /** HTTP agent that never uses a proxy — for localhost CDP connections. */ const directHttpAgent = new http.Agent(); const directHttpsAgent = new https.Agent(); /** * Returns a plain (non-proxy) agent for WebSocket or HTTP connections * when the target is a loopback address. Returns `undefined` otherwise * so callers fall through to their default behaviour. */ function getDirectAgentForCdp(url) { try { const parsed = new URL(url); if (isLoopbackHost(parsed.hostname)) return parsed.protocol === "https:" || parsed.protocol === "wss:" ? directHttpsAgent : directHttpAgent; } catch {} } /** * Returns `true` when any proxy-related env var is set that could * interfere with loopback connections. */ function hasProxyEnv() { return hasProxyEnvConfigured(); } const LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]"; function noProxyValueCoversLocalhost(value) { const entries = new Set((value ?? "").split(",").map((entry) => entry.trim().toLowerCase()).filter(Boolean)); return entries.has("localhost") && entries.has("127.0.0.1") && entries.has("[::1]"); } function noProxyAlreadyCoversLocalhost() { return noProxyValueCoversLocalhost(process.env.NO_PROXY) && noProxyValueCoversLocalhost(process.env.no_proxy); } function appendLoopbackEntries(value) { return value ? `${value},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES; } function isLoopbackCdpUrl(url) { try { return isLoopbackHost(new URL(url).hostname); } catch { return false; } } var NoProxyLeaseManager = class { constructor() { this.leaseCount = 0; this.snapshot = null; } acquire(url) { if (!isLoopbackCdpUrl(url) || !hasProxyEnv()) return null; if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) { const noProxy = process.env.NO_PROXY; const noProxyLower = process.env.no_proxy; const appliedNoProxy = appendLoopbackEntries(noProxy || noProxyLower); const appliedNoProxyLower = appendLoopbackEntries(noProxyLower || noProxy); process.env.NO_PROXY = appliedNoProxy; process.env.no_proxy = appliedNoProxyLower; this.snapshot = { noProxy, noProxyLower, appliedNoProxy, appliedNoProxyLower }; } this.leaseCount += 1; let released = false; return () => { if (released) return; released = true; this.release(); }; } release() { if (this.leaseCount <= 0) return; this.leaseCount -= 1; if (this.leaseCount > 0 || !this.snapshot) return; const { noProxy, noProxyLower, appliedNoProxy, appliedNoProxyLower } = this.snapshot; const currentNoProxy = process.env.NO_PROXY; const currentNoProxyLower = process.env.no_proxy; if (currentNoProxy === appliedNoProxy) if (noProxy !== void 0) process.env.NO_PROXY = noProxy; else delete process.env.NO_PROXY; if (currentNoProxyLower === appliedNoProxyLower) if (noProxyLower !== void 0) process.env.no_proxy = noProxyLower; else delete process.env.no_proxy; this.snapshot = null; } }; const noProxyLeaseManager = new NoProxyLeaseManager(); /** * Scoped NO_PROXY bypass for loopback CDP URLs. * * This wrapper only mutates env vars for loopback destinations. On restore, * it avoids clobbering external NO_PROXY changes that happened while calls * were in-flight. */ async function withNoProxyForCdpUrl(url, fn) { const release = noProxyLeaseManager.acquire(url); try { return await fn(); } finally { release?.(); } } /** * Scoped managed-proxy bypass for the exact CDP URL about to be used. * * Proxyline dynamic bypass registrations are exact URL matches, so callers * must register the concrete `/json/version` or `ws://.../devtools/...` URL * rather than a CDP base URL. */ function withManagedProxyForCdpUrl(url, fn) { const release = registerManagedProxyBrowserCdpBypass(url); let result; try { result = fn(); } catch (err) { release?.(); throw err; } const maybeThenable = result; if (typeof maybeThenable === "object" && maybeThenable !== null && "finally" in maybeThenable && typeof maybeThenable.finally === "function") return maybeThenable.finally(() => release?.()); release?.(); return result; } /** * Validate managed-proxy loopback policy without keeping a long-lived bypass. * Exact CDP request sites install their own scoped bypasses. */ function assertManagedProxyAllowsCdpUrl(url) { withManagedProxyForCdpUrl(url, () => void 0); } //#endregion //#region extensions/browser/src/browser/constants.ts /** * Browser default configuration constants. * * Shared defaults for config resolution, tool schemas, managed Chrome launch, * tab cleanup, screenshots, and AI snapshot sizing. */ /** Default enabled state for the browser plugin. */ const DEFAULT_OPENCLAW_BROWSER_ENABLED = true; /** Default JavaScript evaluation permission for managed browser actions. */ const DEFAULT_BROWSER_EVALUATE_ENABLED = true; /** Default color for the managed OpenClaw browser profile. */ const DEFAULT_OPENCLAW_BROWSER_COLOR = "#FF4500"; /** Default managed profile name shown to users. */ const DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME = "openclaw"; /** Default browser profile selected when no profile is requested. */ const DEFAULT_BROWSER_DEFAULT_PROFILE_NAME = "openclaw"; /** Default timeout for browser action execution. */ const DEFAULT_BROWSER_ACTION_TIMEOUT_MS = 6e4; /** Default launch readiness window for managed local Chrome. */ const DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS = 15e3; /** Default CDP readiness window after managed Chrome launch. */ const DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS = 8e3; /** Default timeout for screenshot capture. */ const DEFAULT_BROWSER_SCREENSHOT_TIMEOUT_MS = 2e4; /** Default timeout for snapshot capture. */ const DEFAULT_BROWSER_SNAPSHOT_TIMEOUT_MS = 2e4; /** Default maximum AI snapshot text size. */ const DEFAULT_AI_SNAPSHOT_MAX_CHARS = 4e4; /** Default maximum AI snapshot text size in efficient mode. */ const DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS = 8e3; //#endregion //#region extensions/browser/src/browser/cdp-timeouts.ts /** * CDP and Chrome launch timeout constants. * * Centralizes timing so local loopback probes stay fast while remote/browser * node probes retain enough handshake slack for real networks. */ const CDP_HTTP_REQUEST_TIMEOUT_MS = 1500; const CDP_WS_HANDSHAKE_TIMEOUT_MS = 5e3; const CDP_JSON_NEW_TIMEOUT_MS = 1500; const CHROME_BOOTSTRAP_PREFS_TIMEOUT_MS = 1e4; const CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS = 5e3; const CHROME_LAUNCH_READY_WINDOW_MS = DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS; const CHROME_STOP_TIMEOUT_MS = 2500; const CHROME_STDERR_HINT_MAX_CHARS = 2e3; const PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS = 2e3; const PROFILE_ATTACH_RETRY_TIMEOUT_MS = 1200; const CHROME_MCP_ATTACH_READY_WINDOW_MS = 8e3; /** Return true when a profile can use the short loopback CDP probe class. */ function usesFastLoopbackCdpProbeClass(params) { return params.profileIsLoopback && params.attachOnly !== true; } function normalizeTimeoutMs(value) { return clampTimerTimeoutMs(value); } function maxTimerTimeoutMs(...values) { return values.reduce((max, value) => Math.max(max, resolveTimerTimeoutMs(value, 1)), 1); } /** Resolve HTTP and WebSocket reachability timeouts for a CDP profile. */ function resolveCdpReachabilityTimeouts(params) { const normalized = normalizeTimeoutMs(params.timeoutMs); const remoteHttpTimeoutMs = resolveTimerTimeoutMs(params.remoteHttpTimeoutMs, CDP_HTTP_REQUEST_TIMEOUT_MS); const remoteHandshakeTimeoutMs = resolveTimerTimeoutMs(params.remoteHandshakeTimeoutMs, CDP_WS_HANDSHAKE_TIMEOUT_MS); if (usesFastLoopbackCdpProbeClass({ profileIsLoopback: params.profileIsLoopback, attachOnly: params.attachOnly })) { const httpTimeoutMs = normalized ?? 300; return { httpTimeoutMs, wsTimeoutMs: Math.max(200, Math.min(PROFILE_WS_REACHABILITY_MAX_TIMEOUT_MS, httpTimeoutMs * 2)) }; } if (normalized !== void 0) { const requestedWsTimeoutMs = addTimerTimeoutGraceMs(normalized, normalized) ?? normalized; return { httpTimeoutMs: maxTimerTimeoutMs(normalized, remoteHttpTimeoutMs), wsTimeoutMs: maxTimerTimeoutMs(requestedWsTimeoutMs, remoteHandshakeTimeoutMs) }; } return { httpTimeoutMs: remoteHttpTimeoutMs, wsTimeoutMs: remoteHandshakeTimeoutMs }; } //#endregion //#region extensions/browser/src/browser/errors.ts /** * Browser domain errors. * * Provides HTTP-mappable error classes and stable blocked-policy messages used * by route handlers, clients, and Gateway proxy code. */ /** Stable message for blocked CDP endpoint configuration. */ const BROWSER_ENDPOINT_BLOCKED_MESSAGE = "browser endpoint blocked by policy"; /** Stable message for blocked page navigation targets. */ const BROWSER_NAVIGATION_BLOCKED_MESSAGE = "browser navigation blocked by policy"; /** Base browser error carrying an HTTP status code. */ var BrowserError = class extends Error { constructor(message, status = 500, options) { super(message, options); this.name = new.target.name; this.status = status; } }; /** * Raised when a browser CDP endpoint (the cdpUrl itself) fails the * configured SSRF policy. Distinct from a blocked navigation target so * callers see "fix your browser endpoint config" rather than "fix your * navigation URL". */ var BrowserCdpEndpointBlockedError = class extends BrowserError { constructor(options) { super(BROWSER_ENDPOINT_BLOCKED_MESSAGE, 400, options); } }; /** Validation failure for browser route or config input. */ var BrowserValidationError = class extends BrowserError { constructor(message, options) { super(message, 400, options); } }; /** Raised when a target id prefix matches multiple tabs. */ var BrowserTargetAmbiguousError = class extends BrowserError { constructor(message = "ambiguous target id prefix", options) { super(message, 409, options); } }; /** Raised when a requested browser tab cannot be resolved. */ var BrowserTabNotFoundError = class extends BrowserError { constructor(inputOrMessage, options) { const input = typeof inputOrMessage === "object" ? inputOrMessage.input?.trim() : inputOrMessage?.trim(); const message = input ? /^\d+$/.test(input) ? `tab not found: browser tab "${input}" not found. Numeric values are not tab targets; use a stable tab id like "t1", a label, or a raw targetId. For positional selection, use "openclaw browser tab select ${input}".` : `tab not found: browser tab "${input}" not found. Use action=tabs and pass suggestedTargetId, tabId, label, or raw targetId.` : "tab not found"; super(message, 404, options); } }; /** Raised when a requested browser profile does not exist. */ var BrowserProfileNotFoundError = class extends BrowserError { constructor(message, options) { super(message, 404, options); } }; /** Raised when a browser config mutation conflicts with existing state. */ var BrowserConflictError = class extends BrowserError { constructor(message, options) { super(message, 409, options); } }; /** Raised when a browser profile cannot be reset by the current driver. */ var BrowserResetUnsupportedError = class extends BrowserError { constructor(message, options) { super(message, 400, options); } }; /** Raised when a profile is configured but not currently reachable. */ var BrowserProfileUnavailableError = class extends BrowserError { constructor(message, options) { super(message, 409, options); } }; /** Raised when browser resource allocation, such as CDP ports, is exhausted. */ var BrowserResourceExhaustedError = class extends BrowserError { constructor(message, options) { super(message, 507, options); } }; /** Map browser-domain errors to HTTP response details. */ function toBrowserErrorResponse(err) { if (err instanceof BrowserError) return { status: err.status, message: err.message }; if (err instanceof Error && err.name === "BlockedBrowserTargetError") return { status: 409, message: err.message }; if (err instanceof Error && err.name === "SsrFBlockedError") return { status: 400, message: BROWSER_NAVIGATION_BLOCKED_MESSAGE }; if (err instanceof Error && err.name === "InvalidBrowserNavigationUrlError") return { status: 400, message: err.message }; return null; } //#endregion //#region extensions/browser/src/browser/rate-limit-message.ts /** * Rate-limit message selection for Browser service providers. */ const BROWSER_SERVICE_RATE_LIMIT_MESSAGE = "Browser service rate limit reached. Wait for the current session to complete, or retry later."; const BROWSERBASE_RATE_LIMIT_MESSAGE = "Browserbase rate limit reached (max concurrent sessions). Wait for the current session to complete, or upgrade your plan."; function isAbsoluteHttp(url) { return /^https?:\/\//i.test(url.trim()); } function isBrowserbaseUrl(url) { if (!isAbsoluteHttp(url)) return false; try { const host = new URL(url).hostname.trim().toLowerCase(); return host === "browserbase.com" || host.endsWith(".browserbase.com"); } catch { return false; } } /** Returns the provider-specific rate-limit message for a browser service URL. */ function resolveBrowserRateLimitMessage(url) { return isBrowserbaseUrl(url) ? BROWSERBASE_RATE_LIMIT_MESSAGE : BROWSER_SERVICE_RATE_LIMIT_MESSAGE; } //#endregion //#region extensions/browser/src/browser/ssrf-policy-helpers.ts /** * SSRF policy helpers for Browser routes that need one-off hostname grants. */ /** Returns an SSRF policy with the hostname added to allowedHostnames. */ function withAllowedHostname(ssrfPolicy, hostname) { return { ...ssrfPolicy, allowedHostnames: uniqueStrings([...ssrfPolicy?.allowedHostnames ?? [], hostname]) }; } //#endregion //#region extensions/browser/src/browser/timer-delay.ts /** * Timer delay normalization for Browser waits and cleanup loops. */ /** Largest timeout delay accepted reliably by Node timers. */ const MAX_SAFE_TIMEOUT_DELAY_MS = 2147483647; /** Clamps timer delays to Node's safe range with an optional lower bound. */ function normalizeBrowserTimerDelayMs(timeoutMs, opts) { const rawMinMs = opts?.minMs ?? 1; const minMs = Math.min(MAX_SAFE_TIMEOUT_DELAY_MS, Math.max(0, Number.isFinite(rawMinMs) ? Math.floor(rawMinMs) : 1)); return Math.min(MAX_SAFE_TIMEOUT_DELAY_MS, Math.max(minMs, Number.isFinite(timeoutMs) ? Math.floor(timeoutMs) : minMs)); } //#endregion //#region extensions/browser/src/browser/cdp.helpers.ts /** * Chrome DevTools Protocol URL, fetch, and socket helpers. * * Handles CDP URL normalization, SSRF-guarded HTTP discovery, credential * redaction/headers, and request/response correlation over WebSocket. */ /** * Returns true when the URL uses a WebSocket protocol (ws: or wss:). * Used to distinguish direct-WebSocket CDP endpoints * from HTTP(S) endpoints that require /json/version discovery. */ function isWebSocketUrl(url) { try { const parsed = new URL(url); return parsed.protocol === "ws:" || parsed.protocol === "wss:"; } catch { return false; } } /** * Returns true when `url` is a ws/wss URL with a `/devtools/<kind>/<id>` * path segment — i.e. a handshake-ready per-browser or per-target CDP * endpoint that can be opened directly without HTTP discovery. * * Bare ws roots (`ws://host:port`, `ws://host:port/`) and any other * non-`/devtools/...` paths are NOT direct endpoints: Chrome's debug * port only accepts WebSocket upgrades on the specific path returned * by `GET /json/version`. Callers with a bare ws root must normalise * it to http for discovery instead of attempting a root handshake that * Chrome will reject with HTTP 400. */ function isDirectCdpWebSocketEndpoint(url) { if (!isWebSocketUrl(url)) return false; try { const parsed = new URL(url); return /\/devtools\/(?:browser|page|worker|shared_worker|service_worker)\/[^/]/i.test(parsed.pathname); } catch { return false; } /* c8 ignore stop */ } async function assertCdpEndpointAllowed(cdpUrl, ssrfPolicy) { if (!ssrfPolicy) return; const parsed = new URL(cdpUrl); if (![ "http:", "https:", "ws:", "wss:" ].includes(parsed.protocol)) throw new Error(`Invalid CDP URL protocol: ${parsed.protocol.replace(":", "")}`); try { const policy = isLoopbackHost(parsed.hostname) ? withAllowedHostname(ssrfPolicy, parsed.hostname) : ssrfPolicy; await resolvePinnedHostnameWithPolicy(parsed.hostname, { policy }); } catch (error) { throw new BrowserCdpEndpointBlockedError({ cause: error }); } } function rawCdpMessageToString(data) { if (typeof data === "string") return data; if (Buffer.isBuffer(data)) return data.toString("utf8"); if (Array.isArray(data)) return Buffer.concat(data).toString("utf8"); if (ArrayBuffer.isView(data)) return Buffer.from(data.buffer, data.byteOffset, data.byteLength).toString("utf8"); return Buffer.from(data).toString("utf8"); } /** Merge URL basic-auth credentials into headers without overriding explicit auth. */ function getHeadersWithAuth(url, headers = {}) { const mergedHeaders = { ...headers }; try { const parsed = new URL(url); if (Object.keys(mergedHeaders).some((key) => key.trim().toLowerCase() === "authorization")) return mergedHeaders; if (parsed.username || parsed.password) { const auth = Buffer.from(`${parsed.username}:${parsed.password}`).toString("base64"); return { ...mergedHeaders, Authorization: `Basic ${auth}` }; } } catch {} return mergedHeaders; } function stripUrlCredentials(url) { try { const parsed = new URL(url); if (!parsed.username && !parsed.password) return url; parsed.username = ""; parsed.password = ""; return parsed.toString(); } catch { return url; } } /** Append a JSON endpoint path to a CDP HTTP base URL. */ function appendCdpPath(cdpUrl, path) { const url = new URL(cdpUrl); url.pathname = `${url.pathname.replace(/\/$/, "")}${path.startsWith("/") ? path : `/${path}`}`; return url.toString(); } /** Normalize ws/wss and direct devtools URLs back to the HTTP JSON endpoint base. */ function normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) { try { const url = new URL(cdpUrl); if (url.protocol === "ws:") url.protocol = "http:"; else if (url.protocol === "wss:") url.protocol = "https:"; url.pathname = url.pathname.replace(/\/devtools\/browser\/.*$/, ""); url.pathname = url.pathname.replace(/\/cdp$/, ""); return url.toString().replace(/\/$/, ""); } catch { return cdpUrl.replace(/^ws:/, "http:").replace(/^wss:/, "https:").replace(/\/devtools\/browser\/.*$/, "").replace(/\/cdp$/, "").replace(/\/$/, ""); } } function createCdpSender(ws, opts) { let nextId = 1; const pending = /* @__PURE__ */ new Map(); const commandTimeoutMs = typeof opts?.commandTimeoutMs === "number" && Number.isFinite(opts.commandTimeoutMs) ? normalizeBrowserTimerDelayMs(opts.commandTimeoutMs) : void 0; const clearPendingTimer = (p) => { if (p.timer !== void 0) clearTimeout(p.timer); }; const send = (method, params, sessionId) => { const id = nextId++; const msg = { id, method, params, sessionId }; return new Promise((resolve, reject) => { if (ws.readyState !== WebSocket.OPEN) { reject(/* @__PURE__ */ new Error("CDP socket closed")); return; } const entry = { resolve, reject }; if (commandTimeoutMs !== void 0) entry.timer = setTimeout(() => { closeWithError(/* @__PURE__ */ new Error(`CDP command ${method} timed out after ${commandTimeoutMs}ms`)); }, commandTimeoutMs); pending.set(id, entry); try { ws.send(JSON.stringify(msg)); } catch (err) { pending.delete(id); clearPendingTimer(entry); reject(err instanceof Error ? err : new Error(String(err))); } }); }; const closeWithError = (err) => { for (const [, p] of pending) { clearPendingTimer(p); p.reject(err); } pending.clear(); try { ws.close(); } catch {} }; ws.on("error", (err) => { /* c8 ignore next */ closeWithError(err instanceof Error ? err : new Error(String(err))); }); ws.on("message", (data) => { try { const parsed = JSON.parse(rawCdpMessageToString(data)); if (typeof parsed.id !== "number") return; const p = pending.get(parsed.id); if (!p) return; pending.delete(parsed.id); clearPendingTimer(p); if (parsed.error?.message) { p.reject(new Error(parsed.error.message)); return; } p.resolve(parsed.result); } catch {} }); ws.on("close", () => { closeWithError(/* @__PURE__ */ new Error("CDP socket closed")); }); return { send, closeWithError }; } /** Fetch and parse a CDP JSON endpoint through the configured SSRF guard. */ async function fetchJson(url, timeoutMs = CDP_HTTP_REQUEST_TIMEOUT_MS, init, ssrfPolicy) { const { response, release } = await fetchCdpChecked(url, timeoutMs, init, ssrfPolicy); try { return await response.json(); } finally { await release(); } } /** Fetch a CDP endpoint and return the response with an idempotent release hook. */ async function fetchCdpChecked(url, timeoutMs = CDP_HTTP_REQUEST_TIMEOUT_MS, init, ssrfPolicy) { const ctrl = new AbortController(); const t = setTimeout(ctrl.abort.bind(ctrl), normalizeBrowserTimerDelayMs(timeoutMs)); let guardedRelease; let released = false; const release = async () => { if (released) return; released = true; clearTimeout(t); await guardedRelease?.(); }; try { const headers = getHeadersWithAuth(url, init?.headers || {}); const fetchUrl = stripUrlCredentials(url); const res = await withManagedProxyForCdpUrl(fetchUrl, () => withNoProxyForCdpUrl(url, async () => { const parsedUrl = new URL(fetchUrl); const policy = isLoopbackHost(parsedUrl.hostname) ? withAllowedHostname(ssrfPolicy, parsedUrl.hostname) : ssrfPolicy ?? { allowPrivateNetwork: true }; const guarded = await fetchWithSsrFGuard({ url: fetchUrl, init: { ...init, headers }, signal: ctrl.signal, policy, auditContext: "browser-cdp" }); guardedRelease = guarded.release; return guarded.response; })); if (!res.ok) { if (res.status === 429) throw new Error(`${resolveBrowserRateLimitMessage(url)} Do NOT retry the browser tool.`); throw new Error(`HTTP ${res.status}`); } return { response: res, release }; } catch (error) { await release(); if (error instanceof SsrFBlockedError) throw new BrowserCdpEndpointBlockedError({ cause: error }); throw error; } } /** Probe that a CDP endpoint responds with an OK HTTP status. */ async function fetchOk(url, timeoutMs = CDP_HTTP_REQUEST_TIMEOUT_MS, init, ssrfPolicy) { const { release } = await fetchCdpChecked(url, timeoutMs, init, ssrfPolicy); await release(); } /** Open a CDP WebSocket with URL basic-auth and proxy bypass handling. */ function openCdpWebSocket(wsUrl, opts) { const headers = getHeadersWithAuth(wsUrl, opts?.headers ?? {}); const handshakeTimeoutMs = typeof opts?.handshakeTimeoutMs === "number" && Number.isFinite(opts.handshakeTimeoutMs) ? Math.max(1, Math.floor(opts.handshakeTimeoutMs)) : CDP_WS_HANDSHAKE_TIMEOUT_MS; const agent = getDirectAgentForCdp(wsUrl); return withManagedProxyForCdpUrl(stripUrlCredentials(wsUrl), () => new WebSocket(wsUrl, { handshakeTimeout: handshakeTimeoutMs, ...Object.keys(headers).length ? { headers } : {}, ...agent ? { agent } : {} })); } function sleep(ms) { return new Promise((resolve) => { setTimeout(resolve, ms); }); } function normalizeRetryCount(value, fallback) { if (typeof value !== "number" || !Number.isFinite(value)) return fallback; return Math.max(0, Math.floor(value)); } function computeHandshakeRetryDelayMs(attempt, opts) { const baseDelayMs = typeof opts?.handshakeRetryDelayMs === "number" && Number.isFinite(opts.handshakeRetryDelayMs) ? Math.max(1, Math.floor(opts.handshakeRetryDelayMs)) : 200; const maxDelayMs = typeof opts?.handshakeMaxRetryDelayMs === "number" && Number.isFinite(opts.handshakeMaxRetryDelayMs) ? Math.max(baseDelayMs, Math.floor(opts.handshakeMaxRetryDelayMs)) : 3e3; const raw = Math.min(maxDelayMs, baseDelayMs * 2 ** Math.max(0, attempt - 1)); const jitterScale = .8 + Math.random() * .4; return Math.max(1, Math.floor(raw * jitterScale)); } function shouldRetryCdpHandshakeError(err) { if (!(err instanceof Error)) return false; const msg = err.message.toLowerCase(); if (!msg) return false; if (msg.includes("rate limit")) return false; const statusMatch = msg.match(/(?:unexpected server response|response):\s*(\d{3})/); if (statusMatch?.[1]) return Number(statusMatch[1]) >= 500; return msg.includes("cdp socket closed") || msg.includes("econnreset") || msg.includes("econnrefused") || msg.includes("econnaborted") || msg.includes("ehostunreach") || msg.includes("enetunreach") || msg.includes("etimedout") || msg.includes("socket hang up") || msg.includes("websocket error") || msg.includes("closed before"); } async function withCdpSocket(wsUrl, fn, opts) { const maxHandshakeRetries = normalizeRetryCount(opts?.handshakeRetries, 2); let lastHandshakeError; for (let attempt = 0; attempt <= maxHandshakeRetries; attempt += 1) { const ws = openCdpWebSocket(wsUrl, opts); const { send, closeWithError } = createCdpSender(ws, opts); const openPromise = new Promise((resolve, reject) => { ws.once("open", () => resolve()); ws.once("error", (err) => reject(err)); ws.once("close", () => reject(/* @__PURE__ */ new Error("CDP socket closed"))); }); try { await openPromise; } catch (err) { lastHandshakeError = err; /* c8 ignore next */ closeWithError(err instanceof Error ? err : new Error(String(err))); try { ws.close(); } catch {} if (attempt >= maxHandshakeRetries || !shouldRetryCdpHandshakeError(err)) throw err; await sleep(computeHandshakeRetryDelayMs(attempt + 1, opts)); continue; } try { return await fn(send); } catch (err) { closeWithError(err instanceof Error ? err : new Error(String(err))); throw err; } finally { try { ws.close(); } catch {} } } if (lastHandshakeError instanceof Error) throw lastHandshakeError; throw new Error("CDP socket failed to open"); } //#endregion export { CHROME_MCP_ATTACH_READY_WINDOW_MS as A, DEFAULT_BROWSER_EVALUATE_ENABLED as B, BrowserTargetAmbiguousError as C, CHROME_BOOTSTRAP_EXIT_TIMEOUT_MS as D, CDP_JSON_NEW_TIMEOUT_MS as E, usesFastLoopbackCdpProbeClass as F, DEFAULT_OPENCLAW_BROWSER_COLOR as G, DEFAULT_BROWSER_LOCAL_LAUNCH_TIMEOUT_MS as H, DEFAULT_AI_SNAPSHOT_EFFICIENT_MAX_CHARS as I, assertManagedProxyAllowsCdpUrl as J, DEFAULT_OPENCLAW_BROWSER_ENABLED as K, DEFAULT_AI_SNAPSHOT_MAX_CHARS as L, CHROME_STOP_TIMEOUT_MS as M, PROFILE_ATTACH_RETRY_TIMEOUT_MS as N, CHROME_BOOTSTRAP_PREFS_TIMEOUT_MS as O, resolveCdpReachabilityTimeouts as P, DEFAULT_BROWSER_ACTION_TIMEOUT_MS as R, BrowserTabNotFoundError as S, toBrowserErrorResponse as T, DEFAULT_BROWSER_SCREENSHOT_TIMEOUT_MS as U, DEFAULT_BROWSER_LOCAL_CDP_READY_TIMEOUT_MS as V, DEFAULT_BROWSER_SNAPSHOT_TIMEOUT_MS as W, withNoProxyForCdpUrl as Y, BrowserError as _, fetchOk as a, BrowserResetUnsupportedError as b, isWebSocketUrl as c, withCdpSocket as d, normalizeBrowserTimerDelayMs as f, BrowserConflictError as g, BrowserCdpEndpointBlockedError as h, fetchJson as i, CHROME_STDERR_HINT_MAX_CHARS as j, CHROME_LAUNCH_READY_WINDOW_MS as k, normalizeCdpHttpBaseForJsonEndpoints as l, resolveBrowserRateLimitMessage as m, assertCdpEndpointAllowed as n, getHeadersWithAuth as o, withAllowedHostname as p, DEFAULT_OPENCLAW_BROWSER_PROFILE_NAME as q, fetchCdpChecked as r, isDirectCdpWebSocketEndpoint as s, appendCdpPath as t, openCdpWebSocket as u, BrowserProfileNotFoundError as v, BrowserValidationError as w, BrowserResourceExhaustedError as x, BrowserProfileUnavailableError as y, DEFAULT_BROWSER_DEFAULT_PROFILE_NAME as z };