browserclaw 0.5.3 → 0.5.5
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 +324 -194
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +324 -194
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -829,15 +829,16 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
829
829
|
const headers = {};
|
|
830
830
|
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
831
831
|
const browser = await playwrightCore.chromium.connectOverCDP(endpoint, { timeout, headers });
|
|
832
|
-
const
|
|
833
|
-
cached = connected;
|
|
834
|
-
observeBrowser(browser);
|
|
835
|
-
browser.on("disconnected", () => {
|
|
832
|
+
const onDisconnected = () => {
|
|
836
833
|
if (cached?.browser === browser) cached = null;
|
|
837
834
|
for (const key of roleRefsByTarget.keys()) {
|
|
838
835
|
if (key.startsWith(normalized + "::")) roleRefsByTarget.delete(key);
|
|
839
836
|
}
|
|
840
|
-
}
|
|
837
|
+
};
|
|
838
|
+
const connected = { browser, cdpUrl: normalized, authToken, onDisconnected };
|
|
839
|
+
cached = connected;
|
|
840
|
+
observeBrowser(browser);
|
|
841
|
+
browser.on("disconnected", onDisconnected);
|
|
841
842
|
return connected;
|
|
842
843
|
} catch (err) {
|
|
843
844
|
lastErr = err;
|
|
@@ -867,6 +868,14 @@ async function disconnectBrowser() {
|
|
|
867
868
|
if (cur) await cur.browser.close().catch(() => {
|
|
868
869
|
});
|
|
869
870
|
}
|
|
871
|
+
function cdpSocketNeedsAttach(wsUrl) {
|
|
872
|
+
try {
|
|
873
|
+
const pathname = new URL(wsUrl).pathname;
|
|
874
|
+
return pathname === "/cdp" || pathname.endsWith("/cdp") || pathname.includes("/devtools/browser/") || pathname === "/";
|
|
875
|
+
} catch {
|
|
876
|
+
return false;
|
|
877
|
+
}
|
|
878
|
+
}
|
|
870
879
|
async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
871
880
|
const httpBase = normalizeCdpHttpBaseForJsonEndpoints(cdpUrl);
|
|
872
881
|
const ctrl = new AbortController();
|
|
@@ -883,8 +892,10 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
883
892
|
}
|
|
884
893
|
if (!Array.isArray(targets)) return;
|
|
885
894
|
const target = targets.find((entry) => String(entry?.id ?? "").trim() === targetId);
|
|
886
|
-
const
|
|
887
|
-
if (!
|
|
895
|
+
const wsUrlRaw = String(target?.webSocketDebuggerUrl ?? "").trim();
|
|
896
|
+
if (!wsUrlRaw) return;
|
|
897
|
+
const wsUrl = normalizeCdpWsUrl(wsUrlRaw, httpBase);
|
|
898
|
+
const needsAttach = cdpSocketNeedsAttach(wsUrl);
|
|
888
899
|
await new Promise((resolve2) => {
|
|
889
900
|
let done = false;
|
|
890
901
|
const finish = () => {
|
|
@@ -897,8 +908,9 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
897
908
|
}
|
|
898
909
|
resolve2();
|
|
899
910
|
};
|
|
900
|
-
const timer = setTimeout(finish,
|
|
911
|
+
const timer = setTimeout(finish, 3e3);
|
|
901
912
|
let ws;
|
|
913
|
+
let nextId = 1;
|
|
902
914
|
try {
|
|
903
915
|
ws = new WebSocket(wsUrl);
|
|
904
916
|
} catch {
|
|
@@ -906,11 +918,27 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
906
918
|
return;
|
|
907
919
|
}
|
|
908
920
|
ws.onopen = () => {
|
|
921
|
+
if (needsAttach) {
|
|
922
|
+
ws.send(JSON.stringify({ id: nextId++, method: "Target.attachToTarget", params: { targetId, flatten: true } }));
|
|
923
|
+
} else {
|
|
924
|
+
ws.send(JSON.stringify({ id: nextId++, method: "Runtime.terminateExecution" }));
|
|
925
|
+
setTimeout(finish, 300);
|
|
926
|
+
}
|
|
927
|
+
};
|
|
928
|
+
ws.onmessage = (event) => {
|
|
929
|
+
if (!needsAttach) return;
|
|
909
930
|
try {
|
|
910
|
-
|
|
931
|
+
const msg = JSON.parse(String(event.data));
|
|
932
|
+
if (msg.id && msg.result?.sessionId) {
|
|
933
|
+
ws.send(JSON.stringify({ id: nextId++, sessionId: msg.result.sessionId, method: "Runtime.terminateExecution" }));
|
|
934
|
+
try {
|
|
935
|
+
ws.send(JSON.stringify({ id: nextId++, method: "Target.detachFromTarget", params: { sessionId: msg.result.sessionId } }));
|
|
936
|
+
} catch {
|
|
937
|
+
}
|
|
938
|
+
setTimeout(finish, 300);
|
|
939
|
+
}
|
|
911
940
|
} catch {
|
|
912
941
|
}
|
|
913
|
-
setTimeout(finish, 300);
|
|
914
942
|
};
|
|
915
943
|
ws.onerror = () => finish();
|
|
916
944
|
ws.onclose = () => finish();
|
|
@@ -922,6 +950,9 @@ async function forceDisconnectPlaywrightForTarget(opts) {
|
|
|
922
950
|
if (!cur || cur.cdpUrl !== normalized) return;
|
|
923
951
|
cached = null;
|
|
924
952
|
connectingByUrl.delete(normalized);
|
|
953
|
+
if (cur.onDisconnected && typeof cur.browser.off === "function") {
|
|
954
|
+
cur.browser.off("disconnected", cur.onDisconnected);
|
|
955
|
+
}
|
|
925
956
|
const targetId = opts.targetId?.trim() || "";
|
|
926
957
|
if (targetId) {
|
|
927
958
|
await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
|
|
@@ -1512,20 +1543,6 @@ function isBlockedHostnameNormalized(normalized) {
|
|
|
1512
1543
|
if (BLOCKED_HOSTNAMES.has(normalized)) return true;
|
|
1513
1544
|
return normalized.endsWith(".localhost") || normalized.endsWith(".local") || normalized.endsWith(".internal");
|
|
1514
1545
|
}
|
|
1515
|
-
function isStrictDecimalOctet(part) {
|
|
1516
|
-
if (!/^[0-9]+$/.test(part)) return false;
|
|
1517
|
-
const n = parseInt(part, 10);
|
|
1518
|
-
if (n < 0 || n > 255) return false;
|
|
1519
|
-
if (String(n) !== part) return false;
|
|
1520
|
-
return true;
|
|
1521
|
-
}
|
|
1522
|
-
function isUnsupportedIPv4Literal(ip) {
|
|
1523
|
-
if (/^[0-9]+$/.test(ip)) return true;
|
|
1524
|
-
const parts = ip.split(".");
|
|
1525
|
-
if (parts.length !== 4) return true;
|
|
1526
|
-
if (!parts.every(isStrictDecimalOctet)) return true;
|
|
1527
|
-
return false;
|
|
1528
|
-
}
|
|
1529
1546
|
var BLOCKED_IPV4_RANGES = /* @__PURE__ */ new Set([
|
|
1530
1547
|
"unspecified",
|
|
1531
1548
|
"broadcast",
|
|
@@ -1544,6 +1561,97 @@ var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set([
|
|
|
1544
1561
|
"multicast"
|
|
1545
1562
|
]);
|
|
1546
1563
|
var RFC2544_BENCHMARK_PREFIX = [ipaddr__namespace.IPv4.parse("198.18.0.0"), 15];
|
|
1564
|
+
var EMBEDDED_IPV4_SENTINEL_RULES = [
|
|
1565
|
+
// IPv4-compatible (::a.b.c.d)
|
|
1566
|
+
{
|
|
1567
|
+
matches: (parts) => parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0,
|
|
1568
|
+
toHextets: (parts) => [parts[6], parts[7]]
|
|
1569
|
+
},
|
|
1570
|
+
// NAT64 local-use (64:ff9b:1::/48)
|
|
1571
|
+
{
|
|
1572
|
+
matches: (parts) => parts[0] === 100 && parts[1] === 65435 && parts[2] === 1 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0,
|
|
1573
|
+
toHextets: (parts) => [parts[6], parts[7]]
|
|
1574
|
+
},
|
|
1575
|
+
// 6to4 (2002::/16)
|
|
1576
|
+
{
|
|
1577
|
+
matches: (parts) => parts[0] === 8194,
|
|
1578
|
+
toHextets: (parts) => [parts[1], parts[2]]
|
|
1579
|
+
},
|
|
1580
|
+
// Teredo (2001:0000::/32) — IPv4 XOR'd
|
|
1581
|
+
{
|
|
1582
|
+
matches: (parts) => parts[0] === 8193 && parts[1] === 0,
|
|
1583
|
+
toHextets: (parts) => [parts[6] ^ 65535, parts[7] ^ 65535]
|
|
1584
|
+
},
|
|
1585
|
+
// ISATAP — sentinel in parts[4-5]: 0x0000:0x5efe or 0x0200:0x5efe
|
|
1586
|
+
{
|
|
1587
|
+
matches: (parts) => (parts[4] & 64767) === 0 && parts[5] === 24318,
|
|
1588
|
+
toHextets: (parts) => [parts[6], parts[7]]
|
|
1589
|
+
}
|
|
1590
|
+
];
|
|
1591
|
+
function stripIpv6Brackets(value) {
|
|
1592
|
+
if (value.startsWith("[") && value.endsWith("]")) return value.slice(1, -1);
|
|
1593
|
+
return value;
|
|
1594
|
+
}
|
|
1595
|
+
function isNumericIpv4LiteralPart(value) {
|
|
1596
|
+
return /^[0-9]+$/.test(value) || /^0x[0-9a-f]+$/i.test(value);
|
|
1597
|
+
}
|
|
1598
|
+
function parseIpv6WithEmbeddedIpv4(raw) {
|
|
1599
|
+
if (!raw.includes(":") || !raw.includes(".")) return;
|
|
1600
|
+
const match = /^(.*:)([^:%]+(?:\.[^:%]+){3})(%[0-9A-Za-z]+)?$/i.exec(raw);
|
|
1601
|
+
if (!match) return;
|
|
1602
|
+
const [, prefix, embeddedIpv4, zoneSuffix = ""] = match;
|
|
1603
|
+
if (!ipaddr__namespace.IPv4.isValidFourPartDecimal(embeddedIpv4)) return;
|
|
1604
|
+
const octets = embeddedIpv4.split(".").map((part) => Number.parseInt(part, 10));
|
|
1605
|
+
const normalizedIpv6 = `${prefix}${(octets[0] << 8 | octets[1]).toString(16)}:${(octets[2] << 8 | octets[3]).toString(16)}${zoneSuffix}`;
|
|
1606
|
+
if (!ipaddr__namespace.IPv6.isValid(normalizedIpv6)) return;
|
|
1607
|
+
return ipaddr__namespace.IPv6.parse(normalizedIpv6);
|
|
1608
|
+
}
|
|
1609
|
+
function normalizeIpParseInput(raw) {
|
|
1610
|
+
const trimmed = raw?.trim();
|
|
1611
|
+
if (!trimmed) return;
|
|
1612
|
+
return stripIpv6Brackets(trimmed);
|
|
1613
|
+
}
|
|
1614
|
+
function parseCanonicalIpAddress(raw) {
|
|
1615
|
+
const normalized = normalizeIpParseInput(raw);
|
|
1616
|
+
if (!normalized) return;
|
|
1617
|
+
if (ipaddr__namespace.IPv4.isValid(normalized)) {
|
|
1618
|
+
if (!ipaddr__namespace.IPv4.isValidFourPartDecimal(normalized)) return;
|
|
1619
|
+
return ipaddr__namespace.IPv4.parse(normalized);
|
|
1620
|
+
}
|
|
1621
|
+
if (ipaddr__namespace.IPv6.isValid(normalized)) return ipaddr__namespace.IPv6.parse(normalized);
|
|
1622
|
+
return parseIpv6WithEmbeddedIpv4(normalized);
|
|
1623
|
+
}
|
|
1624
|
+
function parseLooseIpAddress(raw) {
|
|
1625
|
+
const normalized = normalizeIpParseInput(raw);
|
|
1626
|
+
if (!normalized) return;
|
|
1627
|
+
if (ipaddr__namespace.isValid(normalized)) return ipaddr__namespace.parse(normalized);
|
|
1628
|
+
return parseIpv6WithEmbeddedIpv4(normalized);
|
|
1629
|
+
}
|
|
1630
|
+
function isCanonicalDottedDecimalIPv4(raw) {
|
|
1631
|
+
const trimmed = raw?.trim();
|
|
1632
|
+
if (!trimmed) return false;
|
|
1633
|
+
const normalized = stripIpv6Brackets(trimmed);
|
|
1634
|
+
if (!normalized) return false;
|
|
1635
|
+
return ipaddr__namespace.IPv4.isValidFourPartDecimal(normalized);
|
|
1636
|
+
}
|
|
1637
|
+
function isLegacyIpv4Literal(raw) {
|
|
1638
|
+
const trimmed = raw?.trim();
|
|
1639
|
+
if (!trimmed) return false;
|
|
1640
|
+
const normalized = stripIpv6Brackets(trimmed);
|
|
1641
|
+
if (!normalized || normalized.includes(":")) return false;
|
|
1642
|
+
if (isCanonicalDottedDecimalIPv4(normalized)) return false;
|
|
1643
|
+
const parts = normalized.split(".");
|
|
1644
|
+
if (parts.length === 0 || parts.length > 4) return false;
|
|
1645
|
+
if (parts.some((part) => part.length === 0)) return false;
|
|
1646
|
+
if (!parts.every((part) => isNumericIpv4LiteralPart(part))) return false;
|
|
1647
|
+
return true;
|
|
1648
|
+
}
|
|
1649
|
+
function looksLikeUnsupportedIpv4Literal(address) {
|
|
1650
|
+
const parts = address.split(".");
|
|
1651
|
+
if (parts.length === 0 || parts.length > 4) return false;
|
|
1652
|
+
if (parts.some((part) => part.length === 0)) return true;
|
|
1653
|
+
return parts.every((part) => /^[0-9]+$/.test(part) || /^0x/i.test(part));
|
|
1654
|
+
}
|
|
1547
1655
|
function isBlockedSpecialUseIpv4Address(address, opts) {
|
|
1548
1656
|
const inRfc2544 = address.match(RFC2544_BENCHMARK_PREFIX);
|
|
1549
1657
|
if (inRfc2544 && opts?.allowRfc2544BenchmarkRange === true) return false;
|
|
@@ -1553,89 +1661,50 @@ function isBlockedSpecialUseIpv6Address(address) {
|
|
|
1553
1661
|
if (BLOCKED_IPV6_RANGES.has(address.range())) return true;
|
|
1554
1662
|
return (address.parts[0] & 65472) === 65216;
|
|
1555
1663
|
}
|
|
1556
|
-
function
|
|
1557
|
-
|
|
1558
|
-
|
|
1559
|
-
|
|
1560
|
-
|
|
1561
|
-
|
|
1562
|
-
|
|
1563
|
-
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
|
|
1571
|
-
|
|
1572
|
-
|
|
1573
|
-
} catch {
|
|
1574
|
-
return true;
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
if (parts[0] === 8194) {
|
|
1578
|
-
const ip4str = `${parts[1] >> 8 & 255}.${parts[1] & 255}.${parts[2] >> 8 & 255}.${parts[2] & 255}`;
|
|
1579
|
-
try {
|
|
1580
|
-
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1581
|
-
} catch {
|
|
1582
|
-
return true;
|
|
1583
|
-
}
|
|
1584
|
-
}
|
|
1585
|
-
if (parts[0] === 8193 && parts[1] === 0) {
|
|
1586
|
-
const hiXored = parts[6] ^ 65535;
|
|
1587
|
-
const loXored = parts[7] ^ 65535;
|
|
1588
|
-
const ip4str = `${hiXored >> 8 & 255}.${hiXored & 255}.${loXored >> 8 & 255}.${loXored & 255}`;
|
|
1589
|
-
try {
|
|
1590
|
-
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1591
|
-
} catch {
|
|
1592
|
-
return true;
|
|
1593
|
-
}
|
|
1594
|
-
}
|
|
1595
|
-
if (parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 65535 && parts[5] === 0) {
|
|
1596
|
-
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1597
|
-
try {
|
|
1598
|
-
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1599
|
-
} catch {
|
|
1600
|
-
return true;
|
|
1601
|
-
}
|
|
1602
|
-
}
|
|
1603
|
-
if (parts[0] === 0 && parts[1] === 0 && parts[2] === 0 && parts[3] === 0 && parts[4] === 0 && parts[5] === 0) {
|
|
1604
|
-
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1605
|
-
try {
|
|
1606
|
-
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1607
|
-
} catch {
|
|
1608
|
-
return true;
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
if ((parts[4] & 65023) === 0 && parts[5] === 24318) {
|
|
1612
|
-
const ip4str = `${parts[6] >> 8 & 255}.${parts[6] & 255}.${parts[7] >> 8 & 255}.${parts[7] & 255}`;
|
|
1613
|
-
try {
|
|
1614
|
-
return isBlockedSpecialUseIpv4Address(ipaddr__namespace.IPv4.parse(ip4str), opts);
|
|
1615
|
-
} catch {
|
|
1616
|
-
return true;
|
|
1617
|
-
}
|
|
1664
|
+
function decodeIpv4FromHextets(high, low) {
|
|
1665
|
+
const octets = [
|
|
1666
|
+
high >>> 8 & 255,
|
|
1667
|
+
high & 255,
|
|
1668
|
+
low >>> 8 & 255,
|
|
1669
|
+
low & 255
|
|
1670
|
+
];
|
|
1671
|
+
return ipaddr__namespace.IPv4.parse(octets.join("."));
|
|
1672
|
+
}
|
|
1673
|
+
function extractEmbeddedIpv4FromIpv6(address) {
|
|
1674
|
+
if (address.isIPv4MappedAddress()) return address.toIPv4Address();
|
|
1675
|
+
if (address.range() === "rfc6145") return decodeIpv4FromHextets(address.parts[6], address.parts[7]);
|
|
1676
|
+
if (address.range() === "rfc6052") return decodeIpv4FromHextets(address.parts[6], address.parts[7]);
|
|
1677
|
+
for (const rule of EMBEDDED_IPV4_SENTINEL_RULES) {
|
|
1678
|
+
if (!rule.matches(address.parts)) continue;
|
|
1679
|
+
const [high, low] = rule.toHextets(address.parts);
|
|
1680
|
+
return decodeIpv4FromHextets(high, low);
|
|
1618
1681
|
}
|
|
1619
|
-
return null;
|
|
1620
1682
|
}
|
|
1621
|
-
function
|
|
1683
|
+
function resolveIpv4SpecialUseBlockOptions(policy) {
|
|
1684
|
+
return { allowRfc2544BenchmarkRange: policy?.allowRfc2544BenchmarkRange === true };
|
|
1685
|
+
}
|
|
1686
|
+
function isBlockedHostnameOrIp(hostname, policy) {
|
|
1687
|
+
const normalized = normalizeHostname(hostname);
|
|
1688
|
+
if (!normalized) return false;
|
|
1689
|
+
return isBlockedHostnameNormalized(normalized) || isPrivateIpAddress(normalized, policy);
|
|
1690
|
+
}
|
|
1691
|
+
function isPrivateIpAddress(address, policy) {
|
|
1622
1692
|
let normalized = address.trim().toLowerCase();
|
|
1623
1693
|
if (normalized.startsWith("[") && normalized.endsWith("]")) normalized = normalized.slice(1, -1);
|
|
1624
1694
|
if (!normalized) return false;
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
const v6 = parsed;
|
|
1695
|
+
const blockOptions = resolveIpv4SpecialUseBlockOptions(policy);
|
|
1696
|
+
const strictIp = parseCanonicalIpAddress(normalized);
|
|
1697
|
+
if (strictIp) {
|
|
1698
|
+
if (strictIp.kind() === "ipv4") return isBlockedSpecialUseIpv4Address(strictIp, blockOptions);
|
|
1699
|
+
const v6 = strictIp;
|
|
1631
1700
|
if (isBlockedSpecialUseIpv6Address(v6)) return true;
|
|
1632
|
-
const
|
|
1633
|
-
if (
|
|
1701
|
+
const embeddedIpv4 = extractEmbeddedIpv4FromIpv6(v6);
|
|
1702
|
+
if (embeddedIpv4) return isBlockedSpecialUseIpv4Address(embeddedIpv4, blockOptions);
|
|
1634
1703
|
return false;
|
|
1635
|
-
} catch {
|
|
1636
1704
|
}
|
|
1637
|
-
if (
|
|
1638
|
-
if (normalized
|
|
1705
|
+
if (normalized.includes(":") && !parseLooseIpAddress(normalized)) return true;
|
|
1706
|
+
if (!isCanonicalDottedDecimalIPv4(normalized) && isLegacyIpv4Literal(normalized)) return true;
|
|
1707
|
+
if (looksLikeUnsupportedIpv4Literal(normalized)) return true;
|
|
1639
1708
|
return false;
|
|
1640
1709
|
}
|
|
1641
1710
|
function normalizeHostnameSet(values) {
|
|
@@ -1717,13 +1786,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
1717
1786
|
);
|
|
1718
1787
|
}
|
|
1719
1788
|
if (!skipPrivateNetworkChecks) {
|
|
1720
|
-
if (
|
|
1721
|
-
throw new InvalidBrowserNavigationUrlError(
|
|
1722
|
-
`Navigation to internal/loopback address blocked: "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1723
|
-
);
|
|
1724
|
-
}
|
|
1725
|
-
const ipOpts = { allowRfc2544BenchmarkRange: params.policy?.allowRfc2544BenchmarkRange };
|
|
1726
|
-
if (isPrivateIpAddress(normalized, ipOpts)) {
|
|
1789
|
+
if (isBlockedHostnameOrIp(normalized, params.policy)) {
|
|
1727
1790
|
throw new InvalidBrowserNavigationUrlError(
|
|
1728
1791
|
`Navigation to internal/loopback address blocked: "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1729
1792
|
);
|
|
@@ -1744,9 +1807,8 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
1744
1807
|
);
|
|
1745
1808
|
}
|
|
1746
1809
|
if (!skipPrivateNetworkChecks) {
|
|
1747
|
-
const ipOpts = { allowRfc2544BenchmarkRange: params.policy?.allowRfc2544BenchmarkRange };
|
|
1748
1810
|
for (const r of results) {
|
|
1749
|
-
if (
|
|
1811
|
+
if (isBlockedHostnameOrIp(r.address, params.policy)) {
|
|
1750
1812
|
throw new InvalidBrowserNavigationUrlError(
|
|
1751
1813
|
`Navigation to internal/loopback address blocked: "${hostname}" resolves to "${r.address}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
1752
1814
|
);
|
|
@@ -1767,6 +1829,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
1767
1829
|
}
|
|
1768
1830
|
async function assertBrowserNavigationAllowed(opts) {
|
|
1769
1831
|
const rawUrl = String(opts.url ?? "").trim();
|
|
1832
|
+
if (!rawUrl) throw new InvalidBrowserNavigationUrlError("url is required");
|
|
1770
1833
|
let parsed;
|
|
1771
1834
|
try {
|
|
1772
1835
|
parsed = new URL(rawUrl);
|
|
@@ -1923,12 +1986,30 @@ function resolveBoundedDelayMs(value, label, maxMs) {
|
|
|
1923
1986
|
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
1924
1987
|
return normalized;
|
|
1925
1988
|
}
|
|
1926
|
-
|
|
1989
|
+
function resolveInteractionTimeoutMs(timeoutMs) {
|
|
1990
|
+
return Math.max(500, Math.min(6e4, Math.floor(timeoutMs ?? 8e3)));
|
|
1991
|
+
}
|
|
1992
|
+
function requireRefOrSelector(ref, selector) {
|
|
1993
|
+
const trimmedRef = typeof ref === "string" ? ref.trim() : "";
|
|
1994
|
+
const trimmedSelector = typeof selector === "string" ? selector.trim() : "";
|
|
1995
|
+
if (!trimmedRef && !trimmedSelector) throw new Error("ref or selector is required");
|
|
1996
|
+
return { ref: trimmedRef || void 0, selector: trimmedSelector || void 0 };
|
|
1997
|
+
}
|
|
1998
|
+
function resolveLocator(page, resolved) {
|
|
1999
|
+
return resolved.ref ? refLocator(page, resolved.ref) : page.locator(resolved.selector);
|
|
2000
|
+
}
|
|
2001
|
+
async function getRestoredPageForTarget(opts) {
|
|
1927
2002
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1928
2003
|
ensurePageState(page);
|
|
1929
2004
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
1930
|
-
|
|
1931
|
-
|
|
2005
|
+
return page;
|
|
2006
|
+
}
|
|
2007
|
+
async function clickViaPlaywright(opts) {
|
|
2008
|
+
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2009
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2010
|
+
const label = resolved.ref ?? resolved.selector;
|
|
2011
|
+
const locator = resolveLocator(page, resolved);
|
|
2012
|
+
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
1932
2013
|
try {
|
|
1933
2014
|
const delayMs = resolveBoundedDelayMs(opts.delayMs, "click delayMs", MAX_CLICK_DELAY_MS);
|
|
1934
2015
|
if (delayMs > 0) {
|
|
@@ -1941,28 +2022,27 @@ async function clickViaPlaywright(opts) {
|
|
|
1941
2022
|
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
1942
2023
|
}
|
|
1943
2024
|
} catch (err) {
|
|
1944
|
-
throw toAIFriendlyError(err,
|
|
2025
|
+
throw toAIFriendlyError(err, label);
|
|
1945
2026
|
}
|
|
1946
2027
|
}
|
|
1947
2028
|
async function hoverViaPlaywright(opts) {
|
|
1948
|
-
const
|
|
1949
|
-
|
|
1950
|
-
|
|
2029
|
+
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2030
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2031
|
+
const label = resolved.ref ?? resolved.selector;
|
|
2032
|
+
const locator = resolveLocator(page, resolved);
|
|
1951
2033
|
try {
|
|
1952
|
-
await
|
|
1953
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4)
|
|
1954
|
-
});
|
|
2034
|
+
await locator.hover({ timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
1955
2035
|
} catch (err) {
|
|
1956
|
-
throw toAIFriendlyError(err,
|
|
2036
|
+
throw toAIFriendlyError(err, label);
|
|
1957
2037
|
}
|
|
1958
2038
|
}
|
|
1959
2039
|
async function typeViaPlaywright(opts) {
|
|
2040
|
+
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
1960
2041
|
const text = String(opts.text ?? "");
|
|
1961
|
-
const page = await
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
const
|
|
1965
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4);
|
|
2042
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2043
|
+
const label = resolved.ref ?? resolved.selector;
|
|
2044
|
+
const locator = resolveLocator(page, resolved);
|
|
2045
|
+
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
1966
2046
|
try {
|
|
1967
2047
|
if (opts.slowly) {
|
|
1968
2048
|
await locator.click({ timeout });
|
|
@@ -1972,39 +2052,38 @@ async function typeViaPlaywright(opts) {
|
|
|
1972
2052
|
}
|
|
1973
2053
|
if (opts.submit) await locator.press("Enter", { timeout });
|
|
1974
2054
|
} catch (err) {
|
|
1975
|
-
throw toAIFriendlyError(err,
|
|
2055
|
+
throw toAIFriendlyError(err, label);
|
|
1976
2056
|
}
|
|
1977
2057
|
}
|
|
1978
2058
|
async function selectOptionViaPlaywright(opts) {
|
|
2059
|
+
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
1979
2060
|
if (!opts.values?.length) throw new Error("values are required");
|
|
1980
|
-
const page = await
|
|
1981
|
-
|
|
1982
|
-
|
|
2061
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2062
|
+
const label = resolved.ref ?? resolved.selector;
|
|
2063
|
+
const locator = resolveLocator(page, resolved);
|
|
1983
2064
|
try {
|
|
1984
|
-
await
|
|
1985
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4)
|
|
1986
|
-
});
|
|
2065
|
+
await locator.selectOption(opts.values, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
1987
2066
|
} catch (err) {
|
|
1988
|
-
throw toAIFriendlyError(err,
|
|
2067
|
+
throw toAIFriendlyError(err, label);
|
|
1989
2068
|
}
|
|
1990
2069
|
}
|
|
1991
2070
|
async function dragViaPlaywright(opts) {
|
|
1992
|
-
const
|
|
1993
|
-
|
|
1994
|
-
|
|
2071
|
+
const resolvedStart = requireRefOrSelector(opts.startRef, opts.startSelector);
|
|
2072
|
+
const resolvedEnd = requireRefOrSelector(opts.endRef, opts.endSelector);
|
|
2073
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2074
|
+
const startLocator = resolveLocator(page, resolvedStart);
|
|
2075
|
+
const endLocator = resolveLocator(page, resolvedEnd);
|
|
2076
|
+
const startLabel = resolvedStart.ref ?? resolvedStart.selector;
|
|
2077
|
+
const endLabel = resolvedEnd.ref ?? resolvedEnd.selector;
|
|
1995
2078
|
try {
|
|
1996
|
-
await
|
|
1997
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4)
|
|
1998
|
-
});
|
|
2079
|
+
await startLocator.dragTo(endLocator, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
1999
2080
|
} catch (err) {
|
|
2000
|
-
throw toAIFriendlyError(err, `${
|
|
2081
|
+
throw toAIFriendlyError(err, `${startLabel} -> ${endLabel}`);
|
|
2001
2082
|
}
|
|
2002
2083
|
}
|
|
2003
2084
|
async function fillFormViaPlaywright(opts) {
|
|
2004
|
-
const page = await
|
|
2005
|
-
|
|
2006
|
-
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2007
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs, 8e3, 6e4);
|
|
2085
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2086
|
+
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
2008
2087
|
for (const field of opts.fields) {
|
|
2009
2088
|
const ref = field.ref.trim();
|
|
2010
2089
|
const type = (typeof field.type === "string" ? field.type.trim() : "") || "text";
|
|
@@ -2029,21 +2108,18 @@ async function fillFormViaPlaywright(opts) {
|
|
|
2029
2108
|
}
|
|
2030
2109
|
}
|
|
2031
2110
|
async function scrollIntoViewViaPlaywright(opts) {
|
|
2032
|
-
const
|
|
2033
|
-
|
|
2034
|
-
|
|
2111
|
+
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2112
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2113
|
+
const label = resolved.ref ?? resolved.selector;
|
|
2114
|
+
const locator = resolveLocator(page, resolved);
|
|
2035
2115
|
try {
|
|
2036
|
-
await
|
|
2037
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4)
|
|
2038
|
-
});
|
|
2116
|
+
await locator.scrollIntoViewIfNeeded({ timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
|
|
2039
2117
|
} catch (err) {
|
|
2040
|
-
throw toAIFriendlyError(err,
|
|
2118
|
+
throw toAIFriendlyError(err, label);
|
|
2041
2119
|
}
|
|
2042
2120
|
}
|
|
2043
2121
|
async function highlightViaPlaywright(opts) {
|
|
2044
|
-
const page = await
|
|
2045
|
-
ensurePageState(page);
|
|
2046
|
-
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2122
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2047
2123
|
try {
|
|
2048
2124
|
await refLocator(page, opts.ref).highlight();
|
|
2049
2125
|
} catch (err) {
|
|
@@ -2051,9 +2127,7 @@ async function highlightViaPlaywright(opts) {
|
|
|
2051
2127
|
}
|
|
2052
2128
|
}
|
|
2053
2129
|
async function setInputFilesViaPlaywright(opts) {
|
|
2054
|
-
const page = await
|
|
2055
|
-
ensurePageState(page);
|
|
2056
|
-
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2130
|
+
const page = await getRestoredPageForTarget(opts);
|
|
2057
2131
|
if (!opts.paths.length) throw new Error("paths are required");
|
|
2058
2132
|
const inputRef = typeof opts.ref === "string" ? opts.ref.trim() : "";
|
|
2059
2133
|
const element = typeof opts.element === "string" ? opts.element.trim() : "";
|
|
@@ -2246,13 +2320,18 @@ async function resizeViewportViaPlaywright(opts) {
|
|
|
2246
2320
|
|
|
2247
2321
|
// src/actions/wait.ts
|
|
2248
2322
|
var MAX_WAIT_TIME_MS = 3e4;
|
|
2323
|
+
function resolveBoundedDelayMs2(value, label, maxMs) {
|
|
2324
|
+
const normalized = Math.floor(value ?? 0);
|
|
2325
|
+
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
2326
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
2327
|
+
return normalized;
|
|
2328
|
+
}
|
|
2249
2329
|
async function waitForViaPlaywright(opts) {
|
|
2250
2330
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2251
2331
|
ensurePageState(page);
|
|
2252
2332
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
2253
2333
|
if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
|
|
2254
|
-
|
|
2255
|
-
await page.waitForTimeout(bounded);
|
|
2334
|
+
await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
|
|
2256
2335
|
}
|
|
2257
2336
|
if (opts.text) {
|
|
2258
2337
|
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
@@ -2414,55 +2493,106 @@ async function evaluateViaPlaywright(opts) {
|
|
|
2414
2493
|
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
2415
2494
|
}
|
|
2416
2495
|
}
|
|
2496
|
+
function createPageDownloadWaiter(page, timeoutMs) {
|
|
2497
|
+
let done = false;
|
|
2498
|
+
let timer;
|
|
2499
|
+
let handler;
|
|
2500
|
+
const cleanup = () => {
|
|
2501
|
+
if (timer) clearTimeout(timer);
|
|
2502
|
+
timer = void 0;
|
|
2503
|
+
if (handler) {
|
|
2504
|
+
page.off("download", handler);
|
|
2505
|
+
handler = void 0;
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
return {
|
|
2509
|
+
promise: new Promise((resolve2, reject) => {
|
|
2510
|
+
handler = (download) => {
|
|
2511
|
+
if (done) return;
|
|
2512
|
+
done = true;
|
|
2513
|
+
cleanup();
|
|
2514
|
+
resolve2(download);
|
|
2515
|
+
};
|
|
2516
|
+
page.on("download", handler);
|
|
2517
|
+
timer = setTimeout(() => {
|
|
2518
|
+
if (done) return;
|
|
2519
|
+
done = true;
|
|
2520
|
+
cleanup();
|
|
2521
|
+
reject(new Error("Timeout waiting for download"));
|
|
2522
|
+
}, timeoutMs);
|
|
2523
|
+
}),
|
|
2524
|
+
cancel: () => {
|
|
2525
|
+
if (done) return;
|
|
2526
|
+
done = true;
|
|
2527
|
+
cleanup();
|
|
2528
|
+
}
|
|
2529
|
+
};
|
|
2530
|
+
}
|
|
2531
|
+
async function saveDownloadPayload(download, outPath) {
|
|
2532
|
+
await writeViaSiblingTempPath({
|
|
2533
|
+
rootDir: path.dirname(outPath),
|
|
2534
|
+
targetPath: outPath,
|
|
2535
|
+
writeTemp: async (tempPath) => {
|
|
2536
|
+
await download.saveAs(tempPath);
|
|
2537
|
+
}
|
|
2538
|
+
});
|
|
2539
|
+
return {
|
|
2540
|
+
url: download.url(),
|
|
2541
|
+
suggestedFilename: download.suggestedFilename(),
|
|
2542
|
+
path: outPath
|
|
2543
|
+
};
|
|
2544
|
+
}
|
|
2545
|
+
async function awaitDownloadPayload(params) {
|
|
2546
|
+
try {
|
|
2547
|
+
const download = await params.waiter.promise;
|
|
2548
|
+
if (params.state.armIdDownload !== params.armId) throw new Error("Download was superseded by another waiter");
|
|
2549
|
+
return await saveDownloadPayload(download, params.outPath);
|
|
2550
|
+
} catch (err) {
|
|
2551
|
+
params.waiter.cancel();
|
|
2552
|
+
throw err;
|
|
2553
|
+
}
|
|
2554
|
+
}
|
|
2417
2555
|
async function downloadViaPlaywright(opts) {
|
|
2418
2556
|
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
2419
2557
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2420
2558
|
const state = ensurePageState(page);
|
|
2421
2559
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2422
2560
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
2423
|
-
const
|
|
2561
|
+
const outPath = String(opts.path ?? "").trim();
|
|
2562
|
+
if (!outPath) throw new Error("path is required");
|
|
2424
2563
|
state.armIdDownload = bumpDownloadArmId();
|
|
2564
|
+
const armId = state.armIdDownload;
|
|
2565
|
+
const waiter = createPageDownloadWaiter(page, timeout);
|
|
2425
2566
|
try {
|
|
2426
|
-
const
|
|
2427
|
-
|
|
2428
|
-
locator.click({ timeout })
|
|
2429
|
-
|
|
2430
|
-
|
|
2431
|
-
|
|
2432
|
-
|
|
2433
|
-
targetPath: outPath,
|
|
2434
|
-
writeTemp: async (tempPath) => {
|
|
2435
|
-
await download.saveAs(tempPath);
|
|
2436
|
-
}
|
|
2437
|
-
});
|
|
2438
|
-
return {
|
|
2439
|
-
url: download.url(),
|
|
2440
|
-
suggestedFilename: download.suggestedFilename(),
|
|
2441
|
-
path: outPath
|
|
2442
|
-
};
|
|
2567
|
+
const locator = refLocator(page, opts.ref);
|
|
2568
|
+
try {
|
|
2569
|
+
await locator.click({ timeout });
|
|
2570
|
+
} catch (err) {
|
|
2571
|
+
throw toAIFriendlyError(err, opts.ref);
|
|
2572
|
+
}
|
|
2573
|
+
return await awaitDownloadPayload({ waiter, state, armId, outPath });
|
|
2443
2574
|
} catch (err) {
|
|
2444
|
-
|
|
2575
|
+
waiter.cancel();
|
|
2576
|
+
throw err;
|
|
2445
2577
|
}
|
|
2446
2578
|
}
|
|
2447
2579
|
async function waitForDownloadViaPlaywright(opts) {
|
|
2448
2580
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2449
|
-
ensurePageState(page);
|
|
2581
|
+
const state = ensurePageState(page);
|
|
2450
2582
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
2451
|
-
|
|
2452
|
-
const
|
|
2453
|
-
|
|
2454
|
-
|
|
2455
|
-
|
|
2456
|
-
|
|
2457
|
-
|
|
2458
|
-
|
|
2459
|
-
|
|
2460
|
-
})
|
|
2461
|
-
|
|
2462
|
-
|
|
2463
|
-
|
|
2464
|
-
path: savePath
|
|
2465
|
-
};
|
|
2583
|
+
state.armIdDownload = bumpDownloadArmId();
|
|
2584
|
+
const armId = state.armIdDownload;
|
|
2585
|
+
const waiter = createPageDownloadWaiter(page, timeout);
|
|
2586
|
+
try {
|
|
2587
|
+
const download = await waiter.promise;
|
|
2588
|
+
if (state.armIdDownload !== armId) throw new Error("Download was superseded by another waiter");
|
|
2589
|
+
const savePath = opts.path ?? download.suggestedFilename();
|
|
2590
|
+
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
2591
|
+
return await saveDownloadPayload(download, savePath);
|
|
2592
|
+
} catch (err) {
|
|
2593
|
+
waiter.cancel();
|
|
2594
|
+
throw err;
|
|
2595
|
+
}
|
|
2466
2596
|
}
|
|
2467
2597
|
async function emulateMediaViaPlaywright(opts) {
|
|
2468
2598
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
@@ -2577,7 +2707,7 @@ async function setLocaleViaPlaywright(opts) {
|
|
|
2577
2707
|
async function setOfflineViaPlaywright(opts) {
|
|
2578
2708
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2579
2709
|
ensurePageState(page);
|
|
2580
|
-
await page.context().setOffline(opts.offline);
|
|
2710
|
+
await page.context().setOffline(Boolean(opts.offline));
|
|
2581
2711
|
}
|
|
2582
2712
|
async function setTimezoneViaPlaywright(opts) {
|
|
2583
2713
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|