browserclaw 0.4.3 → 0.5.1
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 +676 -257
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +58 -10
- package/dist/index.d.ts +58 -10
- package/dist/index.js +655 -261
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
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,261 @@ 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
|
+
if (parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0) {
|
|
1528
|
+
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1529
|
+
try {
|
|
1530
|
+
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1531
|
+
} catch {
|
|
1532
|
+
return true;
|
|
1533
|
+
}
|
|
1534
|
+
}
|
|
1535
|
+
if ((parts[4] & 65023) === 0 && parts[5] === 24318) {
|
|
1536
|
+
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1537
|
+
try {
|
|
1538
|
+
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1539
|
+
} catch {
|
|
1540
|
+
return true;
|
|
1541
|
+
}
|
|
1542
|
+
}
|
|
1543
|
+
return null;
|
|
1544
|
+
}
|
|
1545
|
+
function isPrivateIpAddress(address, opts) {
|
|
1546
|
+
let normalized = address.trim().toLowerCase();
|
|
1547
|
+
if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1);
|
|
1548
|
+
if (!normalized) return false;
|
|
1549
|
+
try {
|
|
1550
|
+
const parsed = ipaddr__namespace.parse(normalized);
|
|
1551
|
+
if (parsed.kind() === "ipv4") {
|
|
1552
|
+
return isBlockedSpecialUseIpv4Address(parsed, opts);
|
|
1553
|
+
}
|
|
1554
|
+
const v6 = parsed;
|
|
1555
|
+
if (isBlockedSpecialUseIpv6Address(v6)) return true;
|
|
1556
|
+
const embeddedV4 = extractEmbeddedIpv4FromIpv6(v6, opts);
|
|
1557
|
+
if (embeddedV4 !== null) return embeddedV4;
|
|
1558
|
+
return false;
|
|
1559
|
+
} catch {
|
|
1560
|
+
}
|
|
1561
|
+
if (!normalized.includes(":") && isUnsupportedIPv4Literal(normalized)) return true;
|
|
1562
|
+
if (normalized.includes(":")) return true;
|
|
1563
|
+
return false;
|
|
1564
|
+
}
|
|
1565
|
+
function normalizeHostnameSet(values) {
|
|
1566
|
+
if (!values || values.length === 0) return /* @__PURE__ */ new Set();
|
|
1567
|
+
return new Set(values.map((v) => normalizeHostname(v)).filter(Boolean));
|
|
1568
|
+
}
|
|
1569
|
+
function normalizeHostnameAllowlist(values) {
|
|
1570
|
+
if (!values || values.length === 0) return [];
|
|
1571
|
+
return Array.from(
|
|
1572
|
+
new Set(
|
|
1573
|
+
values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0)
|
|
1574
|
+
)
|
|
1575
|
+
);
|
|
1576
|
+
}
|
|
1577
|
+
function isHostnameAllowedByPattern(hostname, pattern) {
|
|
1578
|
+
if (pattern.startsWith("*.")) {
|
|
1579
|
+
const suffix = pattern.slice(2);
|
|
1580
|
+
if (!suffix || hostname === suffix) return false;
|
|
1581
|
+
return hostname.endsWith(`.${suffix}`);
|
|
1582
|
+
}
|
|
1583
|
+
return hostname === pattern;
|
|
1584
|
+
}
|
|
1585
|
+
function matchesHostnameAllowlist(hostname, allowlist) {
|
|
1586
|
+
if (allowlist.length === 0) return true;
|
|
1587
|
+
return allowlist.some((pattern) => isHostnameAllowedByPattern(hostname, pattern));
|
|
1588
|
+
}
|
|
1589
|
+
function dedupeAndPreferIpv4(results) {
|
|
1590
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1591
|
+
const ipv4 = [];
|
|
1592
|
+
const ipv6 = [];
|
|
1593
|
+
for (const r of results) {
|
|
1594
|
+
if (seen.has(r.address)) continue;
|
|
1595
|
+
seen.add(r.address);
|
|
1596
|
+
if (r.family === 4) ipv4.push(r.address);
|
|
1597
|
+
else ipv6.push(r.address);
|
|
1598
|
+
}
|
|
1599
|
+
return [...ipv4, ...ipv6];
|
|
1600
|
+
}
|
|
1601
|
+
function createPinnedLookup(params) {
|
|
1602
|
+
const normalizedHost = normalizeHostname(params.hostname);
|
|
1603
|
+
const fallback = params.fallback ?? dns.lookup;
|
|
1604
|
+
const records = params.addresses.map((address) => ({
|
|
1605
|
+
address,
|
|
1606
|
+
family: address.includes(":") ? 6 : 4
|
|
1607
|
+
}));
|
|
1608
|
+
let index = 0;
|
|
1609
|
+
return ((host, options, callback) => {
|
|
1610
|
+
const cb = typeof options === "function" ? options : callback;
|
|
1611
|
+
if (!cb) return;
|
|
1612
|
+
const normalized = normalizeHostname(host);
|
|
1613
|
+
if (!normalized || normalized !== normalizedHost) {
|
|
1614
|
+
if (typeof options === "function" || options === void 0) return fallback(host, cb);
|
|
1615
|
+
return fallback(host, options, cb);
|
|
1616
|
+
}
|
|
1617
|
+
const opts = typeof options === "object" && options !== null ? options : {};
|
|
1618
|
+
const requestedFamily = typeof options === "number" ? options : typeof opts.family === "number" ? opts.family : 0;
|
|
1619
|
+
const candidates = requestedFamily === 4 || requestedFamily === 6 ? records.filter((entry) => entry.family === requestedFamily) : records;
|
|
1620
|
+
const usable = candidates.length > 0 ? candidates : records;
|
|
1621
|
+
if (opts.all) {
|
|
1622
|
+
cb(null, usable);
|
|
1623
|
+
return;
|
|
1624
|
+
}
|
|
1625
|
+
const chosen = usable[index % usable.length];
|
|
1626
|
+
index += 1;
|
|
1627
|
+
cb(null, chosen.address, chosen.family);
|
|
1628
|
+
});
|
|
1629
|
+
}
|
|
1630
|
+
async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
1631
|
+
const normalized = normalizeHostname(hostname);
|
|
1632
|
+
if (!normalized) throw new InvalidBrowserNavigationUrlError(`Invalid hostname: "${hostname}"`);
|
|
1633
|
+
const allowPrivateNetwork = isPrivateNetworkAllowedByPolicy(params.policy);
|
|
1634
|
+
const allowedHostnames = normalizeHostnameSet(params.policy?.allowedHostnames);
|
|
1635
|
+
const hostnameAllowlist = normalizeHostnameAllowlist(params.policy?.hostnameAllowlist);
|
|
1636
|
+
const isExplicitlyAllowed = allowedHostnames.has(normalized);
|
|
1637
|
+
const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitlyAllowed;
|
|
1638
|
+
if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) {
|
|
1639
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1640
|
+
`Navigation blocked: hostname "${hostname}" is not in the allowlist.`
|
|
1641
|
+
);
|
|
1642
|
+
}
|
|
1643
|
+
if (!skipPrivateNetworkChecks) {
|
|
1644
|
+
if (isBlockedHostnameNormalized(normalized)) {
|
|
1645
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1646
|
+
`Navigation to internal/loopback address blocked: "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1647
|
+
);
|
|
1648
|
+
}
|
|
1649
|
+
const ipOpts = { allowRfc2544BenchmarkRange: params.policy?.allowRfc2544BenchmarkRange };
|
|
1650
|
+
if (isPrivateIpAddress(normalized, ipOpts)) {
|
|
1651
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1652
|
+
`Navigation to internal/loopback address blocked: "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1653
|
+
);
|
|
1654
|
+
}
|
|
1655
|
+
}
|
|
1656
|
+
const lookupFn = params.lookupFn ?? promises.lookup;
|
|
1657
|
+
let results;
|
|
1658
|
+
try {
|
|
1659
|
+
results = await lookupFn(normalized, { all: true });
|
|
1660
|
+
} catch {
|
|
1661
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1662
|
+
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1663
|
+
);
|
|
1664
|
+
}
|
|
1665
|
+
if (!results || results.length === 0) {
|
|
1666
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1667
|
+
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1668
|
+
);
|
|
1669
|
+
}
|
|
1670
|
+
if (!skipPrivateNetworkChecks) {
|
|
1671
|
+
const ipOpts = { allowRfc2544BenchmarkRange: params.policy?.allowRfc2544BenchmarkRange };
|
|
1672
|
+
for (const r of results) {
|
|
1673
|
+
if (isPrivateIpAddress(r.address, ipOpts)) {
|
|
1674
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1675
|
+
`Navigation to internal/loopback address blocked: "${hostname}" resolves to "${r.address}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1676
|
+
);
|
|
1677
|
+
}
|
|
1678
|
+
}
|
|
1679
|
+
}
|
|
1680
|
+
const addresses = dedupeAndPreferIpv4(results);
|
|
1681
|
+
if (addresses.length === 0) {
|
|
1682
|
+
throw new InvalidBrowserNavigationUrlError(
|
|
1683
|
+
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}".`
|
|
1684
|
+
);
|
|
1685
|
+
}
|
|
1686
|
+
return {
|
|
1687
|
+
hostname: normalized,
|
|
1688
|
+
addresses,
|
|
1689
|
+
lookup: createPinnedLookup({ hostname: normalized, addresses })
|
|
1690
|
+
};
|
|
1691
|
+
}
|
|
1354
1692
|
async function assertBrowserNavigationAllowed(opts) {
|
|
1355
1693
|
const rawUrl = String(opts.url ?? "").trim();
|
|
1356
1694
|
let parsed;
|
|
@@ -1368,22 +1706,10 @@ async function assertBrowserNavigationAllowed(opts) {
|
|
|
1368
1706
|
"Navigation blocked: strict browser SSRF policy cannot be enforced while env proxy variables are set"
|
|
1369
1707
|
);
|
|
1370
1708
|
}
|
|
1371
|
-
|
|
1372
|
-
|
|
1373
|
-
|
|
1374
|
-
|
|
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
|
-
}
|
|
1709
|
+
await resolvePinnedHostnameWithPolicy(parsed.hostname, {
|
|
1710
|
+
lookupFn: opts.lookupFn,
|
|
1711
|
+
policy: opts.ssrfPolicy
|
|
1712
|
+
});
|
|
1387
1713
|
}
|
|
1388
1714
|
async function assertSafeOutputPath(path2, allowedRoots) {
|
|
1389
1715
|
if (!path2 || typeof path2 !== "string") {
|
|
@@ -1426,111 +1752,6 @@ async function assertSafeOutputPath(path2, allowedRoots) {
|
|
|
1426
1752
|
}
|
|
1427
1753
|
}
|
|
1428
1754
|
}
|
|
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
1755
|
async function assertSafeUploadPaths(paths) {
|
|
1535
1756
|
for (const filePath of paths) {
|
|
1536
1757
|
let stat;
|
|
@@ -1547,21 +1768,48 @@ async function assertSafeUploadPaths(paths) {
|
|
|
1547
1768
|
}
|
|
1548
1769
|
}
|
|
1549
1770
|
}
|
|
1550
|
-
|
|
1551
|
-
|
|
1552
|
-
|
|
1771
|
+
function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
1772
|
+
const trimmed = String(fileName ?? "").trim();
|
|
1773
|
+
if (!trimmed) return fallbackName;
|
|
1774
|
+
let base = path.posix.basename(trimmed);
|
|
1775
|
+
base = path.win32.basename(base);
|
|
1776
|
+
let cleaned = "";
|
|
1777
|
+
for (let i = 0; i < base.length; i++) {
|
|
1778
|
+
const code = base.charCodeAt(i);
|
|
1779
|
+
if (code < 32 || code === 127) continue;
|
|
1780
|
+
cleaned += base[i];
|
|
1781
|
+
}
|
|
1782
|
+
base = cleaned.trim();
|
|
1783
|
+
if (!base || base === "." || base === "..") return fallbackName;
|
|
1784
|
+
if (base.length > 200) base = base.slice(0, 200);
|
|
1785
|
+
return base;
|
|
1786
|
+
}
|
|
1787
|
+
function buildSiblingTempPath(targetPath) {
|
|
1788
|
+
const id = crypto.randomUUID();
|
|
1789
|
+
const safeTail = sanitizeUntrustedFileName(path.basename(targetPath), "output.bin");
|
|
1790
|
+
return path.join(path.dirname(targetPath), `.browserclaw-output-${id}-${safeTail}.part`);
|
|
1791
|
+
}
|
|
1792
|
+
async function writeViaSiblingTempPath(params) {
|
|
1793
|
+
const rootDir = await promises$1.realpath(path.resolve(params.rootDir)).catch(() => path.resolve(params.rootDir));
|
|
1794
|
+
const requestedTargetPath = path.resolve(params.targetPath);
|
|
1795
|
+
const targetPath = await promises$1.realpath(path.dirname(requestedTargetPath)).then((realDir) => path.join(realDir, path.basename(requestedTargetPath))).catch(() => requestedTargetPath);
|
|
1796
|
+
const relativeTargetPath = path.relative(rootDir, targetPath);
|
|
1797
|
+
if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${path.sep}`) || isAbsolute(relativeTargetPath)) {
|
|
1798
|
+
throw new Error("Target path is outside the allowed root");
|
|
1799
|
+
}
|
|
1800
|
+
const tempPath = buildSiblingTempPath(targetPath);
|
|
1801
|
+
let renameSucceeded = false;
|
|
1553
1802
|
try {
|
|
1554
|
-
|
|
1555
|
-
|
|
1556
|
-
|
|
1557
|
-
}
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
if (isInternalIP(address, opts)) return true;
|
|
1561
|
-
} catch {
|
|
1562
|
-
return true;
|
|
1803
|
+
await params.writeTemp(tempPath);
|
|
1804
|
+
await promises$1.rename(tempPath, targetPath);
|
|
1805
|
+
renameSucceeded = true;
|
|
1806
|
+
} finally {
|
|
1807
|
+
if (!renameSucceeded) await promises$1.rm(tempPath, { force: true }).catch(() => {
|
|
1808
|
+
});
|
|
1563
1809
|
}
|
|
1564
|
-
|
|
1810
|
+
}
|
|
1811
|
+
function isAbsolute(p) {
|
|
1812
|
+
return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
|
|
1565
1813
|
}
|
|
1566
1814
|
async function assertBrowserNavigationResultAllowed(opts) {
|
|
1567
1815
|
const rawUrl = String(opts.url ?? "").trim();
|
|
@@ -1669,13 +1917,12 @@ async function fillFormViaPlaywright(opts) {
|
|
|
1669
1917
|
ensurePageState(page);
|
|
1670
1918
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1671
1919
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4);
|
|
1672
|
-
for (
|
|
1673
|
-
const field = opts.fields[i];
|
|
1920
|
+
for (const field of opts.fields) {
|
|
1674
1921
|
const ref = field.ref.trim();
|
|
1675
1922
|
const type = (typeof field.type === "string" ? field.type.trim() : "") || "text";
|
|
1676
1923
|
const rawValue = field.value;
|
|
1677
|
-
const value = rawValue
|
|
1678
|
-
if (!ref)
|
|
1924
|
+
const value = typeof rawValue === "string" ? rawValue : typeof rawValue === "number" || typeof rawValue === "boolean" ? String(rawValue) : "";
|
|
1925
|
+
if (!ref) continue;
|
|
1679
1926
|
const locator = refLocator(page, ref);
|
|
1680
1927
|
if (type === "checkbox" || type === "radio") {
|
|
1681
1928
|
const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
|
|
@@ -1719,61 +1966,78 @@ async function setInputFilesViaPlaywright(opts) {
|
|
|
1719
1966
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1720
1967
|
ensurePageState(page);
|
|
1721
1968
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1722
|
-
|
|
1723
|
-
|
|
1969
|
+
if (!opts.paths.length) throw new Error("paths are required");
|
|
1970
|
+
const inputRef = typeof opts.ref === "string" ? opts.ref.trim() : "";
|
|
1971
|
+
const element = typeof opts.element === "string" ? opts.element.trim() : "";
|
|
1972
|
+
if (inputRef && element) throw new Error("ref and element are mutually exclusive");
|
|
1973
|
+
if (!inputRef && !element) throw new Error("Either ref or element is required for setInputFiles");
|
|
1974
|
+
const locator = inputRef ? refLocator(page, inputRef) : page.locator(element).first();
|
|
1724
1975
|
await assertSafeUploadPaths(opts.paths);
|
|
1725
1976
|
try {
|
|
1726
1977
|
await locator.setInputFiles(opts.paths);
|
|
1727
1978
|
} catch (err) {
|
|
1728
|
-
throw toAIFriendlyError(err,
|
|
1979
|
+
throw toAIFriendlyError(err, inputRef || element);
|
|
1980
|
+
}
|
|
1981
|
+
try {
|
|
1982
|
+
const handle = await locator.elementHandle();
|
|
1983
|
+
if (handle) {
|
|
1984
|
+
await handle.evaluate((el) => {
|
|
1985
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
1986
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
1987
|
+
});
|
|
1988
|
+
}
|
|
1989
|
+
} catch {
|
|
1729
1990
|
}
|
|
1730
1991
|
}
|
|
1731
1992
|
async function armDialogViaPlaywright(opts) {
|
|
1732
1993
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1733
|
-
ensurePageState(page);
|
|
1734
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs,
|
|
1735
|
-
|
|
1736
|
-
|
|
1737
|
-
|
|
1738
|
-
|
|
1739
|
-
|
|
1740
|
-
|
|
1741
|
-
|
|
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);
|
|
1994
|
+
const state = ensurePageState(page);
|
|
1995
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
1996
|
+
state.armIdDialog = bumpDialogArmId();
|
|
1997
|
+
const armId = state.armIdDialog;
|
|
1998
|
+
page.waitForEvent("dialog", { timeout }).then(async (dialog) => {
|
|
1999
|
+
if (state.armIdDialog !== armId) return;
|
|
2000
|
+
if (opts.accept) await dialog.accept(opts.promptText);
|
|
2001
|
+
else await dialog.dismiss();
|
|
2002
|
+
}).catch(() => {
|
|
1754
2003
|
});
|
|
1755
2004
|
}
|
|
1756
2005
|
async function armFileUploadViaPlaywright(opts) {
|
|
1757
2006
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1758
|
-
ensurePageState(page);
|
|
1759
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs,
|
|
1760
|
-
|
|
1761
|
-
|
|
1762
|
-
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
const handler = async (fc) => {
|
|
1766
|
-
clearTimeout(timer);
|
|
2007
|
+
const state = ensurePageState(page);
|
|
2008
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
2009
|
+
state.armIdUpload = bumpUploadArmId();
|
|
2010
|
+
const armId = state.armIdUpload;
|
|
2011
|
+
page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
|
|
2012
|
+
if (state.armIdUpload !== armId) return;
|
|
2013
|
+
if (!opts.paths?.length) {
|
|
1767
2014
|
try {
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
await fc.setFiles(paths);
|
|
1771
|
-
resolve2();
|
|
1772
|
-
} catch (err) {
|
|
1773
|
-
reject(err);
|
|
2015
|
+
await page.keyboard.press("Escape");
|
|
2016
|
+
} catch {
|
|
1774
2017
|
}
|
|
1775
|
-
|
|
1776
|
-
|
|
2018
|
+
return;
|
|
2019
|
+
}
|
|
2020
|
+
try {
|
|
2021
|
+
await assertSafeUploadPaths(opts.paths);
|
|
2022
|
+
} catch {
|
|
2023
|
+
try {
|
|
2024
|
+
await page.keyboard.press("Escape");
|
|
2025
|
+
} catch {
|
|
2026
|
+
}
|
|
2027
|
+
return;
|
|
2028
|
+
}
|
|
2029
|
+
await fileChooser.setFiles(opts.paths);
|
|
2030
|
+
try {
|
|
2031
|
+
const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
|
|
2032
|
+
if (input) {
|
|
2033
|
+
await input.evaluate((el) => {
|
|
2034
|
+
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
2035
|
+
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
2036
|
+
});
|
|
2037
|
+
}
|
|
2038
|
+
} catch {
|
|
2039
|
+
}
|
|
2040
|
+
}).catch(() => {
|
|
1777
2041
|
});
|
|
1778
2042
|
}
|
|
1779
2043
|
|
|
@@ -1787,17 +2051,35 @@ async function pressKeyViaPlaywright(opts) {
|
|
|
1787
2051
|
}
|
|
1788
2052
|
|
|
1789
2053
|
// src/actions/navigation.ts
|
|
2054
|
+
function isRetryableNavigateError(err) {
|
|
2055
|
+
const msg = typeof err === "string" ? err.toLowerCase() : err instanceof Error ? err.message.toLowerCase() : "";
|
|
2056
|
+
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
2057
|
+
}
|
|
1790
2058
|
async function navigateViaPlaywright(opts) {
|
|
1791
2059
|
const url = String(opts.url ?? "").trim();
|
|
1792
2060
|
if (!url) throw new Error("url is required");
|
|
1793
2061
|
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
1794
|
-
await assertBrowserNavigationAllowed({ url,
|
|
1795
|
-
const
|
|
2062
|
+
await assertBrowserNavigationAllowed({ url, ...withBrowserNavigationPolicy(policy) });
|
|
2063
|
+
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
|
|
2064
|
+
let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1796
2065
|
ensurePageState(page);
|
|
1797
|
-
const
|
|
2066
|
+
const navigate = async () => await page.goto(url, { timeout });
|
|
2067
|
+
let response;
|
|
2068
|
+
try {
|
|
2069
|
+
response = await navigate();
|
|
2070
|
+
} catch (err) {
|
|
2071
|
+
if (!isRetryableNavigateError(err)) throw err;
|
|
2072
|
+
await forceDisconnectPlaywrightForTarget({
|
|
2073
|
+
cdpUrl: opts.cdpUrl,
|
|
2074
|
+
targetId: opts.targetId}).catch(() => {
|
|
2075
|
+
});
|
|
2076
|
+
page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2077
|
+
ensurePageState(page);
|
|
2078
|
+
response = await navigate();
|
|
2079
|
+
}
|
|
1798
2080
|
await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...withBrowserNavigationPolicy(policy) });
|
|
1799
2081
|
const finalUrl = page.url();
|
|
1800
|
-
await assertBrowserNavigationResultAllowed({ url: finalUrl,
|
|
2082
|
+
await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
|
|
1801
2083
|
return { url: finalUrl };
|
|
1802
2084
|
}
|
|
1803
2085
|
async function listPagesViaPlaywright(opts) {
|
|
@@ -1823,11 +2105,12 @@ async function createPageViaPlaywright(opts) {
|
|
|
1823
2105
|
}
|
|
1824
2106
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1825
2107
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
2108
|
+
ensureContextState(context);
|
|
1826
2109
|
const page = await context.newPage();
|
|
1827
2110
|
ensurePageState(page);
|
|
1828
2111
|
if (targetUrl !== "about:blank") {
|
|
1829
2112
|
const navigationPolicy = withBrowserNavigationPolicy(policy);
|
|
1830
|
-
const response = await page.goto(targetUrl, { timeout:
|
|
2113
|
+
const response = await page.goto(targetUrl, { timeout: 3e4 }).catch(() => null);
|
|
1831
2114
|
await assertBrowserNavigationRedirectChainAllowed({ request: response?.request(), ...navigationPolicy });
|
|
1832
2115
|
await assertBrowserNavigationResultAllowed({ url: page.url(), ssrfPolicy: policy });
|
|
1833
2116
|
}
|
|
@@ -1936,68 +2219,136 @@ async function evaluateInAllFramesViaPlaywright(opts) {
|
|
|
1936
2219
|
}
|
|
1937
2220
|
return results;
|
|
1938
2221
|
}
|
|
2222
|
+
async function awaitEvalWithAbort(evalPromise, abortPromise) {
|
|
2223
|
+
if (!abortPromise) return await evalPromise;
|
|
2224
|
+
try {
|
|
2225
|
+
return await Promise.race([evalPromise, abortPromise]);
|
|
2226
|
+
} catch (err) {
|
|
2227
|
+
evalPromise.catch(() => {
|
|
2228
|
+
});
|
|
2229
|
+
throw err;
|
|
2230
|
+
}
|
|
2231
|
+
}
|
|
2232
|
+
var BROWSER_EVALUATOR = new Function("args", `
|
|
2233
|
+
"use strict";
|
|
2234
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2235
|
+
try {
|
|
2236
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2237
|
+
var result = typeof candidate === "function" ? candidate() : candidate;
|
|
2238
|
+
if (result && typeof result.then === "function") {
|
|
2239
|
+
return Promise.race([
|
|
2240
|
+
result,
|
|
2241
|
+
new Promise(function(_, reject) {
|
|
2242
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2243
|
+
})
|
|
2244
|
+
]);
|
|
2245
|
+
}
|
|
2246
|
+
return result;
|
|
2247
|
+
} catch (err) {
|
|
2248
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2249
|
+
}
|
|
2250
|
+
`);
|
|
2251
|
+
var ELEMENT_EVALUATOR = new Function("el", "args", `
|
|
2252
|
+
"use strict";
|
|
2253
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2254
|
+
try {
|
|
2255
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2256
|
+
var result = typeof candidate === "function" ? candidate(el) : candidate;
|
|
2257
|
+
if (result && typeof result.then === "function") {
|
|
2258
|
+
return Promise.race([
|
|
2259
|
+
result,
|
|
2260
|
+
new Promise(function(_, reject) {
|
|
2261
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2262
|
+
})
|
|
2263
|
+
]);
|
|
2264
|
+
}
|
|
2265
|
+
return result;
|
|
2266
|
+
} catch (err) {
|
|
2267
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2268
|
+
}
|
|
2269
|
+
`);
|
|
1939
2270
|
async function evaluateViaPlaywright(opts) {
|
|
1940
2271
|
const fnText = String(opts.fn ?? "").trim();
|
|
1941
2272
|
if (!fnText) throw new Error("function is required");
|
|
1942
2273
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1943
2274
|
ensurePageState(page);
|
|
1944
2275
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1945
|
-
const
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
2276
|
+
const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
2277
|
+
let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
|
|
2278
|
+
evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
|
|
2279
|
+
const signal = opts.signal;
|
|
2280
|
+
let abortListener;
|
|
2281
|
+
let abortReject;
|
|
2282
|
+
let abortPromise;
|
|
2283
|
+
if (signal) {
|
|
2284
|
+
abortPromise = new Promise((_, reject) => {
|
|
2285
|
+
abortReject = reject;
|
|
2286
|
+
});
|
|
2287
|
+
abortPromise.catch(() => {
|
|
2288
|
+
});
|
|
2289
|
+
}
|
|
2290
|
+
if (signal) {
|
|
2291
|
+
const disconnect = () => {
|
|
2292
|
+
forceDisconnectPlaywrightForTarget({
|
|
2293
|
+
cdpUrl: opts.cdpUrl,
|
|
2294
|
+
targetId: opts.targetId}).catch(() => {
|
|
2295
|
+
});
|
|
2296
|
+
};
|
|
2297
|
+
if (signal.aborted) {
|
|
2298
|
+
disconnect();
|
|
2299
|
+
throw signal.reason ?? new Error("aborted");
|
|
2300
|
+
}
|
|
2301
|
+
abortListener = () => {
|
|
2302
|
+
disconnect();
|
|
2303
|
+
abortReject?.(signal.reason ?? new Error("aborted"));
|
|
2304
|
+
};
|
|
2305
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
2306
|
+
if (signal.aborted) {
|
|
2307
|
+
abortListener();
|
|
2308
|
+
throw signal.reason ?? new Error("aborted");
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
try {
|
|
2312
|
+
if (opts.ref) {
|
|
2313
|
+
const locator = refLocator(page, opts.ref);
|
|
2314
|
+
return await awaitEvalWithAbort(
|
|
2315
|
+
locator.evaluate(ELEMENT_EVALUATOR, { fnBody: fnText, timeoutMs: evaluateTimeout }),
|
|
2316
|
+
abortPromise
|
|
2317
|
+
);
|
|
2318
|
+
}
|
|
2319
|
+
return await awaitEvalWithAbort(
|
|
2320
|
+
page.evaluate(BROWSER_EVALUATOR, { fnBody: fnText, timeoutMs: evaluateTimeout }),
|
|
2321
|
+
abortPromise
|
|
1960
2322
|
);
|
|
2323
|
+
} finally {
|
|
2324
|
+
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
1961
2325
|
}
|
|
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
2326
|
}
|
|
1982
|
-
|
|
1983
|
-
// src/actions/download.ts
|
|
1984
2327
|
async function downloadViaPlaywright(opts) {
|
|
1985
2328
|
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1986
2329
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1987
|
-
ensurePageState(page);
|
|
2330
|
+
const state = ensurePageState(page);
|
|
1988
2331
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1989
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs,
|
|
2332
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
1990
2333
|
const locator = refLocator(page, opts.ref);
|
|
2334
|
+
state.armIdDownload = bumpDownloadArmId();
|
|
1991
2335
|
try {
|
|
1992
2336
|
const [download] = await Promise.all([
|
|
1993
2337
|
page.waitForEvent("download", { timeout }),
|
|
1994
2338
|
locator.click({ timeout })
|
|
1995
2339
|
]);
|
|
1996
|
-
|
|
2340
|
+
const outPath = opts.path;
|
|
2341
|
+
await writeViaSiblingTempPath({
|
|
2342
|
+
rootDir: path.dirname(outPath),
|
|
2343
|
+
targetPath: outPath,
|
|
2344
|
+
writeTemp: async (tempPath) => {
|
|
2345
|
+
await download.saveAs(tempPath);
|
|
2346
|
+
}
|
|
2347
|
+
});
|
|
1997
2348
|
return {
|
|
1998
2349
|
url: download.url(),
|
|
1999
2350
|
suggestedFilename: download.suggestedFilename(),
|
|
2000
|
-
path:
|
|
2351
|
+
path: outPath
|
|
2001
2352
|
};
|
|
2002
2353
|
} catch (err) {
|
|
2003
2354
|
throw toAIFriendlyError(err, opts.ref);
|
|
@@ -2006,11 +2357,17 @@ async function downloadViaPlaywright(opts) {
|
|
|
2006
2357
|
async function waitForDownloadViaPlaywright(opts) {
|
|
2007
2358
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2008
2359
|
ensurePageState(page);
|
|
2009
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs,
|
|
2360
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
2010
2361
|
const download = await page.waitForEvent("download", { timeout });
|
|
2011
2362
|
const savePath = opts.path ?? download.suggestedFilename();
|
|
2012
2363
|
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
2013
|
-
await
|
|
2364
|
+
await writeViaSiblingTempPath({
|
|
2365
|
+
rootDir: path.dirname(savePath),
|
|
2366
|
+
targetPath: savePath,
|
|
2367
|
+
writeTemp: async (tempPath) => {
|
|
2368
|
+
await download.saveAs(tempPath);
|
|
2369
|
+
}
|
|
2370
|
+
});
|
|
2014
2371
|
return {
|
|
2015
2372
|
url: download.url(),
|
|
2016
2373
|
suggestedFilename: download.suggestedFilename(),
|
|
@@ -2023,33 +2380,51 @@ async function emulateMediaViaPlaywright(opts) {
|
|
|
2023
2380
|
await page.emulateMedia({ colorScheme: opts.colorScheme });
|
|
2024
2381
|
}
|
|
2025
2382
|
async function setDeviceViaPlaywright(opts) {
|
|
2026
|
-
const
|
|
2383
|
+
const name = String(opts.name ?? "").trim();
|
|
2384
|
+
if (!name) throw new Error("device name is required");
|
|
2385
|
+
const device = playwrightCore.devices[name];
|
|
2027
2386
|
if (!device) {
|
|
2028
|
-
|
|
2029
|
-
throw new Error(`Unknown device "${opts.name}". Some available devices: ${available}...`);
|
|
2387
|
+
throw new Error(`Unknown device "${name}".`);
|
|
2030
2388
|
}
|
|
2031
2389
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2032
2390
|
ensurePageState(page);
|
|
2033
2391
|
if (device.viewport) {
|
|
2034
|
-
await page.setViewportSize(
|
|
2392
|
+
await page.setViewportSize({
|
|
2393
|
+
width: device.viewport.width,
|
|
2394
|
+
height: device.viewport.height
|
|
2395
|
+
});
|
|
2035
2396
|
}
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
const
|
|
2039
|
-
|
|
2397
|
+
const session = await page.context().newCDPSession(page);
|
|
2398
|
+
try {
|
|
2399
|
+
const locale = device.locale;
|
|
2400
|
+
if (device.userAgent || locale) {
|
|
2040
2401
|
await session.send("Emulation.setUserAgentOverride", {
|
|
2041
|
-
userAgent: device.userAgent
|
|
2402
|
+
userAgent: device.userAgent ?? "",
|
|
2403
|
+
acceptLanguage: locale ?? void 0
|
|
2042
2404
|
});
|
|
2043
|
-
}
|
|
2044
|
-
|
|
2405
|
+
}
|
|
2406
|
+
if (device.viewport) {
|
|
2407
|
+
await session.send("Emulation.setDeviceMetricsOverride", {
|
|
2408
|
+
mobile: Boolean(device.isMobile),
|
|
2409
|
+
width: device.viewport.width,
|
|
2410
|
+
height: device.viewport.height,
|
|
2411
|
+
deviceScaleFactor: device.deviceScaleFactor ?? 1,
|
|
2412
|
+
screenWidth: device.viewport.width,
|
|
2413
|
+
screenHeight: device.viewport.height
|
|
2045
2414
|
});
|
|
2046
2415
|
}
|
|
2416
|
+
if (device.hasTouch) {
|
|
2417
|
+
await session.send("Emulation.setTouchEmulationEnabled", { enabled: true });
|
|
2418
|
+
}
|
|
2419
|
+
} finally {
|
|
2420
|
+
await session.detach().catch(() => {
|
|
2421
|
+
});
|
|
2047
2422
|
}
|
|
2048
2423
|
}
|
|
2049
2424
|
async function setExtraHTTPHeadersViaPlaywright(opts) {
|
|
2050
2425
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2051
2426
|
ensurePageState(page);
|
|
2052
|
-
await page.setExtraHTTPHeaders(opts.headers);
|
|
2427
|
+
await page.context().setExtraHTTPHeaders(opts.headers);
|
|
2053
2428
|
}
|
|
2054
2429
|
async function setGeolocationViaPlaywright(opts) {
|
|
2055
2430
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
@@ -2057,38 +2432,53 @@ async function setGeolocationViaPlaywright(opts) {
|
|
|
2057
2432
|
const context = page.context();
|
|
2058
2433
|
if (opts.clear) {
|
|
2059
2434
|
await context.setGeolocation(null);
|
|
2060
|
-
await context.clearPermissions()
|
|
2435
|
+
await context.clearPermissions().catch(() => {
|
|
2436
|
+
});
|
|
2061
2437
|
return;
|
|
2062
2438
|
}
|
|
2063
|
-
if (opts.latitude
|
|
2439
|
+
if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
|
|
2064
2440
|
throw new Error("latitude and longitude are required (or set clear=true)");
|
|
2065
2441
|
}
|
|
2066
|
-
await context.grantPermissions(["geolocation"], opts.origin ? { origin: opts.origin } : void 0);
|
|
2067
2442
|
await context.setGeolocation({
|
|
2068
2443
|
latitude: opts.latitude,
|
|
2069
2444
|
longitude: opts.longitude,
|
|
2070
|
-
accuracy: opts.accuracy
|
|
2445
|
+
accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
|
|
2446
|
+
});
|
|
2447
|
+
const origin = opts.origin?.trim() || (() => {
|
|
2448
|
+
try {
|
|
2449
|
+
return new URL(page.url()).origin;
|
|
2450
|
+
} catch {
|
|
2451
|
+
return "";
|
|
2452
|
+
}
|
|
2453
|
+
})();
|
|
2454
|
+
if (origin) await context.grantPermissions(["geolocation"], { origin }).catch(() => {
|
|
2071
2455
|
});
|
|
2072
2456
|
}
|
|
2073
2457
|
async function setHttpCredentialsViaPlaywright(opts) {
|
|
2074
2458
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2075
2459
|
ensurePageState(page);
|
|
2076
|
-
const context = page.context();
|
|
2077
2460
|
if (opts.clear) {
|
|
2078
|
-
await context.setHTTPCredentials(
|
|
2461
|
+
await page.context().setHTTPCredentials(null);
|
|
2079
2462
|
return;
|
|
2080
2463
|
}
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
});
|
|
2464
|
+
const username = String(opts.username ?? "");
|
|
2465
|
+
const password = String(opts.password ?? "");
|
|
2466
|
+
if (!username) throw new Error("username is required (or set clear=true)");
|
|
2467
|
+
await page.context().setHTTPCredentials({ username, password });
|
|
2085
2468
|
}
|
|
2086
2469
|
async function setLocaleViaPlaywright(opts) {
|
|
2087
2470
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2088
2471
|
ensurePageState(page);
|
|
2472
|
+
const locale = String(opts.locale ?? "").trim();
|
|
2473
|
+
if (!locale) throw new Error("locale is required");
|
|
2089
2474
|
const session = await page.context().newCDPSession(page);
|
|
2090
2475
|
try {
|
|
2091
|
-
|
|
2476
|
+
try {
|
|
2477
|
+
await session.send("Emulation.setLocaleOverride", { locale });
|
|
2478
|
+
} catch (err) {
|
|
2479
|
+
if (String(err).includes("Another locale override is already in effect")) return;
|
|
2480
|
+
throw err;
|
|
2481
|
+
}
|
|
2092
2482
|
} finally {
|
|
2093
2483
|
await session.detach().catch(() => {
|
|
2094
2484
|
});
|
|
@@ -2102,9 +2492,18 @@ async function setOfflineViaPlaywright(opts) {
|
|
|
2102
2492
|
async function setTimezoneViaPlaywright(opts) {
|
|
2103
2493
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2104
2494
|
ensurePageState(page);
|
|
2495
|
+
const timezoneId = String(opts.timezoneId ?? "").trim();
|
|
2496
|
+
if (!timezoneId) throw new Error("timezoneId is required");
|
|
2105
2497
|
const session = await page.context().newCDPSession(page);
|
|
2106
2498
|
try {
|
|
2107
|
-
|
|
2499
|
+
try {
|
|
2500
|
+
await session.send("Emulation.setTimezoneOverride", { timezoneId });
|
|
2501
|
+
} catch (err) {
|
|
2502
|
+
const msg = String(err);
|
|
2503
|
+
if (msg.includes("Timezone override is already in effect")) return;
|
|
2504
|
+
if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
|
|
2505
|
+
throw err;
|
|
2506
|
+
}
|
|
2108
2507
|
} finally {
|
|
2109
2508
|
await session.detach().catch(() => {
|
|
2110
2509
|
});
|
|
@@ -2180,24 +2579,38 @@ async function pdfViaPlaywright(opts) {
|
|
|
2180
2579
|
ensurePageState(page);
|
|
2181
2580
|
return { buffer: await page.pdf({ printBackground: true }) };
|
|
2182
2581
|
}
|
|
2183
|
-
|
|
2184
|
-
// src/capture/trace.ts
|
|
2185
2582
|
async function traceStartViaPlaywright(opts) {
|
|
2186
2583
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2187
2584
|
ensurePageState(page);
|
|
2188
2585
|
const context = page.context();
|
|
2586
|
+
const ctxState = ensureContextState(context);
|
|
2587
|
+
if (ctxState.traceActive) {
|
|
2588
|
+
throw new Error("Trace already running. Stop the current trace before starting a new one.");
|
|
2589
|
+
}
|
|
2189
2590
|
await context.tracing.start({
|
|
2190
2591
|
screenshots: opts.screenshots ?? true,
|
|
2191
2592
|
snapshots: opts.snapshots ?? true,
|
|
2192
|
-
sources: opts.sources
|
|
2593
|
+
sources: opts.sources ?? false
|
|
2193
2594
|
});
|
|
2595
|
+
ctxState.traceActive = true;
|
|
2194
2596
|
}
|
|
2195
2597
|
async function traceStopViaPlaywright(opts) {
|
|
2196
2598
|
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
2197
2599
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2198
2600
|
ensurePageState(page);
|
|
2199
2601
|
const context = page.context();
|
|
2200
|
-
|
|
2602
|
+
const ctxState = ensureContextState(context);
|
|
2603
|
+
if (!ctxState.traceActive) {
|
|
2604
|
+
throw new Error("No active trace. Start a trace before stopping it.");
|
|
2605
|
+
}
|
|
2606
|
+
await writeViaSiblingTempPath({
|
|
2607
|
+
rootDir: path.dirname(opts.path),
|
|
2608
|
+
targetPath: opts.path,
|
|
2609
|
+
writeTemp: async (tempPath) => {
|
|
2610
|
+
await context.tracing.stop({ path: tempPath });
|
|
2611
|
+
}
|
|
2612
|
+
});
|
|
2613
|
+
ctxState.traceActive = false;
|
|
2201
2614
|
}
|
|
2202
2615
|
|
|
2203
2616
|
// src/capture/response.ts
|
|
@@ -2280,8 +2693,8 @@ async function cookiesSetViaPlaywright(opts) {
|
|
|
2280
2693
|
const cookie = opts.cookie;
|
|
2281
2694
|
if (!cookie.name || cookie.value === void 0) throw new Error("cookie name and value are required");
|
|
2282
2695
|
const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
|
|
2283
|
-
const
|
|
2284
|
-
if (!hasUrl && !
|
|
2696
|
+
const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() && typeof cookie.path === "string" && cookie.path.trim();
|
|
2697
|
+
if (!hasUrl && !hasDomainPath) throw new Error("cookie requires url, or domain+path");
|
|
2285
2698
|
await page.context().addCookies([cookie]);
|
|
2286
2699
|
}
|
|
2287
2700
|
async function cookiesClearViaPlaywright(opts) {
|
|
@@ -3360,11 +3773,17 @@ exports.InvalidBrowserNavigationUrlError = InvalidBrowserNavigationUrlError;
|
|
|
3360
3773
|
exports.assertBrowserNavigationAllowed = assertBrowserNavigationAllowed;
|
|
3361
3774
|
exports.assertBrowserNavigationRedirectChainAllowed = assertBrowserNavigationRedirectChainAllowed;
|
|
3362
3775
|
exports.assertBrowserNavigationResultAllowed = assertBrowserNavigationResultAllowed;
|
|
3776
|
+
exports.createPinnedLookup = createPinnedLookup;
|
|
3777
|
+
exports.ensureContextState = ensureContextState;
|
|
3778
|
+
exports.forceDisconnectPlaywrightForTarget = forceDisconnectPlaywrightForTarget;
|
|
3363
3779
|
exports.getChromeWebSocketUrl = getChromeWebSocketUrl;
|
|
3364
3780
|
exports.isChromeCdpReady = isChromeCdpReady;
|
|
3365
3781
|
exports.isChromeReachable = isChromeReachable;
|
|
3366
3782
|
exports.normalizeCdpHttpBaseForJsonEndpoints = normalizeCdpHttpBaseForJsonEndpoints;
|
|
3367
3783
|
exports.requiresInspectableBrowserNavigationRedirects = requiresInspectableBrowserNavigationRedirects;
|
|
3784
|
+
exports.resolvePinnedHostnameWithPolicy = resolvePinnedHostnameWithPolicy;
|
|
3785
|
+
exports.sanitizeUntrustedFileName = sanitizeUntrustedFileName;
|
|
3368
3786
|
exports.withBrowserNavigationPolicy = withBrowserNavigationPolicy;
|
|
3787
|
+
exports.writeViaSiblingTempPath = writeViaSiblingTempPath;
|
|
3369
3788
|
//# sourceMappingURL=index.cjs.map
|
|
3370
3789
|
//# sourceMappingURL=index.cjs.map
|