browserclaw 0.4.2 → 0.5.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.
package/dist/index.cjs CHANGED
@@ -7,14 +7,36 @@ var net = require('net');
7
7
  var child_process = require('child_process');
8
8
  var playwrightCore = require('playwright-core');
9
9
  var promises = require('dns/promises');
10
+ var dns = require('dns');
10
11
  var promises$1 = require('fs/promises');
12
+ var crypto = require('crypto');
13
+ var ipaddr = require('ipaddr.js');
11
14
 
12
15
  function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
13
16
 
17
+ function _interopNamespace(e) {
18
+ if (e && e.__esModule) return e;
19
+ var n = Object.create(null);
20
+ if (e) {
21
+ Object.keys(e).forEach(function (k) {
22
+ if (k !== 'default') {
23
+ var d = Object.getOwnPropertyDescriptor(e, k);
24
+ Object.defineProperty(n, k, d.get ? d : {
25
+ enumerable: true,
26
+ get: function () { return e[k]; }
27
+ });
28
+ }
29
+ });
30
+ }
31
+ n.default = e;
32
+ return Object.freeze(n);
33
+ }
34
+
14
35
  var os__default = /*#__PURE__*/_interopDefault(os);
15
36
  var path__default = /*#__PURE__*/_interopDefault(path);
16
37
  var fs__default = /*#__PURE__*/_interopDefault(fs);
17
38
  var net__default = /*#__PURE__*/_interopDefault(net);
39
+ var ipaddr__namespace = /*#__PURE__*/_interopNamespace(ipaddr);
18
40
 
19
41
  // src/chrome-launcher.ts
