browserclaw 0.5.7 → 0.5.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -1441,6 +1441,64 @@ async function stopChrome(running, timeoutMs = 2500) {
1441
1441
  } catch {
1442
1442
  }
1443
1443
  }
1444
+ var BrowserTabNotFoundError = class extends Error {
1445
+ constructor(message = "Tab not found") {
1446
+ super(message);
1447
+ this.name = "BrowserTabNotFoundError";
1448
+ }
1449
+ };
1450
+ var OPENCLAW_EXTENSION_RELAY_BROWSER = "OpenClaw/extension-relay";
1451
+ var extensionRelayByCdpUrl = /* @__PURE__ */ new Map();
1452
+ async function fetchJsonForCdp(url, timeoutMs) {
1453
+ const ctrl = new AbortController();
1454
+ const t = setTimeout(() => ctrl.abort(), timeoutMs);
1455
+ try {
1456
+ const res = await fetch(url, { signal: ctrl.signal });
1457
+ if (!res.ok) return null;
1458
+ return await res.json();
1459
+ } catch {
1460
+ return null;
1461
+ } finally {
1462
+ clearTimeout(t);
1463
+ }
1464
+ }
1465
+ function appendCdpPath2(cdpUrl, cdpPath) {
1466
+ try {
1467
+ const url = new URL(cdpUrl);
1468
+ url.pathname = `${url.pathname.replace(/\/$/, "")}${cdpPath.startsWith("/") ? cdpPath : `/${cdpPath}`}`;
1469
+ return url.toString();
1470
+ } catch {
1471
+ return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
1472
+ }
1473
+ }
1474
+ async function isExtensionRelayCdpEndpoint(cdpUrl) {
1475
+ const normalized = normalizeCdpUrl(cdpUrl);
1476
+ const cached = extensionRelayByCdpUrl.get(normalized);
1477
+ if (cached !== void 0) return cached;
1478
+ try {
1479
+ const version = await fetchJsonForCdp(appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(normalized), "/json/version"), 2e3);
1480
+ const isRelay = String(version?.Browser ?? "").trim() === OPENCLAW_EXTENSION_RELAY_BROWSER;
1481
+ extensionRelayByCdpUrl.set(normalized, isRelay);
1482
+ return isRelay;
1483
+ } catch {
1484
+ extensionRelayByCdpUrl.set(normalized, false);
1485
+ return false;
1486
+ }
1487
+ }
1488
+ async function withPlaywrightPageCdpSession(page, fn) {
1489
+ const session = await page.context().newCDPSession(page);
1490
+ try {
1491
+ return await fn(session);
1492
+ } finally {
1493
+ await session.detach().catch(() => {
1494
+ });
1495
+ }
1496
+ }
1497
+ async function withPageScopedCdpClient(opts) {
1498
+ return await withPlaywrightPageCdpSession(opts.page, async (session) => {
1499
+ return await opts.fn((method, params) => session.send(method, params));
1500
+ });
1501
+ }
1444
1502
  var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
1445
1503
  function noProxyAlreadyCoversLocalhost() {
1446
1504
  const current = process.env.NO_PROXY || process.env.no_proxy || "";
@@ -1713,9 +1771,11 @@ async function connectBrowser(cdpUrl, authToken) {
1713
1771
  if (authToken && !headers["Authorization"]) headers["Authorization"] = `Bearer ${authToken}`;
1714
1772
  const browser = await withNoProxyForCdpUrl(endpoint, () => playwrightCore.chromium.connectOverCDP(endpoint, { timeout, headers }));
1715
1773
  const onDisconnected = () => {
1716
- if (cachedByCdpUrl.get(normalized)?.browser === browser) cachedByCdpUrl.delete(normalized);
1717
- for (const key of roleRefsByTarget.keys()) {
1718
- if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
1774
+ if (cachedByCdpUrl.get(normalized)?.browser === browser) {
1775
+ cachedByCdpUrl.delete(normalized);
1776
+ for (const key of roleRefsByTarget.keys()) {
1777
+ if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
1778
+ }
1719
1779
  }
1720
1780
  };
1721
1781
  const connected = { browser, cdpUrl: normalized, onDisconnected };
@@ -1859,8 +1919,36 @@ async function pageTargetId(page) {
1859
1919
  });
1860
1920
  }
1861
1921
  }
1922
+ function matchPageByTargetList(pages, targets, targetId) {
1923
+ const target = targets.find((entry) => entry.id === targetId);
1924
+ if (!target) return null;
1925
+ const urlMatch = pages.filter((page) => page.url() === target.url);
1926
+ if (urlMatch.length === 1) return urlMatch[0] ?? null;
1927
+ if (urlMatch.length > 1) {
1928
+ const sameUrlTargets = targets.filter((entry) => entry.url === target.url);
1929
+ if (sameUrlTargets.length === urlMatch.length) {
1930
+ const idx = sameUrlTargets.findIndex((entry) => entry.id === targetId);
1931
+ if (idx >= 0 && idx < urlMatch.length) return urlMatch[idx] ?? null;
1932
+ }
1933
+ }
1934
+ return null;
1935
+ }
1936
+ async function findPageByTargetIdViaTargetList(pages, targetId, cdpUrl) {
1937
+ const targets = await fetchJsonForCdp(appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(cdpUrl), "/json/list"), 2e3);
1938
+ if (!Array.isArray(targets)) return null;
1939
+ return matchPageByTargetList(pages, targets, targetId);
1940
+ }
1862
1941
  async function findPageByTargetId(browser, targetId, cdpUrl) {
1863
1942
  const pages = await getAllPages(browser);
1943
+ const isExtensionRelay = cdpUrl ? await isExtensionRelayCdpEndpoint(cdpUrl).catch(() => false) : false;
1944
+ if (cdpUrl && isExtensionRelay) {
1945
+ try {
1946
+ const matched = await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
1947
+ if (matched) return matched;
1948
+ } catch {
1949
+ }
1950
+ return pages.length === 1 ? pages[0] ?? null : null;
1951
+ }
1864
1952
  let resolvedViaCdp = false;
1865
1953
  for (const page of pages) {
1866
1954
  let tid = null;
@@ -1874,34 +1962,11 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
1874
1962
  }
1875
1963
  if (cdpUrl) {
1876
1964
  try {
1877
- const listUrl = `${normalizeCdpHttpBaseForJsonEndpoints(cdpUrl)}/json/list`;
1878
- const headers = {};
1879
- const ctrl = new AbortController();
1880
- const t = setTimeout(() => ctrl.abort(), 2e3);
1881
- try {
1882
- const response = await fetch(listUrl, { headers, signal: ctrl.signal });
1883
- if (response.ok) {
1884
- const targets = await response.json();
1885
- const target = targets.find((entry) => entry.id === targetId);
1886
- if (target) {
1887
- const urlMatch = pages.filter((p) => p.url() === target.url);
1888
- if (urlMatch.length === 1) return urlMatch[0];
1889
- if (urlMatch.length > 1) {
1890
- const sameUrlTargets = targets.filter((entry) => entry.url === target.url);
1891
- if (sameUrlTargets.length === urlMatch.length) {
1892
- const idx = sameUrlTargets.findIndex((entry) => entry.id === targetId);
1893
- if (idx >= 0 && idx < urlMatch.length) return urlMatch[idx];
1894
- }
1895
- }
1896
- }
1897
- }
1898
- } finally {
1899
- clearTimeout(t);
1900
- }
1965
+ return await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
1901
1966
  } catch {
1902
1967
  }
1903
1968
  }
1904
- if (!resolvedViaCdp && pages.length === 1) return pages[0];
1969
+ if (!resolvedViaCdp && pages.length === 1) return pages[0] ?? null;
1905
1970
  return null;
1906
1971
  }