20
42
  var CHROMIUM_BUNDLE_IDS = /* @__PURE__ */ new Set([
@@ -624,8 +646,31 @@ async function stopChrome(running, timeoutMs = 2500) {
624
646
  var cached = null;
625
647
  var connectingByUrl = /* @__PURE__ */ new Map();
626
648
  var pageStates = /* @__PURE__ */ new WeakMap();
649
+ var contextStates = /* @__PURE__ */ new WeakMap();
627
650
  var observedContexts = /* @__PURE__ */ new WeakSet();
628
651
  var observedPages = /* @__PURE__ */ new WeakSet();
652
+ var nextUploadArmId = 0;
653
+ var nextDialogArmId = 0;
654
+ var nextDownloadArmId = 0;
655
+ function bumpUploadArmId() {
656
+ nextUploadArmId += 1;
657
+ return nextUploadArmId;
658
+ }
659
+ function bumpDialogArmId() {
660
+ nextDialogArmId += 1;
661
+ return nextDialogArmId;
662
+ }
663
+ function bumpDownloadArmId() {
664
+ nextDownloadArmId += 1;
665
+ return nextDownloadArmId;
666
+ }
667
+ function ensureContextState(context) {
668
+ const existing = contextStates.get(context);
669
+ if (existing) return existing;
670
+ const state = { traceActive: false };
671
+ contextStates.set(context, state);
672
+ return state;
673
+ }
629
674
  var roleRefsByTarget = /* @__PURE__ */ new Map();
630
675
  var MAX_ROLE_REFS_CACHE = 50;
631
676
  var MAX_CONSOLE_MESSAGES = 500;
@@ -645,7 +690,10 @@ function ensurePageState(page) {
645
690
  errors: [],
646
691
  requests: [],
647
692
  requestIds: /* @__PURE__ */ new WeakMap(),
648
- nextRequestId: 0
693
+ nextRequestId: 0,
694
+ armIdUpload: 0,
695
+ armIdDialog: 0,
696
+ armIdDownload: 0
649
697
  };
650
698
  pageStates.set(page, state);
651
699
  if (!observedPages.has(page)) {
@@ -722,6 +770,7 @@ function applyStealthToPage(page) {
722
770
  function observeContext(context) {
723
771
  if (observedContexts.has(context)) return;
724
772
  observedContexts.add(context);
773
+ ensureContextState(context);
725
774
  context.addInitScript(STEALTH_SCRIPT).catch((e) => {
726
775
  if (process.env.DEBUG) console.warn("[browserclaw] stealth initScript failed:", e.message);
727
776
  });
@@ -817,6 +866,35 @@ async function disconnectBrowser() {
817
866
  if (cur) await cur.browser.close().catch(() => {
818
867
  });
819
868
  }
869
+ async function forceDisconnectPlaywrightForTarget(opts) {
870
+ const normalized = normalizeCdpUrl(opts.cdpUrl);
871
+ const cur = cached;
872
+ if (!cur || cur.cdpUrl !== normalized) return;
873
+ cached = null;
874
+ connectingByUrl.delete(normalized);
875
+ const targetId = opts.targetId?.trim() || "";
876
+ if (targetId) {
877
+ try {
878
+ const pages = await getAllPages(cur.browser);
879
+ for (const page of pages) {
880
+ const tid = await pageTargetId(page).catch(() => null);
881
+ if (tid === targetId) {
882
+ const session = await page.context().newCDPSession(page);
883
+ try {
884
+ await session.send("Runtime.terminateExecution");
885
+ } finally {
886
+ await session.detach().catch(() => {
887
+ });
888
+ }
889
+ break;
890
+ }
891
+ }
892
+ } catch {
893
+ }
894
+ }
895
+ cur.browser.close().catch(() => {
896
+ });
897
+ }
820
898
  async function getAllPages(browser) {
821
899
  return browser.contexts().flatMap((c) => c.pages());
822
900
  }
@@ -1338,6 +1416,11 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
1338
1416
  var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
1339
1417
  var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
1340
1418
  var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
1419
+ var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
1420
+ "localhost",
1421
+ "localhost.localdomain",
1422
+ "metadata.google.internal"
1423
+ ]);
1341
1424
  function isAllowedNonNetworkNavigationUrl(parsed) {
1342
1425
  return SAFE_NON_NETWORK_URLS.has(parsed.href);
1343
1426
  }
@@ -1351,6 +1434,218 @@ function hasProxyEnvConfigured(env = process.env) {
1351
1434
  }
1352
1435
  return false;
1353
1436
  }
1437
+ function normalizeHostname(hostname) {
1438
+ let h = String(hostname ?? "").trim().toLowerCase();
1439
+ if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
1440
+ if (h.endsWith(".")) h = h.slice(0, -1);
1441
+ return h;
1442
+ }
1443
+ function isBlockedHostnameNormalized(normalized) {
1444
+ if (BLOCKED_HOSTNAMES.has(normalized)) return true;
1445
+ return normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal");
1446
+ }
1447
+ function isStrictDecimalOctet(part) {
1448
+ if (!/^[0-9]+$/.test(part)) return false;
1449
+ const n = parseInt(part, 10);
1450
+ if (n < 0 || n > 255) return false;
1451
+ if (String(n) !== part) return false;
1452
+ return true;
1453
+ }
1454
+ function isUnsupportedIPv4Literal(ip) {
1455
+ if (/^[0-9]+$/.test(ip)) return true;
1456
+ const parts = ip.split(".");
1457
+ if (parts.length !== 4) return true;
1458
+ if (!parts.every(isStrictDecimalOctet)) return true;
1459
+ return false;
1460
+ }
1461
+ var BLOCKED_IPV4_RANGES = /* @__PURE__ */ new Set([
1462
+ "unspecified",
1463
+ "broadcast",
1464
+ "multicast",
1465
+ "linkLocal",
1466
+ "loopback",
1467
+ "carrierGradeNat",
1468
+ "private",
1469
+ "reserved"
1470
+ ]);
1471
+ var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set([
1472
+ "unspecified",
1473
+ "loopback",
1474
+ "linkLocal",
1475
+ "uniqueLocal",
1476
+ "multicast"
1477
+ ]);
1478
+ var RFC2544_BENCHMARK_PREFIX = [ipaddr__namespace.IPv4.parse("198.18.0.0"), 15];
1479
+ function isBlockedSpecialUseIpv4Address(address, opts) {
1480
+ const inRfc2544 = address.match(RFC2544_BENCHMARK_PREFIX);
1481
+ if (inRfc2544 && opts?.allowRfc2544BenchmarkRange === true) return false;
1482
+ return BLOCKED_IPV4_RANGES.has(address.range()) || inRfc2544;
1483
+ }
1484
+ function isBlockedSpecialUseIpv6Address(address) {
1485
+ if (BLOCKED_IPV6_RANGES.has(address.range())) return true;
1486
+ return (address.parts[0] & 65472) === 65216;
1487
+ }
1488
+ function extractEmbeddedIpv4FromIpv6(v6, opts) {
1489
+ if (v6.isIPv4MappedAddress()) {
1490
+ return isBlockedSpecialUseIpv4Address(v6.toIPv4Address(), opts);
1491
+ }
1492
+ const parts = v6.parts;
1493
+ if (parts[0] === 100 && parts[1] === 65435 && parts[2] === 0 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0) {
1494
+ const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
1495
+ try {
1496
+ return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
1497
+ } catch {
1498
+ return true;
1499
+ }
1500
+ }
1501
+ if (parts[0] === 100 && parts[1] === 65435 && parts[2] === 1) {
1502
+ const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
1503
+ try {
1504
+ return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
1505
+ } catch {
1506
+ return true;
1507
+ }
1508
+ }
1509
+ if (parts[0] === 8194) {
1510
+ const ip4str = `${parts[1] >> 8 & 255}.${parts[1] & 255}.${parts[2] >> 8 & 255}.${parts[2] & 255}`;
1511
+ try {
1512
+ return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
1513
+ } catch {
1514
+ return true;
1515
+ }
1516
+ }
1517
+ if (parts[0] === 8193 && parts[1] === 0) {
1518
+ const hiXored = parts[6] ^ 65535;
1519
+ const loXored = parts[7] ^ 65535;
1520
+ const ip4str = `${hiXored >> 8 & 255}.${hiXored & 255}.${loXored >> 8 & 255}.${loXored & 255}`;
1521
+ try {
1522
+ return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
1523
+ } catch {
1524
+ return true;
1525
+ }
1526
+ }
1527
+ return null;
1528
+ }
1529
+ function isPrivateIpAddress(address, opts) {
1530
+ let normalized = address.trim().toLowerCase();
1531
+ if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1);
1532
+ if (!normalized) return false;
1533
+ try {
1534
+ const parsed = ipaddr__namespace.parse(normalized);
1535
+ if (parsed.kind() === "ipv4") {
1536
+ return isBlockedSpecialUseIpv4Address(parsed, opts);
1537
+ }
1538
+ const v6 = parsed;
1539
+ if (isBlockedSpecialUseIpv6Address(v6)) return true;
1540
+ const embeddedV4 = extractEmbeddedIpv4FromIpv6(v6, opts);
1541
+ if (embeddedV4 !== null) return embeddedV4;
1542
+ return false;
1543
+ } catch {
1544
+ }
1545
+ if (!normalized.includes(":") && isUnsupportedIPv4Literal(normalized)) return true;
1546
+ if (normalized.includes(":")) return true;
1547
+ return false;
1548
+ }
1549
+ function dedupeAndPreferIpv4(results) {
1550
+ const seen = /* @__PURE__ */ new Set();
1551
+ const ipv4 = [];
1552
+ const ipv6 = [];
1553
+ for (const r of results) {
1554
+ if (seen.has(r.address)) continue;
1555
+ seen.add(r.address);
1556
+ if (r.family === 4) ipv4.push(r.address);
1557
+ else ipv6.push(r.address);
1558
+ }
1559
+ return [...ipv4, ...ipv6];
1560
+ }
1561
+ function createPinnedLookup(params) {
1562
+ const normalizedHost = normalizeHostname(params.hostname);
1563
+ const fallback = params.fallback ?? dns.lookup;
1564
+ const records = params.addresses.map((address) => ({
1565
+ address,
1566
+ family: address.includes(":") ? 6 : 4
1567
+ }));
1568
+ let index = 0;
1569
+ return ((host, options, callback) => {
1570
+ const cb = typeof options === "function" ? options : callback;
1571
+ if (!cb) return;
1572
+ const normalized = normalizeHostname(host);
1573
+ if (!normalized || normalized !== normalizedHost) {
1574
+ if (typeof options === "function" || options === void 0) return fallback(host, cb);
1575
+ return fallback(host, options, cb);
1576
+ }
1577
+ const opts = typeof options === "object" && options !== null ? options : {};
1578
+ const requestedFamily = typeof options === "number" ? options : typeof opts.family === "number" ? opts.family : 0;
1579
+ const candidates = requestedFamily === 4 || requestedFamily === 6 ? records.filter((entry) => entry.family === requestedFamily) : records;
1580
+ const usable = candidates.length > 0 ? candidates : records;
1581
+ if (opts.all) {
1582
+ cb(null, usable);
1583
+ return;
1584
+ }
1585
+ const chosen = usable[index % usable.length];
1586
+ index += 1;
1587
+ cb(null, chosen.address, chosen.family);
1588
+ });
1589
+ }
1590
+ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
1591
+ const normalized = normalizeHostname(hostname);
1592
+ if (!normalized) throw new InvalidBrowserNavigationUrlError(`Invalid hostname: "${hostname}"`);
1593
+ const allowPrivateNetwork = isPrivateNetworkAllowedByPolicy(params.policy);
1594
+ const allowedHostnames = [
1595
+ ...params.policy?.allowedHostnames ?? [],
1596
+ ...params.policy?.hostnameAllowlist ?? []
1597
+ ].map((h) => normalizeHostname(h));
1598
+ const isExplicitlyAllowed = allowedHostnames.some((h) => h === normalized);
1599
+ const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitlyAllowed;
1600
+ if (!skipPrivateNetworkChecks) {
1601
+ if (isBlockedHostnameNormalized(normalized)) {
1602
+ throw new InvalidBrowserNavigationUrlError(
1603
+ `Navigation to internal/loopback address blocked: "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
1604
+ );
1605
+ }
1606
+ const ipOpts = { allowRfc2544BenchmarkRange: params.policy?.allowRfc2544BenchmarkRange };
1607
+ if (isPrivateIpAddress(normalized, ipOpts)) {
1608
+ throw new InvalidBrowserNavigationUrlError(
1609
+ `Navigation to internal/loopback address blocked: "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
1610
+ );
1611
+ }
1612
+ }
1613
+ const lookupFn = params.lookupFn ?? promises.lookup;
1614
+ let results;
1615
+ try {
1616
+ results = await lookupFn(normalized, { all: true });
1617
+ } catch {
1618
+ throw new InvalidBrowserNavigationUrlError(
1619
+ `Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
1620
+ );
1621
+ }
1622
+ if (!results || results.length === 0) {
1623
+ throw new InvalidBrowserNavigationUrlError(
1624
+ `Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
1625
+ );
1626
+ }
1627
+ if (!skipPrivateNetworkChecks) {
1628
+ const ipOpts = { allowRfc2544BenchmarkRange: params.policy?.allowRfc2544BenchmarkRange };
1629
+ for (const r of results) {
1630
+ if (isPrivateIpAddress(r.address, ipOpts)) {
1631
+ throw new InvalidBrowserNavigationUrlError(
1632
+ `Navigation to internal/loopback address blocked: "${hostname}" resolves to "${r.address}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
1633
+ );
1634
+ }
1635
+ }
1636
+ }
1637
+ const addresses = dedupeAndPreferIpv4(results);
1638
+ if (addresses.length === 0) {
1639
+ throw new InvalidBrowserNavigationUrlError(
1640
+ `Navigation to internal/loopback address blocked: unable to resolve "${hostname}".`
1641
+ );
1642
+ }
1643
+ return {
1644
+ hostname: normalized,
1645
+ addresses,
1646
+ lookup: createPinnedLookup({ hostname: normalized, addresses })
1647
+ };
1648
+ }
1354
1649
  async function assertBrowserNavigationAllowed(opts) {
1355
1650
  const rawUrl = String(opts.url ?? "").trim();
1356
1651
  let parsed;
@@ -1370,20 +1665,10 @@ async function assertBrowserNavigationAllowed(opts) {
1370
1665
  }
1371
1666
  const policy = opts.ssrfPolicy;
1372
1667
  if (policy?.dangerouslyAllowPrivateNetwork ?? policy?.allowPrivateNetwork ?? true) return;
1373
- const allowedHostnames = [
1374
- ...policy?.allowedHostnames ?? [],
1375
- ...policy?.hostnameAllowlist ?? []
1376
- ];
1377
- if (allowedHostnames.length) {
1378
- const hostname = parsed.hostname.toLowerCase();
1379
- if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
1380
- }
1381
- const ipOpts = { allowRfc2544BenchmarkRange: policy?.allowRfc2544BenchmarkRange };
1382
- if (await isInternalUrlResolved(rawUrl, opts.lookupFn, ipOpts)) {
1383
- throw new InvalidBrowserNavigationUrlError(
1384
- `Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
1385
- );
1386
- }
1668
+ await resolvePinnedHostnameWithPolicy(parsed.hostname, {
1669
+ lookupFn: opts.lookupFn,
1670
+ policy
1671
+ });
1387
1672
  }
1388
1673
  async function assertSafeOutputPath(path2, allowedRoots) {
1389
1674
  if (!path2 || typeof path2 !== "string") {
@@ -1426,111 +1711,6 @@ async function assertSafeOutputPath(path2, allowedRoots) {
1426
1711
  }
1427
1712
  }
1428
1713
  }
1429
- function expandIPv6(ip) {
1430
- let normalized = ip;
1431
- const v4Match = normalized.match(/^(.+:)(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3})$/);
1432
- if (v4Match) {
1433
- const octets = v4Match[2].split(".").map(Number);
1434
- if (octets.some((o) => o > 255)) return null;
1435
- const hexHi = (octets[0] << 8 | octets[1]).toString(16).padStart(4, "0");
1436
- const hexLo = (octets[2] << 8 | octets[3]).toString(16).padStart(4, "0");
1437
- normalized = v4Match[1] + hexHi + ":" + hexLo;
1438
- }
1439
- const halves = normalized.split("::");
1440
- if (halves.length > 2) return null;
1441
- if (halves.length === 2) {
1442
- const left = halves[0] !== "" ? halves[0].split(":") : [];
1443
- const right = halves[1] !== "" ? halves[1].split(":") : [];
1444
- const needed = 8 - left.length - right.length;
1445
- if (needed < 0) return null;
1446
- const groups2 = [...left, ...Array(needed).fill("0"), ...right];
1447
- if (groups2.length !== 8) return null;
1448
- return groups2.map((g) => g.padStart(4, "0")).join(":");
1449
- }
1450
- const groups = normalized.split(":");
1451
- if (groups.length !== 8) return null;
1452
- return groups.map((g) => g.padStart(4, "0")).join(":");
1453
- }
1454
- function hexToIPv4(hiHex, loHex) {
1455
- const hi = parseInt(hiHex, 16);
1456
- const lo = parseInt(loHex, 16);
1457
- return `${hi >> 8 & 255}.${hi & 255}.${lo >> 8 & 255}.${lo & 255}`;
1458
- }
1459
- function extractEmbeddedIPv4(lower) {
1460
- if (lower.startsWith("::ffff:")) {
1461
- return lower.slice(7);
1462
- }
1463
- const expanded = expandIPv6(lower);
1464
- if (expanded === null) return "";
1465
- const groups = expanded.split(":");
1466
- if (groups.length !== 8) return "";
1467
- if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0000" && groups[3] === "0000" && groups[4] === "0000" && groups[5] === "0000") {
1468
- return hexToIPv4(groups[6], groups[7]);
1469
- }
1470
- if (groups[0] === "0064" && groups[1] === "ff9b" && groups[2] === "0001") {
1471
- return hexToIPv4(groups[6], groups[7]);
1472
- }
1473
- if (groups[0] === "2002") {
1474
- return hexToIPv4(groups[1], groups[2]);
1475
- }
1476
- if (groups[0] === "2001" && groups[1] === "0000") {
1477
- const hiXored = (parseInt(groups[6], 16) ^ 65535).toString(16).padStart(4, "0");
1478
- const loXored = (parseInt(groups[7], 16) ^ 65535).toString(16).padStart(4, "0");
1479
- return hexToIPv4(hiXored, loXored);
1480
- }
1481
- return null;
1482
- }
1483
- function isStrictDecimalOctet(part) {
1484
- if (!/^[0-9]+$/.test(part)) return false;
1485
- const n = parseInt(part, 10);
1486
- if (n < 0 || n > 255) return false;
1487
- if (String(n) !== part) return false;
1488
- return true;
1489
- }
1490
- function isUnsupportedIPv4Literal(ip) {
1491
- if (/^[0-9]+$/.test(ip)) return true;
1492
- const parts = ip.split(".");
1493
- if (parts.length !== 4) return true;
1494
- if (!parts.every(isStrictDecimalOctet)) return true;
1495
- return false;
1496
- }
1497
- function isInternalIP(ip, opts) {
1498
- if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
1499
- if (/^127\./.test(ip)) return true;
1500
- if (/^10\./.test(ip)) return true;
1501
- if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
1502
- if (/^192\.168\./.test(ip)) return true;
1503
- if (/^169\.254\./.test(ip)) return true;
1504
- if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
1505
- if (ip === "0.0.0.0") return true;
1506
- if (!opts?.allowRfc2544BenchmarkRange && /^198\.1[89]\./.test(ip)) return true;
1507
- const lower = ip.toLowerCase();
1508
- if (lower === "::1") return true;
1509
- if (lower.startsWith("fe80:")) return true;
1510
- if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
1511
- if (lower.startsWith("ff")) return true;
1512
- const embedded = extractEmbeddedIPv4(lower);
1513
- if (embedded !== null) {
1514
- if (embedded === "") return true;
1515
- return isInternalIP(embedded, opts);
1516
- }
1517
- return false;
1518
- }
1519
- function isInternalUrl(url, opts) {
1520
- let parsed;
1521
- try {
1522
- parsed = new URL(url);
1523
- } catch {
1524
- return true;
1525
- }
1526
- const hostname = parsed.hostname.toLowerCase();
1527
- if (hostname === "localhost") return true;
1528
- if (isInternalIP(hostname, opts)) return true;
1529
- if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
1530
- return true;
1531
- }
1532
- return false;
1533
- }
1534
1714
  async function assertSafeUploadPaths(paths) {
1535
1715
  for (const filePath of paths) {
1536
1716
  let stat;
@@ -1547,21 +1727,48 @@ async function assertSafeUploadPaths(paths) {
1547
1727
  }
1548
1728
  }
1549
1729
  }
1550
- async function isInternalUrlResolved(url, lookupFn = promises.lookup, opts) {
1551
- if (isInternalUrl(url, opts)) return true;
1552
- let parsed;
1553
- try {
1554
- parsed = new URL(url);
1555
- } catch {
1556
- return true;
1557
- }
1730
+ function sanitizeUntrustedFileName(fileName, fallbackName) {
1731
+ const trimmed = String(fileName ?? "").trim();
1732
+ if (!trimmed) return fallbackName;
1733
+ let base = path.posix.basename(trimmed);
1734
+ base = path.win32.basename(base);
1735
+ let cleaned = "";
1736
+ for (let i = 0; i < base.length; i++) {
1737
+ const code = base.charCodeAt(i);
1738
+ if (code < 32 || code === 127) continue;
1739
+ cleaned += base[i];
1740
+ }
1741
+ base = cleaned.trim();
1742
+ if (!base || base === "." || base === "..") return fallbackName;
1743
+ if (base.length > 200) base = base.slice(0, 200);
1744
+ return base;
1745
+ }
1746
+ function buildSiblingTempPath(targetPath) {
1747
+ const id = crypto.randomUUID();
1748
+ const safeTail = sanitizeUntrustedFileName(path.basename(targetPath), "output.bin");
1749
+ return path.join(path.dirname(targetPath), `.browserclaw-output-${id}-${safeTail}.part`);
1750
+ }
1751
+ async function writeViaSiblingTempPath(params) {
1752
+ const rootDir = await promises$1.realpath(path.resolve(params.rootDir)).catch(() => path.resolve(params.rootDir));
1753
+ const requestedTargetPath = path.resolve(params.targetPath);
1754
+ const targetPath = await promises$1.realpath(path.dirname(requestedTargetPath)).then((realDir) => path.join(realDir, path.basename(requestedTargetPath))).catch(() => requestedTargetPath);
1755
+ const relativeTargetPath = path.relative(rootDir, targetPath);
1756
+ if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${path.sep}`) || isAbsolute(relativeTargetPath)) {
1757
+ throw new Error("Target path is outside the allowed root");
1758
+ }
1759
+ const tempPath = buildSiblingTempPath(targetPath);
1760
+ let renameSucceeded = false;
1558
1761
  try {
1559
- const { address } = await lookupFn(parsed.hostname);
1560
- if (isInternalIP(address, opts)) return true;
1561
- } catch {
1562
- return true;
1762
+ await params.writeTemp(tempPath);
1763
+ await promises$1.rename(tempPath, targetPath);
1764
+ renameSucceeded = true;
1765
+ } finally {
1766
+ if (!renameSucceeded) await promises$1.rm(tempPath, { force: true }).catch(() => {
1767
+ });
1563
1768
  }
1564
- return false;
1769
+ }
1770
+ function isAbsolute(p) {
1771
+ return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
1565
1772
  }
1566
1773
  async function assertBrowserNavigationResultAllowed(opts) {
1567
1774
  const rawUrl = String(opts.url ?? "").trim();
@@ -1669,13 +1876,12 @@ async function fillFormViaPlaywright(opts) {
1669
1876
  ensurePageState(page);
1670
1877
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1671
1878
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4);
1672
- for (let i = 0; i < opts.fields.length; i++) {
1673
- const field = opts.fields[i];
1879
+ for (const field of opts.fields) {
1674
1880
  const ref = field.ref.trim();
1675
1881
  const type = (typeof field.type === "string" ? field.type.trim() : "") || "text";
1676
1882
  const rawValue = field.value;
1677
- const value = rawValue == null ? "" : String(rawValue);
1678
- if (!ref) throw new Error(`fill(): field at index ${i} has empty ref`);
1883
+ const value = typeof rawValue === "string" ? rawValue : typeof rawValue === "number" || typeof rawValue === "boolean" ? String(rawValue) : "";
1884
+ if (!ref) continue;
1679
1885
  const locator = refLocator(page, ref);
1680
1886
  if (type === "checkbox" || type === "radio") {
1681
1887
  const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
@@ -1719,61 +1925,78 @@ async function setInputFilesViaPlaywright(opts) {
1719
1925
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1720
1926
  ensurePageState(page);
1721
1927
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1722
- const locator = opts.ref ? refLocator(page, opts.ref) : opts.element ? page.locator(opts.element).first() : null;
1723
- if (!locator) throw new Error("Either ref or element is required for setInputFiles");
1928
+ if (!opts.paths.length) throw new Error("paths are required");
1929
+ const inputRef = typeof opts.ref === "string" ? opts.ref.trim() : "";
1930
+ const element = typeof opts.element === "string" ? opts.element.trim() : "";
1931
+ if (inputRef && element) throw new Error("ref and element are mutually exclusive");
1932
+ if (!inputRef && !element) throw new Error("Either ref or element is required for setInputFiles");
1933
+ const locator = inputRef ? refLocator(page, inputRef) : page.locator(element).first();
1724
1934
  await assertSafeUploadPaths(opts.paths);
1725
1935
  try {
1726
1936
  await locator.setInputFiles(opts.paths);
1727
1937
  } catch (err) {
1728
- throw toAIFriendlyError(err, opts.ref ?? opts.element ?? "unknown");
1938
+ throw toAIFriendlyError(err, inputRef || element);
1939
+ }
1940
+ try {
1941
+ const handle = await locator.elementHandle();
1942
+ if (handle) {
1943
+ await handle.evaluate((el) => {
1944
+ el.dispatchEvent(new Event("input", { bubbles: true }));
1945
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1946
+ });
1947
+ }
1948
+ } catch {
1729
1949
  }
1730
1950
  }
1731
1951
  async function armDialogViaPlaywright(opts) {
1732
1952
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1733
- ensurePageState(page);
1734
- const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
1735
- return new Promise((resolve2, reject) => {
1736
- const timer = setTimeout(() => {
1737
- page.removeListener("dialog", handler);
1738
- reject(new Error(`No dialog appeared within ${timeout}ms`));
1739
- }, timeout);
1740
- const handler = async (dialog) => {
1741
- clearTimeout(timer);
1742
- try {
1743
- if (opts.accept) {
1744
- await dialog.accept(opts.promptText);
1745
- } else {
1746
- await dialog.dismiss();
1747
- }
1748
- resolve2();
1749
- } catch (err) {
1750
- reject(err);
1751
- }
1752
- };
1753
- page.once("dialog", handler);
1953
+ const state = ensurePageState(page);
1954
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
1955
+ state.armIdDialog = bumpDialogArmId();
1956
+ const armId = state.armIdDialog;
1957
+ page.waitForEvent("dialog", { timeout }).then(async (dialog) => {
1958
+ if (state.armIdDialog !== armId) return;
1959
+ if (opts.accept) await dialog.accept(opts.promptText);
1960
+ else await dialog.dismiss();
1961
+ }).catch(() => {
1754
1962
  });
1755
1963
  }
1756
1964
  async function armFileUploadViaPlaywright(opts) {
1757
1965
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1758
- ensurePageState(page);
1759
- const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
1760
- return new Promise((resolve2, reject) => {
1761
- const timer = setTimeout(() => {
1762
- page.removeListener("filechooser", handler);
1763
- reject(new Error(`No file chooser appeared within ${timeout}ms`));
1764
- }, timeout);
1765
- const handler = async (fc) => {
1766
- clearTimeout(timer);
1966
+ const state = ensurePageState(page);
1967
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
1968
+ state.armIdUpload = bumpUploadArmId();
1969
+ const armId = state.armIdUpload;
1970
+ page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
1971
+ if (state.armIdUpload !== armId) return;
1972
+ if (!opts.paths?.length) {
1767
1973
  try {
1768
- const paths = opts.paths ?? [];
1769
- if (paths.length > 0) await assertSafeUploadPaths(paths);
1770
- await fc.setFiles(paths);
1771
- resolve2();
1772
- } catch (err) {
1773
- reject(err);
1974
+ await page.keyboard.press("Escape");
1975
+ } catch {
1774
1976
  }
1775
- };
1776
- page.once("filechooser", handler);
1977
+ return;
1978
+ }
1979
+ try {
1980
+ await assertSafeUploadPaths(opts.paths);
1981
+ } catch {
1982
+ try {
1983
+ await page.keyboard.press("Escape");
1984
+ } catch {
1985
+ }
1986
+ return;
1987
+ }
1988
+ await fileChooser.setFiles(opts.paths);
1989
+ try {
1990
+ const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
1991
+ if (input) {
1992
+ await input.evaluate((el) => {
1993
+ el.dispatchEvent(new Event("input", { bubbles: true }));
1994
+ el.dispatchEvent(new Event("change", { bubbles: true }));
1995
+ });
1996
+ }
1997
+ } catch {
1998
+ }
1999
+ }).catch(() => {
1777
2000
  });
1778
2001
  }
1779
2002
 
@@ -1787,17 +2010,35 @@ async function pressKeyViaPlaywright(opts) {
1787
2010
  }
1788
2011
 
1789
2012
  // src/actions/navigation.ts
2013
+ function isRetryableNavigateError(err) {
2014
+ const msg = typeof err === "string" ? err.toLowerCase() : err instanceof Error ? err.message.toLowerCase() : "";
2015
+ return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
2016
+ }
1790
2017
  async function navigateViaPlaywright(opts) {
1791
2018
  const url = String(opts.url ?? "").trim();
1792
2019
  if (!url) throw new Error("url is required");
1793
2020
  const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
1794
- await assertBrowserNavigationAllowed({ url, ssrfPolicy: policy });
1795
- const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2021
+ await assertBrowserNavigationAllowed({ url, ...withBrowserNavigationPolicy(policy) });
2022
+ const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
2023
+ let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1796
2024
  ensurePageState(page);
1797
- const response = await page.goto(url, { timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
2025
+ const navigate = async () => await page.goto(url, { timeout });
2026
+ let response;
2027
+ try {
2028
+ response = await navigate();
2029
+ } catch (err) {
2030
+ if (!isRetryableNavigateError(err)) throw err;
2031
+ await forceDisconnectPlaywrightForTarget({
2032
+ cdpUrl: opts.cdpUrl,
2033
+ targetId: opts.targetId}).catch(() => {
2034
+ });
2035
+ page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2036
+ ensurePageState(page);
2037
+ response = await navigate();
2038
+ }
1798
2039
  await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...withBrowserNavigationPolicy(policy) });
1799
2040
  const finalUrl = page.url();
1800
- await assertBrowserNavigationResultAllowed({ url: finalUrl, ssrfPolicy: policy });
2041
+ await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
1801
2042
  return { url: finalUrl };
1802
2043
  }
1803
2044
  async function listPagesViaPlaywright(opts) {
@@ -1823,11 +2064,12 @@ async function createPageViaPlaywright(opts) {
1823
2064
  }
1824
2065
  const { browser } = await connectBrowser(opts.cdpUrl);
1825
2066
  const context = browser.contexts()[0] ?? await browser.newContext();
2067
+ ensureContextState(context);
1826
2068
  const page = await context.newPage();
1827
2069
  ensurePageState(page);
1828
2070
  if (targetUrl !== "about:blank") {
1829
2071
  const navigationPolicy = withBrowserNavigationPolicy(policy);
1830
- const response = await page.goto(targetUrl, { timeout: normalizeTimeoutMs(void 0, 2e4) });
2072
+ const response = await page.goto(targetUrl, { timeout: 3e4 }).catch(() => null);
1831
2073
  await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...navigationPolicy });
1832
2074
  await assertBrowserNavigationResultAllowed({ url: page.url(), ssrfPolicy: policy });
1833
2075
  }
@@ -1936,68 +2178,136 @@ async function evaluateInAllFramesViaPlaywright(opts) {
1936
2178
  }
1937
2179
  return results;
1938
2180
  }
2181
+ async function awaitEvalWithAbort(evalPromise, abortPromise) {
2182
+ if (!abortPromise) return await evalPromise;
2183
+ try {
2184
+ return await Promise.race([evalPromise, abortPromise]);
2185
+ } catch (err) {
2186
+ evalPromise.catch(() => {
2187
+ });
2188
+ throw err;
2189
+ }
2190
+ }
2191
+ var BROWSER_EVALUATOR = new Function("args", `
2192
+ "use strict";
2193
+ var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
2194
+ try {
2195
+ var candidate = eval("(" + fnBody + ")");
2196
+ var result = typeof candidate === "function" ? candidate() : candidate;
2197
+ if (result && typeof result.then === "function") {
2198
+ return Promise.race([
2199
+ result,
2200
+ new Promise(function(_, reject) {
2201
+ setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
2202
+ })
2203
+ ]);
2204
+ }
2205
+ return result;
2206
+ } catch (err) {
2207
+ throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
2208
+ }
2209
+ `);
2210
+ var ELEMENT_EVALUATOR = new Function("el", "args", `
2211
+ "use strict";
2212
+ var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
2213
+ try {
2214
+ var candidate = eval("(" + fnBody + ")");
2215
+ var result = typeof candidate === "function" ? candidate(el) : candidate;
2216
+ if (result && typeof result.then === "function") {
2217
+ return Promise.race([
2218
+ result,
2219
+ new Promise(function(_, reject) {
2220
+ setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
2221
+ })
2222
+ ]);
2223
+ }
2224
+ return result;
2225
+ } catch (err) {
2226
+ throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
2227
+ }
2228
+ `);
1939
2229
  async function evaluateViaPlaywright(opts) {
1940
2230
  const fnText = String(opts.fn ?? "").trim();
1941
2231
  if (!fnText) throw new Error("function is required");
1942
2232
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1943
2233
  ensurePageState(page);
1944
2234
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1945
- const timeout = opts.timeoutMs != null ? opts.timeoutMs : void 0;
1946
- if (opts.ref) {
1947
- const locator = refLocator(page, opts.ref);
1948
- return await locator.evaluate(
1949
- // eslint-disable-next-line no-eval
1950
- (el, fnBody) => {
1951
- try {
1952
- const candidate = (0, eval)("(" + fnBody + ")");
1953
- return typeof candidate === "function" ? candidate(el) : candidate;
1954
- } catch (err) {
1955
- throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
1956
- }
1957
- },
1958
- fnText,
1959
- { timeout }
2235
+ const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
2236
+ let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
2237
+ evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
2238
+ const signal = opts.signal;
2239
+ let abortListener;
2240
+ let abortReject;
2241
+ let abortPromise;
2242
+ if (signal) {
2243
+ abortPromise = new Promise((_, reject) => {
2244
+ abortReject = reject;
2245
+ });
2246
+ abortPromise.catch(() => {
2247
+ });
2248
+ }
2249
+ if (signal) {
2250
+ const disconnect = () => {
2251
+ forceDisconnectPlaywrightForTarget({
2252
+ cdpUrl: opts.cdpUrl,
2253
+ targetId: opts.targetId}).catch(() => {
2254
+ });
2255
+ };
2256
+ if (signal.aborted) {
2257
+ disconnect();
2258
+ throw signal.reason ?? new Error("aborted");
2259
+ }
2260
+ abortListener = () => {
2261
+ disconnect();
2262
+ abortReject?.(signal.reason ?? new Error("aborted"));
2263
+ };
2264
+ signal.addEventListener("abort", abortListener, { once: true });
2265
+ if (signal.aborted) {
2266
+ abortListener();
2267
+ throw signal.reason ?? new Error("aborted");
2268
+ }
2269
+ }
2270
+ try {
2271
+ if (opts.ref) {
2272
+ const locator = refLocator(page, opts.ref);
2273
+ return await awaitEvalWithAbort(
2274
+ locator.evaluate(ELEMENT_EVALUATOR, { fnBody: fnText, timeoutMs: evaluateTimeout }),
2275
+ abortPromise
2276
+ );
2277
+ }
2278
+ return await awaitEvalWithAbort(
2279
+ page.evaluate(BROWSER_EVALUATOR, { fnBody: fnText, timeoutMs: evaluateTimeout }),
2280
+ abortPromise
1960
2281
  );
2282
+ } finally {
2283
+ if (signal && abortListener) signal.removeEventListener("abort", abortListener);
1961
2284
  }
1962
- const evalPromise = page.evaluate(
1963
- // eslint-disable-next-line no-eval
1964
- (fnBody) => {
1965
- try {
1966
- const candidate = (0, eval)("(" + fnBody + ")");
1967
- return typeof candidate === "function" ? candidate() : candidate;
1968
- } catch (err) {
1969
- throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
1970
- }
1971
- },
1972
- fnText
1973
- );
1974
- if (!opts.signal) return evalPromise;
1975
- return Promise.race([
1976
- evalPromise,
1977
- new Promise((_, reject) => {
1978
- opts.signal.addEventListener("abort", () => reject(new Error("Evaluate aborted")), { once: true });
1979
- })
1980
- ]);
1981
2285
  }
1982
-
1983
- // src/actions/download.ts
1984
2286
  async function downloadViaPlaywright(opts) {
1985
2287
  await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
1986
2288
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1987
- ensurePageState(page);
2289
+ const state = ensurePageState(page);
1988
2290
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
1989
- const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
2291
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
1990
2292
  const locator = refLocator(page, opts.ref);
2293
+ state.armIdDownload = bumpDownloadArmId();
1991
2294
  try {
1992
2295
  const [download] = await Promise.all([
1993
2296
  page.waitForEvent("download", { timeout }),
1994
2297
  locator.click({ timeout })
1995
2298
  ]);
1996
- await download.saveAs(opts.path);
2299
+ const outPath = opts.path;
2300
+ await writeViaSiblingTempPath({
2301
+ rootDir: path.dirname(outPath),
2302
+ targetPath: outPath,
2303
+ writeTemp: async (tempPath) => {
2304
+ await download.saveAs(tempPath);
2305
+ }
2306
+ });
1997
2307
  return {
1998
2308
  url: download.url(),
1999
2309
  suggestedFilename: download.suggestedFilename(),
2000
- path: opts.path
2310
+ path: outPath
2001
2311
  };
2002
2312
  } catch (err) {
2003
2313
  throw toAIFriendlyError(err, opts.ref);
@@ -2006,11 +2316,17 @@ async function downloadViaPlaywright(opts) {
2006
2316
  async function waitForDownloadViaPlaywright(opts) {
2007
2317
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2008
2318
  ensurePageState(page);
2009
- const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
2319
+ const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
2010
2320
  const download = await page.waitForEvent("download", { timeout });
2011
2321
  const savePath = opts.path ?? download.suggestedFilename();
2012
2322
  await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
2013
- await download.saveAs(savePath);
2323
+ await writeViaSiblingTempPath({
2324
+ rootDir: path.dirname(savePath),
2325
+ targetPath: savePath,
2326
+ writeTemp: async (tempPath) => {
2327
+ await download.saveAs(tempPath);
2328
+ }
2329
+ });
2014
2330
  return {
2015
2331
  url: download.url(),
2016
2332
  suggestedFilename: download.suggestedFilename(),
@@ -2023,33 +2339,51 @@ async function emulateMediaViaPlaywright(opts) {
2023
2339
  await page.emulateMedia({ colorScheme: opts.colorScheme });
2024
2340
  }
2025
2341
  async function setDeviceViaPlaywright(opts) {
2026
- const device = playwrightCore.devices[opts.name];
2342
+ const name = String(opts.name ?? "").trim();
2343
+ if (!name) throw new Error("device name is required");
2344
+ const device = playwrightCore.devices[name];
2027
2345
  if (!device) {
2028
- const available = Object.keys(playwrightCore.devices).slice(0, 10).join(", ");
2029
- throw new Error(`Unknown device "${opts.name}". Some available devices: ${available}...`);
2346
+ throw new Error(`Unknown device "${name}".`);
2030
2347
  }
2031
2348
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2032
2349
  ensurePageState(page);
2033
2350
  if (device.viewport) {
2034
- await page.setViewportSize(device.viewport);
2351
+ await page.setViewportSize({
2352
+ width: device.viewport.width,
2353
+ height: device.viewport.height
2354
+ });
2035
2355
  }
2036
- if (device.userAgent) {
2037
- const context = page.context();
2038
- const session = await context.newCDPSession(page);
2039
- try {
2356
+ const session = await page.context().newCDPSession(page);
2357
+ try {
2358
+ const locale = device.locale;
2359
+ if (device.userAgent || locale) {
2040
2360
  await session.send("Emulation.setUserAgentOverride", {
2041
- userAgent: device.userAgent
2361
+ userAgent: device.userAgent ?? "",
2362
+ acceptLanguage: locale ?? void 0
2042
2363
  });
2043
- } finally {
2044
- await session.detach().catch(() => {
2364
+ }
2365
+ if (device.viewport) {
2366
+ await session.send("Emulation.setDeviceMetricsOverride", {
2367
+ mobile: Boolean(device.isMobile),
2368
+ width: device.viewport.width,
2369
+ height: device.viewport.height,
2370
+ deviceScaleFactor: device.deviceScaleFactor ?? 1,
2371
+ screenWidth: device.viewport.width,
2372
+ screenHeight: device.viewport.height
2045
2373
  });
2046
2374
  }
2375
+ if (device.hasTouch) {
2376
+ await session.send("Emulation.setTouchEmulationEnabled", { enabled: true });
2377
+ }
2378
+ } finally {
2379
+ await session.detach().catch(() => {
2380
+ });
2047
2381
  }
2048
2382
  }
2049
2383
  async function setExtraHTTPHeadersViaPlaywright(opts) {
2050
2384
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2051
2385
  ensurePageState(page);
2052
- await page.setExtraHTTPHeaders(opts.headers);
2386
+ await page.context().setExtraHTTPHeaders(opts.headers);
2053
2387
  }
2054
2388
  async function setGeolocationViaPlaywright(opts) {
2055
2389
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
@@ -2057,38 +2391,53 @@ async function setGeolocationViaPlaywright(opts) {
2057
2391
  const context = page.context();
2058
2392
  if (opts.clear) {
2059
2393
  await context.setGeolocation(null);
2060
- await context.clearPermissions();
2394
+ await context.clearPermissions().catch(() => {
2395
+ });
2061
2396
  return;
2062
2397
  }
2063
- if (opts.latitude === void 0 || opts.longitude === void 0) {
2398
+ if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
2064
2399
  throw new Error("latitude and longitude are required (or set clear=true)");
2065
2400
  }
2066
- await context.grantPermissions(["geolocation"], opts.origin ? { origin: opts.origin } : void 0);
2067
2401
  await context.setGeolocation({
2068
2402
  latitude: opts.latitude,
2069
2403
  longitude: opts.longitude,
2070
- accuracy: opts.accuracy
2404
+ accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
2405
+ });
2406
+ const origin = opts.origin?.trim() || (() => {
2407
+ try {
2408
+ return new URL(page.url()).origin;
2409
+ } catch {
2410
+ return "";
2411
+ }
2412
+ })();
2413
+ if (origin) await context.grantPermissions(["geolocation"], { origin }).catch(() => {
2071
2414
  });
2072
2415
  }
2073
2416
  async function setHttpCredentialsViaPlaywright(opts) {
2074
2417
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2075
2418
  ensurePageState(page);
2076
- const context = page.context();
2077
2419
  if (opts.clear) {
2078
- await context.setHTTPCredentials({ username: "", password: "" });
2420
+ await page.context().setHTTPCredentials(null);
2079
2421
  return;
2080
2422
  }
2081
- await context.setHTTPCredentials({
2082
- username: opts.username ?? "",
2083
- password: opts.password ?? ""
2084
- });
2423
+ const username = String(opts.username ?? "");
2424
+ const password = String(opts.password ?? "");
2425
+ if (!username) throw new Error("username is required (or set clear=true)");
2426
+ await page.context().setHTTPCredentials({ username, password });
2085
2427
  }
2086
2428
  async function setLocaleViaPlaywright(opts) {
2087
2429
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2088
2430
  ensurePageState(page);
2431
+ const locale = String(opts.locale ?? "").trim();
2432
+ if (!locale) throw new Error("locale is required");
2089
2433
  const session = await page.context().newCDPSession(page);
2090
2434
  try {
2091
- await session.send("Emulation.setLocaleOverride", { locale: opts.locale });
2435
+ try {
2436
+ await session.send("Emulation.setLocaleOverride", { locale });
2437
+ } catch (err) {
2438
+ if (String(err).includes("Another locale override is already in effect")) return;
2439
+ throw err;
2440
+ }
2092
2441
  } finally {
2093
2442
  await session.detach().catch(() => {
2094
2443
  });
@@ -2102,9 +2451,18 @@ async function setOfflineViaPlaywright(opts) {
2102
2451
  async function setTimezoneViaPlaywright(opts) {
2103
2452
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2104
2453
  ensurePageState(page);
2454
+ const timezoneId = String(opts.timezoneId ?? "").trim();
2455
+ if (!timezoneId) throw new Error("timezoneId is required");
2105
2456
  const session = await page.context().newCDPSession(page);
2106
2457
  try {
2107
- await session.send("Emulation.setTimezoneOverride", { timezoneId: opts.timezoneId });
2458
+ try {
2459
+ await session.send("Emulation.setTimezoneOverride", { timezoneId });
2460
+ } catch (err) {
2461
+ const msg = String(err);
2462
+ if (msg.includes("Timezone override is already in effect")) return;
2463
+ if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
2464
+ throw err;
2465
+ }
2108
2466
  } finally {
2109
2467
  await session.detach().catch(() => {
2110
2468
  });
@@ -2180,24 +2538,38 @@ async function pdfViaPlaywright(opts) {
2180
2538
  ensurePageState(page);
2181
2539
  return { buffer: await page.pdf({ printBackground: true }) };
2182
2540
  }
2183
-
2184
- // src/capture/trace.ts
2185
2541
  async function traceStartViaPlaywright(opts) {
2186
2542
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2187
2543
  ensurePageState(page);
2188
2544
  const context = page.context();
2545
+ const ctxState = ensureContextState(context);
2546
+ if (ctxState.traceActive) {
2547
+ throw new Error("Trace already running. Stop the current trace before starting a new one.");
2548
+ }
2189
2549
  await context.tracing.start({
2190
2550
  screenshots: opts.screenshots ?? true,
2191
2551
  snapshots: opts.snapshots ?? true,
2192
- sources: opts.sources
2552
+ sources: opts.sources ?? false
2193
2553
  });
2554
+ ctxState.traceActive = true;
2194
2555
  }
2195
2556
  async function traceStopViaPlaywright(opts) {
2196
2557
  await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
2197
2558
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
2198
2559
  ensurePageState(page);
2199
2560
  const context = page.context();
2200
- await context.tracing.stop({ path: opts.path });
2561
+ const ctxState = ensureContextState(context);
2562
+ if (!ctxState.traceActive) {
2563
+ throw new Error("No active trace. Start a trace before stopping it.");
2564
+ }
2565
+ await writeViaSiblingTempPath({
2566
+ rootDir: path.dirname(opts.path),
2567
+ targetPath: opts.path,
2568
+ writeTemp: async (tempPath) => {
2569
+ await context.tracing.stop({ path: tempPath });
2570
+ }
2571
+ });
2572
+ ctxState.traceActive = false;
2201
2573
  }
2202
2574
 
2203
2575
  // src/capture/response.ts
@@ -2280,8 +2652,8 @@ async function cookiesSetViaPlaywright(opts) {
2280
2652
  const cookie = opts.cookie;
2281
2653
  if (!cookie.name || cookie.value === void 0) throw new Error("cookie name and value are required");
2282
2654
  const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
2283
- const hasDomain = typeof cookie.domain === "string" && cookie.domain.trim();
2284
- if (!hasUrl && !hasDomain) throw new Error("cookie requires url or domain");
2655
+ const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() && typeof cookie.path === "string" && cookie.path.trim();
2656
+ if (!hasUrl && !hasDomainPath) throw new Error("cookie requires url, or domain+path");
2285
2657
  await page.context().addCookies([cookie]);
2286
2658
  }
2287
2659
  async function cookiesClearViaPlaywright(opts) {
@@ -3360,11 +3732,17 @@ exports.InvalidBrowserNavigationUrlError = InvalidBrowserNavigationUrlError;
3360
3732
  exports.assertBrowserNavigationAllowed = assertBrowserNavigationAllowed;
3361
3733
  exports.assertBrowserNavigationRedirectChainAllowed = assertBrowserNavigationRedirectChainAllowed;
3362
3734
  exports.assertBrowserNavigationResultAllowed = assertBrowserNavigationResultAllowed;
3735
+ exports.createPinnedLookup = createPinnedLookup;
3736
+ exports.ensureContextState = ensureContextState;
3737
+ exports.forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightForTarget;
3363
3738
  exports.getChromeWebSocketUrl = getChromeWebSocketUrl;
3364
3739
  exports.isChromeCdpReady = isChromeCdpReady;
3365
3740
  exports.isChromeReachable = isChromeReachable;
3366
3741
  exports.normalizeCdpHttpBaseForJsonEndpoints = normalizeCdpHttpBaseForJsonEndpoints;
3367
3742
  exports.requiresInspectableBrowserNavigationRedirects = requiresInspectableBrowserNavigationRedirects;
3743
+ exports.resolvePinnedHostnameWithPolicy = resolvePinnedHostnameWithPolicy;
3744
+ exports.sanitizeUntrustedFileName = sanitizeUntrustedFileName;
3368
3745
  exports.withBrowserNavigationPolicy = withBrowserNavigationPolicy;
3746
+ exports.writeViaSiblingTempPath = writeViaSiblingTempPath;
3369
3747
  //# sourceMappingURL=index.cjs.map
3370
3748
  //# sourceMappingURL=index.cjs.map