1907
1972
  async function getPageForTargetId(opts) {
@@ -1913,10 +1978,49 @@ async function getPageForTargetId(opts) {
1913
1978
  const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
1914
1979
  if (!found) {
1915
1980
  if (pages.length === 1) return first;
1916
- throw new Error(`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`);
1981
+ throw new BrowserTabNotFoundError(`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`);
1917
1982
  }
1918
1983
  return found;
1919
1984
  }
1985
+ async function resolvePageByTargetIdOrThrow(opts) {
1986
+ const { browser } = await connectBrowser(opts.cdpUrl);
1987
+ const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
1988
+ if (!page) throw new BrowserTabNotFoundError();
1989
+ return page;
1990
+ }
1991
+ function parseRoleRef(raw) {
1992
+ const trimmed = raw.trim();
1993
+ if (!trimmed) return null;
1994
+ const normalized = trimmed.startsWith("@") ? trimmed.slice(1) : trimmed.startsWith("ref=") ? trimmed.slice(4) : trimmed;
1995
+ return /^e\d+$/.test(normalized) ? normalized : null;
1996
+ }
1997
+ function requireRef(value) {
1998
+ const raw = typeof value === "string" ? value.trim() : "";
1999
+ const ref = (raw ? parseRoleRef(raw) : null) ?? (raw.startsWith("@") ? raw.slice(1) : raw);
2000
+ if (!ref) throw new Error("ref is required");
2001
+ return ref;
2002
+ }
2003
+ function requireRefOrSelector(ref, selector) {
2004
+ const trimmedRef = typeof ref === "string" ? ref.trim() : "";
2005
+ const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
2006
+ if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
2007
+ return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
2008
+ }
2009
+ function resolveInteractionTimeoutMs(timeoutMs) {
2010
+ return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
2011
+ }
2012
+ function resolveBoundedDelayMs(value, label, maxMs) {
2013
+ const normalized = Math.floor(value ?? 0);
2014
+ if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
2015
+ if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
2016
+ return normalized;
2017
+ }
2018
+ async function getRestoredPageForTarget(opts) {
2019
+ const page = await getPageForTargetId(opts);
2020
+ ensurePageState(page);
2021
+ restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2022
+ return page;
2023
+ }
1920
2024
  function refLocator(page, ref) {
1921
2025
  const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
1922
2026
  if (!normalized.trim()) throw new Error("ref is required");
@@ -2232,12 +2336,14 @@ async function snapshotAi(opts) {
2232
2336
  let snapshot = String(result?.full ?? "");
2233
2337
  const maxChars = opts.maxChars;
2234
2338
  const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
2339
+ let truncated = false;
2235
2340
  if (limit && snapshot.length > limit) {
2236
2341
  const lastNewline = snapshot.lastIndexOf("\n", limit);
2237
2342
  const cutoff = lastNewline > 0 ? lastNewline : limit;
2238
2343
  snapshot = `${snapshot.slice(0, cutoff)}
2239
2344
 
2240
2345
  [...TRUNCATED - page too large]`;
2346
+ truncated = true;
2241
2347
  }
2242
2348
  const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
2243
2349
  storeRoleRefsForTarget({
@@ -2251,6 +2357,7 @@ async function snapshotAi(opts) {
2251
2357
  snapshot: built.snapshot,
2252
2358
  refs: built.refs,
2253
2359
  stats: getRoleSnapshotStats(built.snapshot, built.refs),
2360
+ ...truncated ? { truncated } : {},
2254
2361
  untrusted: true,
2255
2362
  contentMeta: {
2256
2363
  sourceUrl,
@@ -2324,24 +2431,20 @@ async function snapshotAria(opts) {
2324
2431
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2325
2432
  ensurePageState(page);
2326
2433
  const sourceUrl = page.url();
2327
- const session = await page.context().newCDPSession(page);
2328
- try {
2434
+ const res = await withPlaywrightPageCdpSession(page, async (session) => {
2329
2435
  await session.send("Accessibility.enable").catch(() => {
2330
2436
  });
2331
- const res = await session.send("Accessibility.getFullAXTree");
2332
- return {
2333
- nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit),
2334
- untrusted: true,
2335
- contentMeta: {
2336
- sourceUrl,
2337
- contentType: "browser-aria-tree",
2338
- capturedAt: (/* @__PURE__ */ new Date()).toISOString()
2339
- }
2340
- };
2341
- } finally {
2342
- await session.detach().catch(() => {
2343
- });
2344
- }
2437
+ return await session.send("Accessibility.getFullAXTree");
2438
+ });
2439
+ return {
2440
+ nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit),
2441
+ untrusted: true,
2442
+ contentMeta: {
2443
+ sourceUrl,
2444
+ contentType: "browser-aria-tree",
2445
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
2446
+ }
2447
+ };
2345
2448
  }
2346
2449
  function axValue(v) {
2347
2450
  if (!v || typeof v !== "object") return "";
@@ -2793,6 +2896,14 @@ async function assertSafeUploadPaths(paths) {
2793
2896
  }
2794
2897
  }
2795
2898
  }
2899
+ async function resolveStrictExistingUploadPaths(params) {
2900
+ try {
2901
+ await assertSafeUploadPaths(params.requestedPaths);
2902
+ return { ok: true, paths: params.requestedPaths };
2903
+ } catch (err) {
2904
+ return { ok: false, error: err instanceof Error ? err.message : String(err) };
2905
+ }
2906
+ }
2796
2907
  function sanitizeUntrustedFileName(fileName, fallbackName) {
2797
2908
  const trimmed = String(fileName ?? "").trim();
2798
2909
  if (!trimmed) return fallbackName;
@@ -2866,47 +2977,57 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
2866
2977
 
2867
2978
  // src/actions/interaction.ts
2868
2979
  var MAX_CLICK_DELAY_MS = 5e3;
2869
- function resolveBoundedDelayMs(value, label, maxMs) {
2870
- const normalized = Math.floor(value ?? 0);
2871
- if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
2872
- if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
2873
- return normalized;
2874
- }
2875
- function resolveInteractionTimeoutMs(timeoutMs) {
2876
- return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
2877
- }
2878
- function requireRefOrSelector(ref, selector) {
2879
- const trimmedRef = typeof ref === "string" ? ref.trim() : "";
2880
- const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
2881
- if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
2882
- return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
2883
- }
2980
+ var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "switch"]);
2884
2981
  function resolveLocator(page, resolved) {
2885
2982
  return resolved.ref ? refLocator(page, resolved.ref) : page.locator(resolved.selector);
2886
2983
  }
2887
- async function getRestoredPageForTarget(opts) {
2888
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2889
- ensurePageState(page);
2890
- restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
2891
- return page;
2892
- }
2893
2984
  async function clickViaPlaywright(opts) {
2894
2985
  const resolved = requireRefOrSelector(opts.ref, opts.selector);
2895
2986
  const page = await getRestoredPageForTarget(opts);
2896
2987
  const label = resolved.ref ?? resolved.selector;
2897
2988
  const locator = resolveLocator(page, resolved);
2898
2989
  const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
2990
+ let checkableRole = false;
2991
+ if (resolved.ref) {
2992
+ const refId = parseRoleRef(resolved.ref);
2993
+ if (refId) {
2994
+ const state = ensurePageState(page);
2995
+ const info = state.roleRefs?.[refId];
2996
+ if (info && CHECKABLE_ROLES.has(info.role)) checkableRole = true;
2997
+ }
2998
+ }
2899
2999
  try {
2900
3000
  const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
2901
3001
  if (delayMs > 0) {
2902
3002
  await locator.hover({ timeout });
2903
3003
  await new Promise((resolve2) => setTimeout(resolve2, delayMs));
2904
3004
  }
3005
+ let ariaCheckedBefore;
3006
+ if (checkableRole && !opts.doubleClick) {
3007
+ ariaCheckedBefore = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
3008
+ }
2905
3009
  if (opts.doubleClick) {
2906
3010
  await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
2907
3011
  } else {
2908
3012
  await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
2909
3013
  }
3014
+ if (checkableRole && !opts.doubleClick && ariaCheckedBefore !== void 0) {
3015
+ const POLL_INTERVAL_MS = 50;
3016
+ const POLL_TIMEOUT_MS = 500;
3017
+ let changed = false;
3018
+ for (let elapsed = 0; elapsed < POLL_TIMEOUT_MS; elapsed += POLL_INTERVAL_MS) {
3019
+ const current = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
3020
+ if (current === void 0 || current !== ariaCheckedBefore) {
3021
+ changed = true;
3022
+ break;
3023
+ }
3024
+ await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
3025
+ }
3026
+ if (!changed) {
3027
+ await locator.evaluate((el) => el.click()).catch(() => {
3028
+ });
3029
+ }
3030
+ }
2910
3031
  } catch (err) {
2911
3032
  throw toAIFriendlyError(err, label);
2912
3033
  }
@@ -3006,10 +3127,11 @@ async function scrollIntoViewViaPlaywright(opts) {
3006
3127
  }
3007
3128
  async function highlightViaPlaywright(opts) {
3008
3129
  const page = await getRestoredPageForTarget(opts);
3130
+ const ref = requireRef(opts.ref);
3009
3131
  try {
3010
- await refLocator(page, opts.ref).highlight();
3132
+ await refLocator(page, ref).highlight();
3011
3133
  } catch (err) {
3012
- throw toAIFriendlyError(err, opts.ref);
3134
+ throw toAIFriendlyError(err, ref);
3013
3135
  }
3014
3136
  }
3015
3137
  async function setInputFilesViaPlaywright(opts) {
@@ -3020,9 +3142,14 @@ async function setInputFilesViaPlaywright(opts) {
3020
3142
  if (inputRef && element) throw new Error("ref and element are mutually exclusive");
3021
3143
  if (!inputRef && !element) throw new Error("Either ref or element is required for setInputFiles");
3022
3144
  const locator = inputRef ? refLocator(page, inputRef) : page.locator(element).first();
3023
- await assertSafeUploadPaths(opts.paths);
3145
+ const uploadPathsResult = await resolveStrictExistingUploadPaths({
3146
+ requestedPaths: opts.paths,
3147
+ scopeLabel: "uploads directory"
3148
+ });
3149
+ if (!uploadPathsResult.ok) throw new Error(uploadPathsResult.error);
3150
+ const resolvedPaths = uploadPathsResult.paths;
3024
3151
  try {
3025
- await locator.setInputFiles(opts.paths);
3152
+ await locator.setInputFiles(resolvedPaths);
3026
3153
  } catch (err) {
3027
3154
  throw toAIFriendlyError(err, inputRef || element);
3028
3155
  }
@@ -3065,16 +3192,18 @@ async function armFileUploadViaPlaywright(opts) {
3065
3192
  }
3066
3193
  return;
3067
3194
  }
3068
- try {
3069
- await assertSafeUploadPaths(opts.paths);
3070
- } catch {
3195
+ const uploadPathsResult = await resolveStrictExistingUploadPaths({
3196
+ requestedPaths: opts.paths,
3197
+ scopeLabel: "uploads directory"
3198
+ });
3199
+ if (!uploadPathsResult.ok) {
3071
3200
  try {
3072
3201
  await page.keyboard.press("Escape");
3073
3202
  } catch {
3074
3203
  }
3075
3204
  return;
3076
3205
  }
3077
- await fileChooser.setFiles(opts.paths);
3206
+ await fileChooser.setFiles(uploadPathsResult.paths);
3078
3207
  try {
3079
3208
  const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
3080
3209
  if (input) {
@@ -3146,21 +3275,21 @@ async function listPagesViaPlaywright(opts) {
3146
3275
  return results;
3147
3276
  }
3148
3277
  async function createPageViaPlaywright(opts) {
3149
- const targetUrl = (opts.url ?? "").trim() || "about:blank";
3150
- const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
3151
- if (targetUrl !== "about:blank") {
3152
- await assertBrowserNavigationAllowed({ url: targetUrl, ssrfPolicy: policy });
3153
- }
3154
3278
  const { browser } = await connectBrowser(opts.cdpUrl);
3155
3279
  const context = browser.contexts()[0] ?? await browser.newContext();
3156
3280
  ensureContextState(context);
3157
3281
  const page = await context.newPage();
3158
3282
  ensurePageState(page);
3283
+ const targetUrl = (opts.url ?? "").trim() || "about:blank";
3284
+ const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
3159
3285
  if (targetUrl !== "about:blank") {
3160
3286
  const navigationPolicy = withBrowserNavigationPolicy(policy);
3161
- const response = await page.goto(targetUrl, { timeout: 3e4 }).catch(() => null);
3162
- await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...navigationPolicy });
3163
- await assertBrowserNavigationResultAllowed({ url: page.url(), ssrfPolicy: policy });
3287
+ await assertBrowserNavigationAllowed({ url: targetUrl, ...navigationPolicy });
3288
+ await assertBrowserNavigationRedirectChainAllowed({
3289
+ request: (await page.goto(targetUrl, { timeout: 3e4 }).catch(() => null))?.request(),
3290
+ ...navigationPolicy
3291
+ });
3292
+ await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
3164
3293
  }
3165
3294
  const tid = await pageTargetId(page).catch(() => null);
3166
3295
  if (!tid) throw new Error("Failed to get targetId for new page");
@@ -3171,27 +3300,31 @@ async function createPageViaPlaywright(opts) {
3171
3300
  type: "page"
3172
3301
  };
3173
3302
  }
3174
- async function closePageByTargetIdViaPlaywright(opts) {
3175
- const { browser } = await connectBrowser(opts.cdpUrl);
3176
- const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
3177
- if (!page) throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
3303
+ async function closePageViaPlaywright(opts) {
3304
+ const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3305
+ ensurePageState(page);
3178
3306
  await page.close();
3179
3307
  }
3308
+ async function closePageByTargetIdViaPlaywright(opts) {
3309
+ await (await resolvePageByTargetIdOrThrow(opts)).close();
3310
+ }
3180
3311
  async function focusPageByTargetIdViaPlaywright(opts) {
3181
- const { browser } = await connectBrowser(opts.cdpUrl);
3182
- const page = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
3183
- if (!page) throw new Error(`Tab not found (targetId: ${opts.targetId}). Use browser.tabs() to list open tabs.`);
3312
+ const page = await resolvePageByTargetIdOrThrow(opts);
3184
3313
  try {
3185
3314
  await page.bringToFront();
3186
3315
  } catch (err) {
3187
- const session = await page.context().newCDPSession(page);
3188
3316
  try {
3189
- await session.send("Page.bringToFront");
3317
+ await withPageScopedCdpClient({
3318
+ cdpUrl: opts.cdpUrl,
3319
+ page,
3320
+ targetId: opts.targetId,
3321
+ fn: async (send) => {
3322
+ await send("Page.bringToFront");
3323
+ }
3324
+ });
3325
+ return;
3190
3326
  } catch {
3191
3327
  throw err;
3192
- } finally {
3193
- await session.detach().catch(() => {
3194
- });
3195
3328
  }
3196
3329
  }
3197
3330
  }
@@ -3379,6 +3512,165 @@ async function evaluateViaPlaywright(opts) {
3379
3512
  if (signal && abortListener) signal.removeEventListener("abort", abortListener);
3380
3513
  }
3381
3514
  }
3515
+
3516
+ // src/actions/batch.ts
3517
+ var MAX_BATCH_DEPTH = 5;
3518
+ var MAX_BATCH_ACTIONS = 100;
3519
+ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, depth = 0) {
3520
+ if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${MAX_BATCH_DEPTH}`);
3521
+ const effectiveTargetId = action.targetId ?? targetId;
3522
+ switch (action.kind) {
3523
+ case "click":
3524
+ await clickViaPlaywright({
3525
+ cdpUrl,
3526
+ targetId: effectiveTargetId,
3527
+ ref: action.ref,
3528
+ selector: action.selector,
3529
+ doubleClick: action.doubleClick,
3530
+ button: action.button,
3531
+ modifiers: action.modifiers,
3532
+ delayMs: action.delayMs,
3533
+ timeoutMs: action.timeoutMs
3534
+ });
3535
+ break;
3536
+ case "type":
3537
+ await typeViaPlaywright({
3538
+ cdpUrl,
3539
+ targetId: effectiveTargetId,
3540
+ ref: action.ref,
3541
+ selector: action.selector,
3542
+ text: action.text,
3543
+ submit: action.submit,
3544
+ slowly: action.slowly,
3545
+ timeoutMs: action.timeoutMs
3546
+ });
3547
+ break;
3548
+ case "press":
3549
+ await pressKeyViaPlaywright({
3550
+ cdpUrl,
3551
+ targetId: effectiveTargetId,
3552
+ key: action.key,
3553
+ delayMs: action.delayMs
3554
+ });
3555
+ break;
3556
+ case "hover":
3557
+ await hoverViaPlaywright({
3558
+ cdpUrl,
3559
+ targetId: effectiveTargetId,
3560
+ ref: action.ref,
3561
+ selector: action.selector,
3562
+ timeoutMs: action.timeoutMs
3563
+ });
3564
+ break;
3565
+ case "scrollIntoView":
3566
+ await scrollIntoViewViaPlaywright({
3567
+ cdpUrl,
3568
+ targetId: effectiveTargetId,
3569
+ ref: action.ref,
3570
+ selector: action.selector,
3571
+ timeoutMs: action.timeoutMs
3572
+ });
3573
+ break;
3574
+ case "drag":
3575
+ await dragViaPlaywright({
3576
+ cdpUrl,
3577
+ targetId: effectiveTargetId,
3578
+ startRef: action.startRef,
3579
+ startSelector: action.startSelector,
3580
+ endRef: action.endRef,
3581
+ endSelector: action.endSelector,
3582
+ timeoutMs: action.timeoutMs
3583
+ });
3584
+ break;
3585
+ case "select":
3586
+ await selectOptionViaPlaywright({
3587
+ cdpUrl,
3588
+ targetId: effectiveTargetId,
3589
+ ref: action.ref,
3590
+ selector: action.selector,
3591
+ values: action.values,
3592
+ timeoutMs: action.timeoutMs
3593
+ });
3594
+ break;
3595
+ case "fill":
3596
+ await fillFormViaPlaywright({
3597
+ cdpUrl,
3598
+ targetId: effectiveTargetId,
3599
+ fields: action.fields,
3600
+ timeoutMs: action.timeoutMs
3601
+ });
3602
+ break;
3603
+ case "resize":
3604
+ await resizeViewportViaPlaywright({
3605
+ cdpUrl,
3606
+ targetId: effectiveTargetId,
3607
+ width: action.width,
3608
+ height: action.height
3609
+ });
3610
+ break;
3611
+ case "wait":
3612
+ if (action.fn && !evaluateEnabled) throw new Error("wait --fn is disabled by config (browser.evaluateEnabled=false)");
3613
+ await waitForViaPlaywright({
3614
+ cdpUrl,
3615
+ targetId: effectiveTargetId,
3616
+ timeMs: action.timeMs,
3617
+ text: action.text,
3618
+ textGone: action.textGone,
3619
+ selector: action.selector,
3620
+ url: action.url,
3621
+ loadState: action.loadState,
3622
+ fn: action.fn,
3623
+ timeoutMs: action.timeoutMs
3624
+ });
3625
+ break;
3626
+ case "evaluate":
3627
+ if (!evaluateEnabled) throw new Error("act:evaluate is disabled by config (browser.evaluateEnabled=false)");
3628
+ await evaluateViaPlaywright({
3629
+ cdpUrl,
3630
+ targetId: effectiveTargetId,
3631
+ fn: action.fn,
3632
+ ref: action.ref,
3633
+ timeoutMs: action.timeoutMs
3634
+ });
3635
+ break;
3636
+ case "close":
3637
+ await closePageViaPlaywright({
3638
+ cdpUrl,
3639
+ targetId: effectiveTargetId
3640
+ });
3641
+ break;
3642
+ case "batch":
3643
+ await batchViaPlaywright({
3644
+ cdpUrl,
3645
+ targetId: effectiveTargetId,
3646
+ actions: action.actions,
3647
+ stopOnError: action.stopOnError,
3648
+ evaluateEnabled,
3649
+ depth: depth + 1
3650
+ });
3651
+ break;
3652
+ default:
3653
+ throw new Error(`Unsupported batch action kind: ${action.kind}`);
3654
+ }
3655
+ }
3656
+ async function batchViaPlaywright(opts) {
3657
+ const depth = opts.depth ?? 0;
3658
+ if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${MAX_BATCH_DEPTH}`);
3659
+ if (opts.actions.length > MAX_BATCH_ACTIONS) throw new Error(`Batch exceeds maximum of ${MAX_BATCH_ACTIONS} actions`);
3660
+ const results = [];
3661
+ const evaluateEnabled = opts.evaluateEnabled !== false;
3662
+ for (const action of opts.actions) {
3663
+ try {
3664
+ await executeSingleAction(action, opts.cdpUrl, opts.targetId, evaluateEnabled, depth);
3665
+ results.push({ ok: true });
3666
+ } catch (err) {
3667
+ const message = err instanceof Error ? err.message : String(err);
3668
+ results.push({ ok: false, error: message });
3669
+ if (opts.stopOnError !== false) break;
3670
+ }
3671
+ }
3672
+ return { results };
3673
+ }
3382
3674
  function createPageDownloadWaiter(page, timeoutMs) {
3383
3675
  let done = false;
3384
3676
  let timer;
@@ -3500,32 +3792,33 @@ async function setDeviceViaPlaywright(opts) {
3500
3792
  height: device.viewport.height
3501
3793
  });
3502
3794
  }
3503
- const session = await page.context().newCDPSession(page);
3504
- try {
3505
- const locale = device.locale;
3506
- if (device.userAgent || locale) {
3507
- await session.send("Emulation.setUserAgentOverride", {
3508
- userAgent: device.userAgent ?? "",
3509
- acceptLanguage: locale ?? void 0
3510
- });
3511
- }
3512
- if (device.viewport) {
3513
- await session.send("Emulation.setDeviceMetricsOverride", {
3514
- mobile: Boolean(device.isMobile),
3515
- width: device.viewport.width,
3516
- height: device.viewport.height,
3517
- deviceScaleFactor: device.deviceScaleFactor ?? 1,
3518
- screenWidth: device.viewport.width,
3519
- screenHeight: device.viewport.height
3520
- });
3521
- }
3522
- if (device.hasTouch) {
3523
- await session.send("Emulation.setTouchEmulationEnabled", { enabled: true });
3795
+ await withPageScopedCdpClient({
3796
+ cdpUrl: opts.cdpUrl,
3797
+ page,
3798
+ targetId: opts.targetId,
3799
+ fn: async (send) => {
3800
+ const locale = device.locale;
3801
+ if (device.userAgent || locale) {
3802
+ await send("Emulation.setUserAgentOverride", {
3803
+ userAgent: device.userAgent ?? "",
3804
+ acceptLanguage: locale ?? void 0
3805
+ });
3806
+ }
3807
+ if (device.viewport) {
3808
+ await send("Emulation.setDeviceMetricsOverride", {
3809
+ mobile: Boolean(device.isMobile),
3810
+ width: device.viewport.width,
3811
+ height: device.viewport.height,
3812
+ deviceScaleFactor: device.deviceScaleFactor ?? 1,
3813
+ screenWidth: device.viewport.width,
3814
+ screenHeight: device.viewport.height
3815
+ });
3816
+ }
3817
+ if (device.hasTouch) {
3818
+ await send("Emulation.setTouchEmulationEnabled", { enabled: true });
3819
+ }
3524
3820
  }
3525
- } finally {
3526
- await session.detach().catch(() => {
3527
- });
3528
- }
3821
+ });
3529
3822
  }
3530
3823
  async function setExtraHTTPHeadersViaPlaywright(opts) {
3531
3824
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
@@ -3577,18 +3870,19 @@ async function setLocaleViaPlaywright(opts) {
3577
3870
  ensurePageState(page);
3578
3871
  const locale = String(opts.locale ?? "").trim();
3579
3872
  if (!locale) throw new Error("locale is required");
3580
- const session = await page.context().newCDPSession(page);
3581
- try {
3582
- try {
3583
- await session.send("Emulation.setLocaleOverride", { locale });
3584
- } catch (err) {
3585
- if (String(err).includes("Another locale override is already in effect")) return;
3586
- throw err;
3873
+ await withPageScopedCdpClient({
3874
+ cdpUrl: opts.cdpUrl,
3875
+ page,
3876
+ targetId: opts.targetId,
3877
+ fn: async (send) => {
3878
+ try {
3879
+ await send("Emulation.setLocaleOverride", { locale });
3880
+ } catch (err) {
3881
+ if (String(err).includes("Another locale override is already in effect")) return;
3882
+ throw err;
3883
+ }
3587
3884
  }
3588
- } finally {
3589
- await session.detach().catch(() => {
3590
- });
3591
- }
3885
+ });
3592
3886
  }
3593
3887
  async function setOfflineViaPlaywright(opts) {
3594
3888
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
@@ -3600,20 +3894,21 @@ async function setTimezoneViaPlaywright(opts) {
3600
3894
  ensurePageState(page);
3601
3895
  const timezoneId = String(opts.timezoneId ?? "").trim();
3602
3896
  if (!timezoneId) throw new Error("timezoneId is required");
3603
- const session = await page.context().newCDPSession(page);
3604
- try {
3605
- try {
3606
- await session.send("Emulation.setTimezoneOverride", { timezoneId });
3607
- } catch (err) {
3608
- const msg = String(err);
3609
- if (msg.includes("Timezone override is already in effect")) return;
3610
- if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
3611
- throw err;
3897
+ await withPageScopedCdpClient({
3898
+ cdpUrl: opts.cdpUrl,
3899
+ page,
3900
+ targetId: opts.targetId,
3901
+ fn: async (send) => {
3902
+ try {
3903
+ await send("Emulation.setTimezoneOverride", { timezoneId });
3904
+ } catch (err) {
3905
+ const msg = String(err);
3906
+ if (msg.includes("Timezone override is already in effect")) return;
3907
+ if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
3908
+ throw err;
3909
+ }
3612
3910
  }
3613
- } finally {
3614
- await session.detach().catch(() => {
3615
- });
3616
- }
3911
+ });
3617
3912
  }
3618
3913
 
3619
3914
  // src/capture/screenshot.ts
@@ -3636,47 +3931,65 @@ async function screenshotWithLabelsViaPlaywright(opts) {
3636
3931
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3637
3932
  ensurePageState(page);
3638
3933
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
3639
- const maxLabels = opts.maxLabels ?? 50;
3934
+ const maxLabels = typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels) ? Math.max(1, Math.floor(opts.maxLabels)) : 150;
3640
3935
  const type = opts.type ?? "png";
3641
3936
  const refs = opts.refs.slice(0, maxLabels);
3642
3937
  const skipped = opts.refs.slice(maxLabels);
3938
+ const viewport = await page.evaluate(() => ({
3939
+ width: window.innerWidth || 0,
3940
+ height: window.innerHeight || 0
3941
+ }));
3643
3942
  const labels = [];
3644
3943
  for (let i = 0; i < refs.length; i++) {
3645
3944
  const ref = refs[i];
3646
3945
  try {
3647
3946
  const locator = refLocator(page, ref);
3648
3947
  const box = await locator.boundingBox({ timeout: 2e3 });
3649
- if (box) {
3650
- labels.push({ ref, index: i + 1, box });
3651
- } else {
3948
+ if (!box) {
3652
3949
  skipped.push(ref);
3950
+ continue;
3653
3951
  }
3952
+ const x1 = box.x + box.width;
3953
+ const y1 = box.y + box.height;
3954
+ if (x1 < 0 || box.x > viewport.width || y1 < 0 || box.y > viewport.height) {
3955
+ skipped.push(ref);
3956
+ continue;
3957
+ }
3958
+ labels.push({ ref, index: i + 1, box });
3654
3959
  } catch {
3655
3960
  skipped.push(ref);
3656
3961
  }
3657
3962
  }
3658
- await page.evaluate((labelData) => {
3659
- const container = document.createElement("div");
3660
- container.id = "__browserclaw_labels__";
3661
- container.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647;";
3662
- for (const { index, box } of labelData) {
3663
- const border = document.createElement("div");
3664
- border.style.cssText = `position:absolute;left:${box.x}px;top:${box.y}px;width:${box.width}px;height:${box.height}px;border:2px solid #FF4500;box-sizing:border-box;`;
3665
- container.appendChild(border);
3666
- const badge = document.createElement("div");
3667
- badge.textContent = String(index);
3668
- badge.style.cssText = `position:absolute;left:${box.x}px;top:${Math.max(0, box.y - 18)}px;background:#FF4500;color:#fff;font:bold 12px/16px monospace;padding:0 4px;border-radius:2px;`;
3669
- container.appendChild(badge);
3963
+ try {
3964
+ if (labels.length > 0) {
3965
+ await page.evaluate((labelData) => {
3966
+ document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => el.remove());
3967
+ const container = document.createElement("div");
3968
+ container.setAttribute("data-browserclaw-labels", "1");
3969
+ container.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647;";
3970
+ for (const { index, box } of labelData) {
3971
+ const border = document.createElement("div");
3972
+ border.style.cssText = `position:absolute;left:${box.x}px;top:${box.y}px;width:${box.width}px;height:${box.height}px;border:2px solid #FF4500;box-sizing:border-box;`;
3973
+ container.appendChild(border);
3974
+ const badge = document.createElement("div");
3975
+ badge.textContent = String(index);
3976
+ badge.style.cssText = `position:absolute;left:${box.x}px;top:${Math.max(0, box.y - 18)}px;background:#FF4500;color:#fff;font:bold 12px/16px monospace;padding:0 4px;border-radius:2px;`;
3977
+ container.appendChild(badge);
3978
+ }
3979
+ document.documentElement.appendChild(container);
3980
+ }, labels.map((l) => ({ index: l.index, box: l.box })));
3670
3981
  }
3671
- document.body.appendChild(container);
3672
- }, labels.map((l) => ({ index: l.index, box: l.box })));
3673
- const buffer = await page.screenshot({ type });
3674
- await page.evaluate(() => {
3675
- const el = document.getElementById("__browserclaw_labels__");
3676
- if (el) el.remove();
3677
- }).catch(() => {
3678
- });
3679
- return { buffer, labels, skipped };
3982
+ return {
3983
+ buffer: await page.screenshot({ type }),
3984
+ labels,
3985
+ skipped
3986
+ };
3987
+ } finally {
3988
+ await page.evaluate(() => {
3989
+ document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => el.remove());
3990
+ }).catch(() => {
3991
+ });
3992
+ }
3680
3993
  }
3681
3994
 
3682
3995
  // src/capture/pdf.ts
@@ -3720,15 +4033,33 @@ async function traceStopViaPlaywright(opts) {
3720
4033
  }
3721
4034
 
3722
4035
  // src/capture/response.ts
4036
+ function matchUrlPattern(pattern, url) {
4037
+ if (!pattern || !url) return false;
4038
+ if (pattern === url) return true;
4039
+ if (pattern.includes("*")) {
4040
+ const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
4041
+ try {
4042
+ return new RegExp(`^${escaped}$`).test(url);
4043
+ } catch {
4044
+ return false;
4045
+ }
4046
+ }
4047
+ return url.includes(pattern);
4048
+ }
3723
4049
  async function responseBodyViaPlaywright(opts) {
3724
4050
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
3725
4051
  ensurePageState(page);
3726
4052
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
3727
- const response = await page.waitForResponse(opts.url, { timeout });
4053
+ const pattern = String(opts.url ?? "").trim();
4054
+ if (!pattern) throw new Error("url is required");
4055
+ const response = await page.waitForResponse(
4056
+ (resp) => matchUrlPattern(pattern, resp.url()),
4057
+ { timeout }
4058
+ );
3728
4059
  let body = await response.text();
3729
4060
  let truncated = false;
3730
- const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : void 0;
3731
- if (maxChars !== void 0 && body.length > maxChars) {
4061
+ const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
4062
+ if (body.length > maxChars) {
3732
4063
  body = body.slice(0, maxChars);
3733
4064
  truncated = true;
3734
4065
  }
@@ -4148,6 +4479,22 @@ var CrawlPage = class {
4148
4479
  timeoutMs: opts?.timeoutMs
4149
4480
  });
4150
4481
  }
4482
+ /**
4483
+ * Execute multiple browser actions in sequence.
4484
+ *
4485
+ * @param actions - Array of actions to execute
4486
+ * @param opts - Options (stopOnError: stop on first failure, default true)
4487
+ * @returns Array of per-action results
4488
+ */
4489
+ async batch(actions, opts) {
4490
+ return batchViaPlaywright({
4491
+ cdpUrl: this.cdpUrl,
4492
+ targetId: this.targetId,
4493
+ actions,
4494
+ stopOnError: opts?.stopOnError,
4495
+ evaluateEnabled: opts?.evaluateEnabled
4496
+ });
4497
+ }
4151
4498
  // ── Keyboard ─────────────────────────────────────────────────
4152
4499
  /**
4153
4500
  * Press a keyboard key or key combination.
@@ -4875,22 +5222,36 @@ var BrowserClaw = class _BrowserClaw {
4875
5222
  };
4876
5223
 
4877
5224
  exports.BrowserClaw = BrowserClaw;
5225
+ exports.BrowserTabNotFoundError = BrowserTabNotFoundError;
4878
5226
  exports.CrawlPage = CrawlPage;
4879
5227
  exports.InvalidBrowserNavigationUrlError = InvalidBrowserNavigationUrlError;
4880
5228
  exports.assertBrowserNavigationAllowed = assertBrowserNavigationAllowed;
4881
5229
  exports.assertBrowserNavigationRedirectChainAllowed = assertBrowserNavigationRedirectChainAllowed;
4882
5230
  exports.assertBrowserNavigationResultAllowed = assertBrowserNavigationResultAllowed;
5231
+ exports.assertSafeUploadPaths = assertSafeUploadPaths;
5232
+ exports.batchViaPlaywright = batchViaPlaywright;
4883
5233
  exports.createPinnedLookup = createPinnedLookup;
4884
5234
  exports.ensureContextState = ensureContextState;
5235
+ exports.executeSingleAction = executeSingleAction;
4885
5236
  exports.forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightForTarget;
4886
5237
  exports.getChromeWebSocketUrl = getChromeWebSocketUrl;
5238
+ exports.getRestoredPageForTarget = getRestoredPageForTarget;
4887
5239
  exports.isChromeCdpReady = isChromeCdpReady;
4888
5240
  exports.isChromeReachable = isChromeReachable;
4889
5241
  exports.normalizeCdpHttpBaseForJsonEndpoints = normalizeCdpHttpBaseForJsonEndpoints;
5242
+ exports.parseRoleRef = parseRoleRef;
5243
+ exports.requireRef = requireRef;
5244
+ exports.requireRefOrSelector = requireRefOrSelector;
4890
5245
  exports.requiresInspectableBrowserNavigationRedirects = requiresInspectableBrowserNavigationRedirects;
5246
+ exports.resolveBoundedDelayMs = resolveBoundedDelayMs;
5247
+ exports.resolveInteractionTimeoutMs = resolveInteractionTimeoutMs;
5248
+ exports.resolvePageByTargetIdOrThrow = resolvePageByTargetIdOrThrow;
4891
5249
  exports.resolvePinnedHostnameWithPolicy = resolvePinnedHostnameWithPolicy;
5250
+ exports.resolveStrictExistingUploadPaths = resolveStrictExistingUploadPaths;
4892
5251
  exports.sanitizeUntrustedFileName = sanitizeUntrustedFileName;
4893
5252
  exports.withBrowserNavigationPolicy = withBrowserNavigationPolicy;
5253
+ exports.withPageScopedCdpClient = withPageScopedCdpClient;
5254
+ exports.withPlaywrightPageCdpSession = withPlaywrightPageCdpSession;
4894
5255
  exports.writeViaSiblingTempPath = writeViaSiblingTempPath;
4895
5256
  //# sourceMappingURL=index.cjs.map
4896
5257
  //# sourceMappingURL=index.cjs.map