browserclaw 0.5.8 → 0.7.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 +1566 -1144
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +111 -21
- package/dist/index.d.ts +111 -21
- package/dist/index.js +1556 -1137
- package/dist/index.js.map +1 -1
- package/package.json +14 -2
package/dist/index.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
|
-
import os from 'os';
|
|
2
|
-
import path, { posix, win32, resolve, dirname, join, basename, relative, sep, normalize } from 'path';
|
|
3
|
-
import fs from 'fs';
|
|
4
|
-
import net from 'net';
|
|
5
|
-
import { spawn, execFileSync } from 'child_process';
|
|
6
|
-
import { devices, chromium } from 'playwright-core';
|
|
7
1
|
import http from 'http';
|
|
8
2
|
import https from 'https';
|
|
9
|
-
import {
|
|
3
|
+
import { devices, chromium } from 'playwright-core';
|
|
4
|
+
import { spawn, execFileSync } from 'child_process';
|
|
5
|
+
import fs from 'fs';
|
|
6
|
+
import net from 'net';
|
|
7
|
+
import os from 'os';
|
|
8
|
+
import path, { posix, win32, resolve, dirname, join, basename, relative, sep, normalize } from 'path';
|
|
9
|
+
import { randomUUID } from 'crypto';
|
|
10
10
|
import { lookup } from 'dns';
|
|
11
|
+
import { lookup as lookup$1 } from 'dns/promises';
|
|
11
12
|
import { lstat, realpath, rename, rm } from 'fs/promises';
|
|
12
|
-
import { randomUUID } from 'crypto';
|
|
13
13
|
|
|
14
14
|
var __create = Object.create;
|
|
15
15
|
var __defProp = Object.defineProperty;
|
|
@@ -910,7 +910,7 @@ function execText(command, args, timeoutMs = 1200) {
|
|
|
910
910
|
encoding: "utf8",
|
|
911
911
|
maxBuffer: 1024 * 1024
|
|
912
912
|
});
|
|
913
|
-
return
|
|
913
|
+
return output.trim() || null;
|
|
914
914
|
} catch {
|
|
915
915
|
return null;
|
|
916
916
|
}
|
|
@@ -921,7 +921,8 @@ function inferKindFromIdentifier(identifier) {
|
|
|
921
921
|
if (id.includes("edge")) return "edge";
|
|
922
922
|
if (id.includes("chromium")) return "chromium";
|
|
923
923
|
if (id.includes("canary")) return "canary";
|
|
924
|
-
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser"))
|
|
924
|
+
if (id.includes("opera") || id.includes("vivaldi") || id.includes("yandex") || id.includes("thebrowser"))
|
|
925
|
+
return "chromium";
|
|
925
926
|
return "chrome";
|
|
926
927
|
}
|
|
927
928
|
function inferKindFromExeName(name) {
|
|
@@ -938,24 +939,29 @@ function findFirstExe(candidates) {
|
|
|
938
939
|
return null;
|
|
939
940
|
}
|
|
940
941
|
function detectDefaultBrowserBundleIdMac() {
|
|
941
|
-
const plistPath = path.join(
|
|
942
|
+
const plistPath = path.join(
|
|
943
|
+
os.homedir(),
|
|
944
|
+
"Library/Preferences/com.apple.LaunchServices/com.apple.launchservices.secure.plist"
|
|
945
|
+
);
|
|
942
946
|
if (!fileExists(plistPath)) return null;
|
|
943
947
|
const handlersRaw = execText("/usr/bin/plutil", ["-extract", "LSHandlers", "json", "-o", "-", "--", plistPath], 2e3);
|
|
944
|
-
if (
|
|
948
|
+
if (handlersRaw === null) return null;
|
|
945
949
|
let handlers;
|
|
946
950
|
try {
|
|
947
|
-
|
|
951
|
+
const parsed = JSON.parse(handlersRaw);
|
|
952
|
+
if (!Array.isArray(parsed)) return null;
|
|
953
|
+
handlers = parsed;
|
|
948
954
|
} catch {
|
|
949
955
|
return null;
|
|
950
956
|
}
|
|
951
|
-
if (!Array.isArray(handlers)) return null;
|
|
952
957
|
const resolveScheme = (scheme) => {
|
|
953
958
|
let candidate = null;
|
|
954
959
|
for (const entry of handlers) {
|
|
955
|
-
if (
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
960
|
+
if (entry === null || entry === void 0 || typeof entry !== "object") continue;
|
|
961
|
+
const rec = entry;
|
|
962
|
+
if (rec.LSHandlerURLScheme !== scheme) continue;
|
|
963
|
+
const role = (typeof rec.LSHandlerRoleAll === "string" ? rec.LSHandlerRoleAll : null) ?? (typeof rec.LSHandlerRoleViewer === "string" ? rec.LSHandlerRoleViewer : null) ?? null;
|
|
964
|
+
if (role !== null) candidate = role;
|
|
959
965
|
}
|
|
960
966
|
return candidate;
|
|
961
967
|
};
|
|
@@ -963,12 +969,12 @@ function detectDefaultBrowserBundleIdMac() {
|
|
|
963
969
|
}
|
|
964
970
|
function detectDefaultChromiumMac() {
|
|
965
971
|
const bundleId = detectDefaultBrowserBundleIdMac();
|
|
966
|
-
if (
|
|
972
|
+
if (bundleId === null || !CHROMIUM_BUNDLE_IDS.has(bundleId)) return null;
|
|
967
973
|
const appPathRaw = execText("/usr/bin/osascript", ["-e", `POSIX path of (path to application id "${bundleId}")`]);
|
|
968
|
-
if (
|
|
974
|
+
if (appPathRaw === null) return null;
|
|
969
975
|
const appPath = appPathRaw.trim().replace(/\/$/, "");
|
|
970
976
|
const exeName = execText("/usr/bin/defaults", ["read", path.join(appPath, "Contents", "Info"), "CFBundleExecutable"]);
|
|
971
|
-
if (
|
|
977
|
+
if (exeName === null) return null;
|
|
972
978
|
const exePath = path.join(appPath, "Contents", "MacOS", exeName.trim());
|
|
973
979
|
if (!fileExists(exePath)) return null;
|
|
974
980
|
return { kind: inferKindFromIdentifier(bundleId), path: exePath };
|
|
@@ -984,12 +990,15 @@ function findChromeMac() {
|
|
|
984
990
|
{ kind: "chromium", path: "/Applications/Chromium.app/Contents/MacOS/Chromium" },
|
|
985
991
|
{ kind: "chromium", path: path.join(os.homedir(), "Applications/Chromium.app/Contents/MacOS/Chromium") },
|
|
986
992
|
{ kind: "canary", path: "/Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary" },
|
|
987
|
-
{
|
|
993
|
+
{
|
|
994
|
+
kind: "canary",
|
|
995
|
+
path: path.join(os.homedir(), "Applications/Google Chrome Canary.app/Contents/MacOS/Google Chrome Canary")
|
|
996
|
+
}
|
|
988
997
|
]);
|
|
989
998
|
}
|
|
990
999
|
function detectDefaultChromiumLinux() {
|
|
991
|
-
const desktopId = execText("xdg-settings", ["get", "default-web-browser"])
|
|
992
|
-
if (
|
|
1000
|
+
const desktopId = execText("xdg-settings", ["get", "default-web-browser"]) ?? execText("xdg-mime", ["query", "default", "x-scheme-handler/http"]);
|
|
1001
|
+
if (desktopId === null) return null;
|
|
993
1002
|
const trimmed = desktopId.trim();
|
|
994
1003
|
if (!CHROMIUM_DESKTOP_IDS.has(trimmed)) return null;
|
|
995
1004
|
const searchDirs = [
|
|
@@ -1006,17 +1015,18 @@ function detectDefaultChromiumLinux() {
|
|
|
1006
1015
|
break;
|
|
1007
1016
|
}
|
|
1008
1017
|
}
|
|
1009
|
-
if (
|
|
1018
|
+
if (desktopPath === null) return null;
|
|
1010
1019
|
let execLine = null;
|
|
1011
1020
|
try {
|
|
1012
1021
|
const lines = fs.readFileSync(desktopPath, "utf8").split(/\r?\n/);
|
|
1013
|
-
for (const line of lines)
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1022
|
+
for (const line of lines)
|
|
1023
|
+
if (line.startsWith("Exec=")) {
|
|
1024
|
+
execLine = line.slice(5).trim();
|
|
1025
|
+
break;
|
|
1026
|
+
}
|
|
1017
1027
|
} catch {
|
|
1018
1028
|
}
|
|
1019
|
-
if (
|
|
1029
|
+
if (execLine === null) return null;
|
|
1020
1030
|
const tokens = execLine.split(/\s+/);
|
|
1021
1031
|
let command = null;
|
|
1022
1032
|
for (const token of tokens) {
|
|
@@ -1024,9 +1034,9 @@ function detectDefaultChromiumLinux() {
|
|
|
1024
1034
|
command = token.replace(/^["']|["']$/g, "");
|
|
1025
1035
|
break;
|
|
1026
1036
|
}
|
|
1027
|
-
if (
|
|
1037
|
+
if (command === null) return null;
|
|
1028
1038
|
const resolved = command.startsWith("/") ? command : execText("which", [command], 800)?.trim() ?? null;
|
|
1029
|
-
if (
|
|
1039
|
+
if (resolved === null || resolved === "") return null;
|
|
1030
1040
|
const exeName = path.posix.basename(resolved).toLowerCase();
|
|
1031
1041
|
if (!CHROMIUM_EXE_NAMES.has(exeName)) return null;
|
|
1032
1042
|
return { kind: inferKindFromExeName(exeName), path: resolved };
|
|
@@ -1055,21 +1065,30 @@ function findChromeWindows() {
|
|
|
1055
1065
|
const candidates = [];
|
|
1056
1066
|
if (localAppData) {
|
|
1057
1067
|
candidates.push({ kind: "chrome", path: j(localAppData, "Google", "Chrome", "Application", "chrome.exe") });
|
|
1058
|
-
candidates.push({
|
|
1068
|
+
candidates.push({
|
|
1069
|
+
kind: "brave",
|
|
1070
|
+
path: j(localAppData, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1071
|
+
});
|
|
1059
1072
|
candidates.push({ kind: "edge", path: j(localAppData, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1060
1073
|
candidates.push({ kind: "chromium", path: j(localAppData, "Chromium", "Application", "chrome.exe") });
|
|
1061
1074
|
candidates.push({ kind: "canary", path: j(localAppData, "Google", "Chrome SxS", "Application", "chrome.exe") });
|
|
1062
1075
|
}
|
|
1063
1076
|
candidates.push({ kind: "chrome", path: j(programFiles, "Google", "Chrome", "Application", "chrome.exe") });
|
|
1064
1077
|
candidates.push({ kind: "chrome", path: j(programFilesX86, "Google", "Chrome", "Application", "chrome.exe") });
|
|
1065
|
-
candidates.push({
|
|
1066
|
-
|
|
1078
|
+
candidates.push({
|
|
1079
|
+
kind: "brave",
|
|
1080
|
+
path: j(programFiles, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1081
|
+
});
|
|
1082
|
+
candidates.push({
|
|
1083
|
+
kind: "brave",
|
|
1084
|
+
path: j(programFilesX86, "BraveSoftware", "Brave-Browser", "Application", "brave.exe")
|
|
1085
|
+
});
|
|
1067
1086
|
candidates.push({ kind: "edge", path: j(programFiles, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1068
1087
|
candidates.push({ kind: "edge", path: j(programFilesX86, "Microsoft", "Edge", "Application", "msedge.exe") });
|
|
1069
1088
|
return findFirstExe(candidates);
|
|
1070
1089
|
}
|
|
1071
1090
|
function resolveBrowserExecutable(opts) {
|
|
1072
|
-
if (opts?.executablePath) {
|
|
1091
|
+
if (opts?.executablePath !== void 0 && opts.executablePath !== "") {
|
|
1073
1092
|
if (!fileExists(opts.executablePath)) throw new Error(`executablePath not found: ${opts.executablePath}`);
|
|
1074
1093
|
return { kind: "custom", path: opts.executablePath };
|
|
1075
1094
|
}
|
|
@@ -1082,10 +1101,12 @@ function resolveBrowserExecutable(opts) {
|
|
|
1082
1101
|
async function ensurePortAvailable(port) {
|
|
1083
1102
|
await new Promise((resolve2, reject) => {
|
|
1084
1103
|
const tester = net.createServer().once("error", (err) => {
|
|
1085
|
-
if (err.code === "EADDRINUSE") reject(new Error(`Port ${port} is already in use`));
|
|
1104
|
+
if (err.code === "EADDRINUSE") reject(new Error(`Port ${String(port)} is already in use`));
|
|
1086
1105
|
else reject(err);
|
|
1087
1106
|
}).once("listening", () => {
|
|
1088
|
-
tester.close(() =>
|
|
1107
|
+
tester.close(() => {
|
|
1108
|
+
resolve2();
|
|
1109
|
+
});
|
|
1089
1110
|
}).listen(port);
|
|
1090
1111
|
});
|
|
1091
1112
|
}
|
|
@@ -1167,7 +1188,7 @@ function isLoopbackHost(hostname) {
|
|
|
1167
1188
|
return hostname === "localhost" || hostname === "127.0.0.1" || hostname === "::1" || hostname === "[::1]";
|
|
1168
1189
|
}
|
|
1169
1190
|
function hasProxyEnvConfigured() {
|
|
1170
|
-
return
|
|
1191
|
+
return (process.env.HTTP_PROXY ?? process.env.HTTPS_PROXY ?? process.env.http_proxy ?? process.env.https_proxy ?? "") !== "";
|
|
1171
1192
|
}
|
|
1172
1193
|
function normalizeCdpWsUrl(wsUrl, cdpUrl) {
|
|
1173
1194
|
const ws = new URL(wsUrl);
|
|
@@ -1219,7 +1240,12 @@ async function canOpenWebSocket(url, timeoutMs) {
|
|
|
1219
1240
|
}
|
|
1220
1241
|
resolve2(value);
|
|
1221
1242
|
};
|
|
1222
|
-
const timer = setTimeout(
|
|
1243
|
+
const timer = setTimeout(
|
|
1244
|
+
() => {
|
|
1245
|
+
finish(false);
|
|
1246
|
+
},
|
|
1247
|
+
Math.max(50, timeoutMs + 25)
|
|
1248
|
+
);
|
|
1223
1249
|
let ws;
|
|
1224
1250
|
try {
|
|
1225
1251
|
ws = new WebSocket(url);
|
|
@@ -1227,21 +1253,27 @@ async function canOpenWebSocket(url, timeoutMs) {
|
|
|
1227
1253
|
finish(false);
|
|
1228
1254
|
return;
|
|
1229
1255
|
}
|
|
1230
|
-
ws.onopen = () =>
|
|
1231
|
-
|
|
1256
|
+
ws.onopen = () => {
|
|
1257
|
+
finish(true);
|
|
1258
|
+
};
|
|
1259
|
+
ws.onerror = () => {
|
|
1260
|
+
finish(false);
|
|
1261
|
+
};
|
|
1232
1262
|
});
|
|
1233
1263
|
}
|
|
1234
1264
|
async function fetchChromeVersion(cdpUrl, timeoutMs = 500, authToken) {
|
|
1235
1265
|
const ctrl = new AbortController();
|
|
1236
|
-
const t = setTimeout(() =>
|
|
1266
|
+
const t = setTimeout(() => {
|
|
1267
|
+
ctrl.abort();
|
|
1268
|
+
}, timeoutMs);
|
|
1237
1269
|
try {
|
|
1238
1270
|
const httpBase = isWebSocketUrl(cdpUrl) ? normalizeCdpHttpBaseForJsonEndpoints(cdpUrl) : cdpUrl;
|
|
1239
1271
|
const headers = {};
|
|
1240
|
-
if (authToken) headers
|
|
1272
|
+
if (authToken !== void 0 && authToken !== "") headers.Authorization = `Bearer ${authToken}`;
|
|
1241
1273
|
const res = await fetch(appendCdpPath(httpBase, "/json/version"), { signal: ctrl.signal, headers });
|
|
1242
1274
|
if (!res.ok) return null;
|
|
1243
1275
|
const data = await res.json();
|
|
1244
|
-
if (
|
|
1276
|
+
if (data === null || data === void 0 || typeof data !== "object") return null;
|
|
1245
1277
|
return data;
|
|
1246
1278
|
} catch {
|
|
1247
1279
|
return null;
|
|
@@ -1252,18 +1284,32 @@ async function fetchChromeVersion(cdpUrl, timeoutMs = 500, authToken) {
|
|
|
1252
1284
|
async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
|
|
1253
1285
|
if (isWebSocketUrl(cdpUrl)) return await canOpenWebSocket(cdpUrl, timeoutMs);
|
|
1254
1286
|
const version = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
1255
|
-
|
|
1287
|
+
if (version !== null) return true;
|
|
1288
|
+
let isLoopback = false;
|
|
1289
|
+
try {
|
|
1290
|
+
const u = new URL(cdpUrl.startsWith("http") ? cdpUrl : `http://${cdpUrl}`);
|
|
1291
|
+
isLoopback = isLoopbackHost(u.hostname);
|
|
1292
|
+
} catch {
|
|
1293
|
+
}
|
|
1294
|
+
if (!isLoopback) return false;
|
|
1295
|
+
for (let i = 0; i < 2; i++) {
|
|
1296
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1297
|
+
const retry = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
1298
|
+
if (retry !== null) return true;
|
|
1299
|
+
}
|
|
1300
|
+
return false;
|
|
1256
1301
|
}
|
|
1257
1302
|
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
|
|
1258
1303
|
if (isWebSocketUrl(cdpUrl)) return cdpUrl;
|
|
1259
1304
|
const version = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
1260
|
-
const
|
|
1261
|
-
|
|
1305
|
+
const rawWsUrl = version?.webSocketDebuggerUrl;
|
|
1306
|
+
const wsUrl = typeof rawWsUrl === "string" ? rawWsUrl.trim() : "";
|
|
1307
|
+
if (wsUrl === "") return null;
|
|
1262
1308
|
return normalizeCdpWsUrl(wsUrl, cdpUrl);
|
|
1263
1309
|
}
|
|
1264
1310
|
async function isChromeCdpReady(cdpUrl, timeoutMs = 500, handshakeTimeoutMs = 800) {
|
|
1265
1311
|
const wsUrl = await getChromeWebSocketUrl(cdpUrl, timeoutMs);
|
|
1266
|
-
if (
|
|
1312
|
+
if (wsUrl === null) return false;
|
|
1267
1313
|
return await canRunCdpHealthCommand(wsUrl, handshakeTimeoutMs);
|
|
1268
1314
|
}
|
|
1269
1315
|
async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
@@ -1279,7 +1325,12 @@ async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
|
1279
1325
|
}
|
|
1280
1326
|
resolve2(value);
|
|
1281
1327
|
};
|
|
1282
|
-
const timer = setTimeout(
|
|
1328
|
+
const timer = setTimeout(
|
|
1329
|
+
() => {
|
|
1330
|
+
finish(false);
|
|
1331
|
+
},
|
|
1332
|
+
Math.max(50, timeoutMs + 25)
|
|
1333
|
+
);
|
|
1283
1334
|
let ws;
|
|
1284
1335
|
try {
|
|
1285
1336
|
ws = new WebSocket(wsUrl);
|
|
@@ -1297,26 +1348,33 @@ async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
|
|
|
1297
1348
|
ws.onmessage = (event) => {
|
|
1298
1349
|
try {
|
|
1299
1350
|
const parsed = JSON.parse(String(event.data));
|
|
1300
|
-
if (parsed
|
|
1301
|
-
|
|
1351
|
+
if (typeof parsed !== "object" || parsed === null) return;
|
|
1352
|
+
const msg = parsed;
|
|
1353
|
+
if (msg.id !== 1) return;
|
|
1354
|
+
finish(typeof msg.result === "object" && msg.result !== null);
|
|
1302
1355
|
} catch {
|
|
1303
1356
|
}
|
|
1304
1357
|
};
|
|
1305
|
-
ws.onerror = () =>
|
|
1306
|
-
|
|
1358
|
+
ws.onerror = () => {
|
|
1359
|
+
finish(false);
|
|
1360
|
+
};
|
|
1361
|
+
ws.onclose = () => {
|
|
1362
|
+
finish(false);
|
|
1363
|
+
};
|
|
1307
1364
|
});
|
|
1308
1365
|
}
|
|
1309
1366
|
async function launchChrome(opts = {}) {
|
|
1310
1367
|
const cdpPort = opts.cdpPort ?? DEFAULT_CDP_PORT;
|
|
1311
1368
|
await ensurePortAvailable(cdpPort);
|
|
1312
1369
|
const exe = resolveBrowserExecutable({ executablePath: opts.executablePath });
|
|
1313
|
-
if (!exe)
|
|
1370
|
+
if (!exe)
|
|
1371
|
+
throw new Error("No supported browser found (Chrome/Brave/Edge/Chromium). Install one or provide executablePath.");
|
|
1314
1372
|
const profileName = opts.profileName ?? DEFAULT_PROFILE_NAME;
|
|
1315
1373
|
const userDataDir = opts.userDataDir ?? resolveUserDataDir(profileName);
|
|
1316
1374
|
fs.mkdirSync(userDataDir, { recursive: true });
|
|
1317
1375
|
const spawnChrome = () => {
|
|
1318
1376
|
const args = [
|
|
1319
|
-
`--remote-debugging-port=${cdpPort}`,
|
|
1377
|
+
`--remote-debugging-port=${String(cdpPort)}`,
|
|
1320
1378
|
`--user-data-dir=${userDataDir}`,
|
|
1321
1379
|
"--no-first-run",
|
|
1322
1380
|
"--no-default-browser-check",
|
|
@@ -1329,10 +1387,10 @@ async function launchChrome(opts = {}) {
|
|
|
1329
1387
|
"--hide-crash-restore-bubble",
|
|
1330
1388
|
"--password-store=basic"
|
|
1331
1389
|
];
|
|
1332
|
-
if (opts.headless) {
|
|
1390
|
+
if (opts.headless === true) {
|
|
1333
1391
|
args.push("--headless=new", "--disable-gpu");
|
|
1334
1392
|
}
|
|
1335
|
-
if (opts.noSandbox) {
|
|
1393
|
+
if (opts.noSandbox === true) {
|
|
1336
1394
|
args.push("--no-sandbox", "--disable-setuid-sandbox");
|
|
1337
1395
|
}
|
|
1338
1396
|
if (process.platform === "linux") args.push("--disable-dev-shm-usage");
|
|
@@ -1379,12 +1437,12 @@ async function launchChrome(opts = {}) {
|
|
|
1379
1437
|
} catch {
|
|
1380
1438
|
}
|
|
1381
1439
|
const proc = spawnChrome();
|
|
1382
|
-
const cdpUrl = `http://127.0.0.1:${cdpPort}`;
|
|
1440
|
+
const cdpUrl = `http://127.0.0.1:${String(cdpPort)}`;
|
|
1383
1441
|
const stderrChunks = [];
|
|
1384
1442
|
const onStderr = (chunk) => {
|
|
1385
1443
|
stderrChunks.push(chunk);
|
|
1386
1444
|
};
|
|
1387
|
-
proc.stderr
|
|
1445
|
+
proc.stderr.on("data", onStderr);
|
|
1388
1446
|
const readyDeadline = Date.now() + 15e3;
|
|
1389
1447
|
while (Date.now() < readyDeadline) {
|
|
1390
1448
|
if (await isChromeReachable(cdpUrl, 500)) break;
|
|
@@ -1395,14 +1453,14 @@ async function launchChrome(opts = {}) {
|
|
|
1395
1453
|
const stderrHint = stderrOutput ? `
|
|
1396
1454
|
Chrome stderr:
|
|
1397
1455
|
${stderrOutput.slice(0, 2e3)}` : "";
|
|
1398
|
-
const sandboxHint = process.platform === "linux" &&
|
|
1456
|
+
const sandboxHint = process.platform === "linux" && opts.noSandbox !== true ? "\nHint: If running in a container or as root, try setting noSandbox: true." : "";
|
|
1399
1457
|
try {
|
|
1400
1458
|
proc.kill("SIGKILL");
|
|
1401
1459
|
} catch {
|
|
1402
1460
|
}
|
|
1403
|
-
throw new Error(`Failed to start Chrome CDP on port ${cdpPort}.${sandboxHint}${stderrHint}`);
|
|
1461
|
+
throw new Error(`Failed to start Chrome CDP on port ${String(cdpPort)}.${sandboxHint}${stderrHint}`);
|
|
1404
1462
|
}
|
|
1405
|
-
proc.stderr
|
|
1463
|
+
proc.stderr.off("data", onStderr);
|
|
1406
1464
|
stderrChunks.length = 0;
|
|
1407
1465
|
return {
|
|
1408
1466
|
pid: proc.pid ?? -1,
|
|
@@ -1415,14 +1473,14 @@ ${stderrOutput.slice(0, 2e3)}` : "";
|
|
|
1415
1473
|
}
|
|
1416
1474
|
async function stopChrome(running, timeoutMs = 2500) {
|
|
1417
1475
|
const proc = running.proc;
|
|
1418
|
-
if (proc.exitCode
|
|
1476
|
+
if (proc.exitCode !== null) return;
|
|
1419
1477
|
try {
|
|
1420
1478
|
proc.kill("SIGTERM");
|
|
1421
1479
|
} catch {
|
|
1422
1480
|
}
|
|
1423
1481
|
const start = Date.now();
|
|
1424
1482
|
while (Date.now() - start < timeoutMs) {
|
|
1425
|
-
if (proc.exitCode
|
|
1483
|
+
if (proc.exitCode !== null) return;
|
|
1426
1484
|
await new Promise((r) => setTimeout(r, 100));
|
|
1427
1485
|
}
|
|
1428
1486
|
try {
|
|
@@ -1430,17 +1488,211 @@ async function stopChrome(running, timeoutMs = 2500) {
|
|
|
1430
1488
|
} catch {
|
|
1431
1489
|
}
|
|
1432
1490
|
}
|
|
1491
|
+
|
|
1492
|
+
// src/stealth.ts
|
|
1493
|
+
var STEALTH_SCRIPT = `(function() {
|
|
1494
|
+
'use strict';
|
|
1495
|
+
function p(fn) { try { fn(); } catch(_) {} }
|
|
1496
|
+
|
|
1497
|
+
// \u2500\u2500 1. navigator.webdriver \u2192 undefined \u2500\u2500
|
|
1498
|
+
p(function() {
|
|
1499
|
+
Object.defineProperty(navigator, 'webdriver', { get: function() { return undefined; }, configurable: true });
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
// \u2500\u2500 2. navigator.plugins + mimeTypes (only if empty \u2014 Chrome 92+ populates them natively) \u2500\u2500
|
|
1503
|
+
p(function() {
|
|
1504
|
+
if (navigator.plugins && navigator.plugins.length > 0) return;
|
|
1505
|
+
|
|
1506
|
+
function FakePlugin(name, fn, desc, mimes) {
|
|
1507
|
+
this.name = name; this.filename = fn; this.description = desc; this.length = mimes.length;
|
|
1508
|
+
for (var i = 0; i < mimes.length; i++) { this[i] = mimes[i]; mimes[i].enabledPlugin = this; }
|
|
1509
|
+
}
|
|
1510
|
+
FakePlugin.prototype.item = function(i) { return this[i] || null; };
|
|
1511
|
+
FakePlugin.prototype.namedItem = function(n) {
|
|
1512
|
+
for (var i = 0; i < this.length; i++) if (this[i].type === n) return this[i];
|
|
1513
|
+
return null;
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
function M(type, suf, desc) { this.type = type; this.suffixes = suf; this.description = desc; }
|
|
1517
|
+
|
|
1518
|
+
var m1 = new M('application/pdf', 'pdf', 'Portable Document Format');
|
|
1519
|
+
var m2 = new M('application/x-google-chrome-pdf', 'pdf', 'Portable Document Format');
|
|
1520
|
+
var m3 = new M('application/x-nacl', '', 'Native Client Executable');
|
|
1521
|
+
var m4 = new M('application/x-pnacl', '', 'Portable Native Client Executable');
|
|
1522
|
+
|
|
1523
|
+
var plugins = [
|
|
1524
|
+
new FakePlugin('Chrome PDF Plugin', 'internal-pdf-viewer', 'Portable Document Format', [m1]),
|
|
1525
|
+
new FakePlugin('Chrome PDF Viewer', 'mhjfbmdgcfjbbpaeojofohoefgiehjai', '', [m2]),
|
|
1526
|
+
new FakePlugin('Native Client', 'internal-nacl-plugin', '', [m3, m4]),
|
|
1527
|
+
];
|
|
1528
|
+
|
|
1529
|
+
function makeIterable(arr, items) {
|
|
1530
|
+
arr.length = items.length;
|
|
1531
|
+
for (var i = 0; i < items.length; i++) arr[i] = items[i];
|
|
1532
|
+
arr[Symbol.iterator] = function() {
|
|
1533
|
+
var idx = 0;
|
|
1534
|
+
return { next: function() {
|
|
1535
|
+
return idx < items.length ? { value: items[idx++], done: false } : { done: true };
|
|
1536
|
+
}};
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
var pa = { item: function(i) { return plugins[i] || null; },
|
|
1541
|
+
namedItem: function(n) { for (var i = 0; i < plugins.length; i++) if (plugins[i].name === n) return plugins[i]; return null; },
|
|
1542
|
+
refresh: function() {} };
|
|
1543
|
+
makeIterable(pa, plugins);
|
|
1544
|
+
Object.defineProperty(navigator, 'plugins', { get: function() { return pa; } });
|
|
1545
|
+
|
|
1546
|
+
var allMimes = [m1, m2, m3, m4];
|
|
1547
|
+
var ma = { item: function(i) { return allMimes[i] || null; },
|
|
1548
|
+
namedItem: function(n) { for (var i = 0; i < allMimes.length; i++) if (allMimes[i].type === n) return allMimes[i]; return null; } };
|
|
1549
|
+
makeIterable(ma, allMimes);
|
|
1550
|
+
Object.defineProperty(navigator, 'mimeTypes', { get: function() { return ma; } });
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
// \u2500\u2500 3. navigator.languages (cached + frozen so identity check passes) \u2500\u2500
|
|
1554
|
+
p(function() {
|
|
1555
|
+
if (!navigator.languages || navigator.languages.length === 0) {
|
|
1556
|
+
var langs = Object.freeze(['en-US', 'en']);
|
|
1557
|
+
Object.defineProperty(navigator, 'languages', { get: function() { return langs; } });
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
// \u2500\u2500 4. window.chrome \u2500\u2500
|
|
1562
|
+
p(function() {
|
|
1563
|
+
if (window.chrome && window.chrome.runtime && window.chrome.runtime.connect) return;
|
|
1564
|
+
|
|
1565
|
+
var chrome = window.chrome || {};
|
|
1566
|
+
var noop = function() {};
|
|
1567
|
+
var evtStub = { addListener: noop, removeListener: noop, hasListeners: function() { return false; } };
|
|
1568
|
+
chrome.runtime = chrome.runtime || {};
|
|
1569
|
+
chrome.runtime.onMessage = chrome.runtime.onMessage || evtStub;
|
|
1570
|
+
chrome.runtime.onConnect = chrome.runtime.onConnect || evtStub;
|
|
1571
|
+
chrome.runtime.sendMessage = chrome.runtime.sendMessage || noop;
|
|
1572
|
+
chrome.runtime.connect = chrome.runtime.connect || function() {
|
|
1573
|
+
return { onMessage: { addListener: noop }, postMessage: noop, disconnect: noop };
|
|
1574
|
+
};
|
|
1575
|
+
if (chrome.runtime.id === undefined) chrome.runtime.id = undefined;
|
|
1576
|
+
if (!chrome.loadTimes) chrome.loadTimes = function() { return {}; };
|
|
1577
|
+
if (!chrome.csi) chrome.csi = function() { return {}; };
|
|
1578
|
+
if (!chrome.app) {
|
|
1579
|
+
chrome.app = {
|
|
1580
|
+
isInstalled: false,
|
|
1581
|
+
InstallState: { INSTALLED: 'installed', NOT_INSTALLED: 'not_installed', DISABLED: 'disabled' },
|
|
1582
|
+
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
|
|
1583
|
+
getDetails: function() { return null; },
|
|
1584
|
+
getIsInstalled: function() { return false; },
|
|
1585
|
+
runningState: function() { return 'cannot_run'; },
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (!window.chrome) {
|
|
1590
|
+
Object.defineProperty(window, 'chrome', { value: chrome, writable: false, enumerable: true, configurable: false });
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
// \u2500\u2500 5. Permissions API consistency \u2500\u2500
|
|
1595
|
+
p(function() {
|
|
1596
|
+
var orig = navigator.permissions.query.bind(navigator.permissions);
|
|
1597
|
+
function q(params) {
|
|
1598
|
+
if (params.name === 'notifications') {
|
|
1599
|
+
return Promise.resolve({
|
|
1600
|
+
state: typeof Notification !== 'undefined' ? Notification.permission : 'prompt',
|
|
1601
|
+
name: 'notifications', onchange: null,
|
|
1602
|
+
addEventListener: function(){}, removeEventListener: function(){}, dispatchEvent: function(){ return true; },
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
return orig(params);
|
|
1606
|
+
}
|
|
1607
|
+
q.toString = function() { return 'function query() { [native code] }'; };
|
|
1608
|
+
navigator.permissions.query = q;
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
// \u2500\u2500 6. WebGL vendor / renderer \u2500\u2500
|
|
1612
|
+
p(function() {
|
|
1613
|
+
var h = {
|
|
1614
|
+
apply: function(target, self, args) {
|
|
1615
|
+
var param = args[0];
|
|
1616
|
+
if (param === 0x9245) return 'Intel Inc.';
|
|
1617
|
+
if (param === 0x9246) return 'Intel Iris OpenGL Engine';
|
|
1618
|
+
return Reflect.apply(target, self, args);
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
if (typeof WebGLRenderingContext !== 'undefined')
|
|
1622
|
+
WebGLRenderingContext.prototype.getParameter = new Proxy(WebGLRenderingContext.prototype.getParameter, h);
|
|
1623
|
+
if (typeof WebGL2RenderingContext !== 'undefined')
|
|
1624
|
+
WebGL2RenderingContext.prototype.getParameter = new Proxy(WebGL2RenderingContext.prototype.getParameter, h);
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
// \u2500\u2500 7. Notification.permission \u2500\u2500
|
|
1628
|
+
p(function() {
|
|
1629
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {
|
|
1630
|
+
Object.defineProperty(Notification, 'permission', { get: function() { return 'default'; }, configurable: true });
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
// \u2500\u2500 8. navigator.connection (cached so identity check passes) \u2500\u2500
|
|
1635
|
+
p(function() {
|
|
1636
|
+
if (navigator.connection) return;
|
|
1637
|
+
var conn = {
|
|
1638
|
+
effectiveType: '4g', rtt: 50, downlink: 10, saveData: false, onchange: null,
|
|
1639
|
+
addEventListener: function(){}, removeEventListener: function(){}, dispatchEvent: function(){ return true; },
|
|
1640
|
+
};
|
|
1641
|
+
Object.defineProperty(navigator, 'connection', { get: function() { return conn; } });
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
// \u2500\u2500 9. Iframe contentWindow.chrome \u2500\u2500
|
|
1645
|
+
// Handled by patch 4 \u2014 chrome object is now on window, propagates to iframes on same origin.
|
|
1646
|
+
|
|
1647
|
+
// \u2500\u2500 10. console method toString \u2500\u2500
|
|
1648
|
+
p(function() {
|
|
1649
|
+
['log','info','warn','error','debug','table','trace'].forEach(function(n) {
|
|
1650
|
+
if (console[n]) {
|
|
1651
|
+
console[n].toString = function() { return 'function ' + n + '() { [native code] }'; };
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
// \u2500\u2500 11. Headless-mode window / screen fixes \u2500\u2500
|
|
1657
|
+
p(function() {
|
|
1658
|
+
if (window.outerWidth === 0)
|
|
1659
|
+
Object.defineProperty(window, 'outerWidth', { get: function() { return window.innerWidth || 1920; } });
|
|
1660
|
+
if (window.outerHeight === 0)
|
|
1661
|
+
Object.defineProperty(window, 'outerHeight', { get: function() { return (window.innerHeight || 1080) + 85; } });
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
p(function() {
|
|
1665
|
+
if (screen.colorDepth === 0) {
|
|
1666
|
+
Object.defineProperty(screen, 'colorDepth', { get: function() { return 24; } });
|
|
1667
|
+
Object.defineProperty(screen, 'pixelDepth', { get: function() { return 24; } });
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
// \u2500\u2500 12. navigator.hardwareConcurrency \u2500\u2500
|
|
1672
|
+
p(function() {
|
|
1673
|
+
if (!navigator.hardwareConcurrency)
|
|
1674
|
+
Object.defineProperty(navigator, 'hardwareConcurrency', { get: function() { return 4; } });
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
// \u2500\u2500 13. navigator.deviceMemory \u2500\u2500
|
|
1678
|
+
p(function() {
|
|
1679
|
+
if (!navigator.deviceMemory)
|
|
1680
|
+
Object.defineProperty(navigator, 'deviceMemory', { get: function() { return 8; } });
|
|
1681
|
+
});
|
|
1682
|
+
})()`;
|
|
1683
|
+
|
|
1684
|
+
// src/connection.ts
|
|
1433
1685
|
var BrowserTabNotFoundError = class extends Error {
|
|
1434
1686
|
constructor(message = "Tab not found") {
|
|
1435
1687
|
super(message);
|
|
1436
1688
|
this.name = "BrowserTabNotFoundError";
|
|
1437
1689
|
}
|
|
1438
1690
|
};
|
|
1439
|
-
var OPENCLAW_EXTENSION_RELAY_BROWSER = "OpenClaw/extension-relay";
|
|
1440
|
-
var extensionRelayByCdpUrl = /* @__PURE__ */ new Map();
|
|
1441
1691
|
async function fetchJsonForCdp(url, timeoutMs) {
|
|
1442
1692
|
const ctrl = new AbortController();
|
|
1443
|
-
const t = setTimeout(() =>
|
|
1693
|
+
const t = setTimeout(() => {
|
|
1694
|
+
ctrl.abort();
|
|
1695
|
+
}, timeoutMs);
|
|
1444
1696
|
try {
|
|
1445
1697
|
const res = await fetch(url, { signal: ctrl.signal });
|
|
1446
1698
|
if (!res.ok) return null;
|
|
@@ -1460,20 +1712,6 @@ function appendCdpPath2(cdpUrl, cdpPath) {
|
|
|
1460
1712
|
return `${cdpUrl.replace(/\/$/, "")}${cdpPath}`;
|
|
1461
1713
|
}
|
|
1462
1714
|
}
|
|
1463
|
-
async function isExtensionRelayCdpEndpoint(cdpUrl) {
|
|
1464
|
-
const normalized = normalizeCdpUrl(cdpUrl);
|
|
1465
|
-
const cached = extensionRelayByCdpUrl.get(normalized);
|
|
1466
|
-
if (cached !== void 0) return cached;
|
|
1467
|
-
try {
|
|
1468
|
-
const version = await fetchJsonForCdp(appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(normalized), "/json/version"), 2e3);
|
|
1469
|
-
const isRelay = String(version?.Browser ?? "").trim() === OPENCLAW_EXTENSION_RELAY_BROWSER;
|
|
1470
|
-
extensionRelayByCdpUrl.set(normalized, isRelay);
|
|
1471
|
-
return isRelay;
|
|
1472
|
-
} catch {
|
|
1473
|
-
extensionRelayByCdpUrl.set(normalized, false);
|
|
1474
|
-
return false;
|
|
1475
|
-
}
|
|
1476
|
-
}
|
|
1477
1715
|
async function withPlaywrightPageCdpSession(page, fn) {
|
|
1478
1716
|
const session = await page.context().newCDPSession(page);
|
|
1479
1717
|
try {
|
|
@@ -1490,7 +1728,7 @@ async function withPageScopedCdpClient(opts) {
|
|
|
1490
1728
|
}
|
|
1491
1729
|
var LOOPBACK_ENTRIES = "localhost,127.0.0.1,[::1]";
|
|
1492
1730
|
function noProxyAlreadyCoversLocalhost() {
|
|
1493
|
-
const current = process.env.NO_PROXY
|
|
1731
|
+
const current = process.env.NO_PROXY ?? process.env.no_proxy ?? "";
|
|
1494
1732
|
return current.includes("localhost") && current.includes("127.0.0.1") && current.includes("[::1]");
|
|
1495
1733
|
}
|
|
1496
1734
|
function isLoopbackCdpUrl(url) {
|
|
@@ -1508,7 +1746,7 @@ var NoProxyLeaseManager = class {
|
|
|
1508
1746
|
if (this.leaseCount === 0 && !noProxyAlreadyCoversLocalhost()) {
|
|
1509
1747
|
const noProxy = process.env.NO_PROXY;
|
|
1510
1748
|
const noProxyLower = process.env.no_proxy;
|
|
1511
|
-
const current = noProxy
|
|
1749
|
+
const current = noProxy ?? noProxyLower ?? "";
|
|
1512
1750
|
const applied = current ? `${current},${LOOPBACK_ENTRIES}` : LOOPBACK_ENTRIES;
|
|
1513
1751
|
process.env.NO_PROXY = applied;
|
|
1514
1752
|
process.env.no_proxy = applied;
|
|
@@ -1553,9 +1791,12 @@ function getHeadersWithAuth(endpoint, baseHeaders = {}) {
|
|
|
1553
1791
|
const headers = { ...baseHeaders };
|
|
1554
1792
|
try {
|
|
1555
1793
|
const parsed = new URL(endpoint);
|
|
1556
|
-
if (
|
|
1557
|
-
|
|
1558
|
-
|
|
1794
|
+
if (Object.keys(headers).some((k) => k.toLowerCase() === "authorization")) return headers;
|
|
1795
|
+
if (parsed.username || parsed.password) {
|
|
1796
|
+
const credentials = Buffer.from(
|
|
1797
|
+
`${decodeURIComponent(parsed.username)}:${decodeURIComponent(parsed.password)}`
|
|
1798
|
+
).toString("base64");
|
|
1799
|
+
headers.Authorization = `Basic ${credentials}`;
|
|
1559
1800
|
}
|
|
1560
1801
|
} catch {
|
|
1561
1802
|
}
|
|
@@ -1603,7 +1844,7 @@ function roleRefsKey(cdpUrl, targetId) {
|
|
|
1603
1844
|
function findNetworkRequestById(state, id) {
|
|
1604
1845
|
for (let i = state.requests.length - 1; i >= 0; i--) {
|
|
1605
1846
|
const candidate = state.requests[i];
|
|
1606
|
-
if (candidate
|
|
1847
|
+
if (candidate.id === id) return candidate;
|
|
1607
1848
|
}
|
|
1608
1849
|
return void 0;
|
|
1609
1850
|
}
|
|
@@ -1634,16 +1875,16 @@ function ensurePageState(page) {
|
|
|
1634
1875
|
});
|
|
1635
1876
|
page.on("pageerror", (err) => {
|
|
1636
1877
|
state.errors.push({
|
|
1637
|
-
message: err
|
|
1638
|
-
name: err
|
|
1639
|
-
stack: err
|
|
1878
|
+
message: err.message !== "" ? err.message : String(err),
|
|
1879
|
+
name: err.name !== "" ? err.name : void 0,
|
|
1880
|
+
stack: err.stack !== void 0 && err.stack !== "" ? err.stack : void 0,
|
|
1640
1881
|
timestamp: (/* @__PURE__ */ new Date()).toISOString()
|
|
1641
1882
|
});
|
|
1642
1883
|
if (state.errors.length > MAX_PAGE_ERRORS) state.errors.shift();
|
|
1643
1884
|
});
|
|
1644
1885
|
page.on("request", (req) => {
|
|
1645
1886
|
state.nextRequestId += 1;
|
|
1646
|
-
const id = `r${state.nextRequestId}`;
|
|
1887
|
+
const id = `r${String(state.nextRequestId)}`;
|
|
1647
1888
|
state.requestIds.set(req, id);
|
|
1648
1889
|
state.requests.push({
|
|
1649
1890
|
id,
|
|
@@ -1657,7 +1898,7 @@ function ensurePageState(page) {
|
|
|
1657
1898
|
page.on("response", (resp) => {
|
|
1658
1899
|
const req = resp.request();
|
|
1659
1900
|
const id = state.requestIds.get(req);
|
|
1660
|
-
if (
|
|
1901
|
+
if (id === void 0) return;
|
|
1661
1902
|
const rec = findNetworkRequestById(state, id);
|
|
1662
1903
|
if (rec) {
|
|
1663
1904
|
rec.status = resp.status();
|
|
@@ -1666,7 +1907,7 @@ function ensurePageState(page) {
|
|
|
1666
1907
|
});
|
|
1667
1908
|
page.on("requestfailed", (req) => {
|
|
1668
1909
|
const id = state.requestIds.get(req);
|
|
1669
|
-
if (
|
|
1910
|
+
if (id === void 0) return;
|
|
1670
1911
|
const rec = findNetworkRequestById(state, id);
|
|
1671
1912
|
if (rec) {
|
|
1672
1913
|
rec.failureText = req.failure()?.errorText;
|
|
@@ -1680,10 +1921,10 @@ function ensurePageState(page) {
|
|
|
1680
1921
|
}
|
|
1681
1922
|
return state;
|
|
1682
1923
|
}
|
|
1683
|
-
var STEALTH_SCRIPT = `Object.defineProperty(navigator, 'webdriver', { get: () => undefined })`;
|
|
1684
1924
|
function applyStealthToPage(page) {
|
|
1685
1925
|
page.evaluate(STEALTH_SCRIPT).catch((e) => {
|
|
1686
|
-
if (process.env.DEBUG
|
|
1926
|
+
if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
|
|
1927
|
+
console.warn("[browserclaw] stealth evaluate failed:", e instanceof Error ? e.message : String(e));
|
|
1687
1928
|
});
|
|
1688
1929
|
}
|
|
1689
1930
|
function observeContext(context) {
|
|
@@ -1691,7 +1932,8 @@ function observeContext(context) {
|
|
|
1691
1932
|
observedContexts.add(context);
|
|
1692
1933
|
ensureContextState(context);
|
|
1693
1934
|
context.addInitScript(STEALTH_SCRIPT).catch((e) => {
|
|
1694
|
-
if (process.env.DEBUG
|
|
1935
|
+
if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
|
|
1936
|
+
console.warn("[browserclaw] stealth initScript failed:", e instanceof Error ? e.message : String(e));
|
|
1695
1937
|
});
|
|
1696
1938
|
for (const page of context.pages()) {
|
|
1697
1939
|
ensurePageState(page);
|
|
@@ -1707,15 +1949,15 @@ function observeBrowser(browser) {
|
|
|
1707
1949
|
}
|
|
1708
1950
|
function rememberRoleRefsForTarget(opts) {
|
|
1709
1951
|
const targetId = opts.targetId.trim();
|
|
1710
|
-
if (
|
|
1952
|
+
if (targetId === "") return;
|
|
1711
1953
|
roleRefsByTarget.set(roleRefsKey(opts.cdpUrl, targetId), {
|
|
1712
1954
|
refs: opts.refs,
|
|
1713
|
-
...opts.frameSelector ? { frameSelector: opts.frameSelector } : {},
|
|
1714
|
-
...opts.mode ? { mode: opts.mode } : {}
|
|
1955
|
+
...opts.frameSelector !== void 0 && opts.frameSelector !== "" ? { frameSelector: opts.frameSelector } : {},
|
|
1956
|
+
...opts.mode !== void 0 ? { mode: opts.mode } : {}
|
|
1715
1957
|
});
|
|
1716
1958
|
while (roleRefsByTarget.size > MAX_ROLE_REFS_CACHE) {
|
|
1717
1959
|
const first = roleRefsByTarget.keys().next();
|
|
1718
|
-
if (first.done) break;
|
|
1960
|
+
if (first.done === true) break;
|
|
1719
1961
|
roleRefsByTarget.delete(first.value);
|
|
1720
1962
|
}
|
|
1721
1963
|
}
|
|
@@ -1724,7 +1966,7 @@ function storeRoleRefsForTarget(opts) {
|
|
|
1724
1966
|
state.roleRefs = opts.refs;
|
|
1725
1967
|
state.roleRefsFrameSelector = opts.frameSelector;
|
|
1726
1968
|
state.roleRefsMode = opts.mode;
|
|
1727
|
-
if (
|
|
1969
|
+
if (opts.targetId === void 0 || opts.targetId.trim() === "") return;
|
|
1728
1970
|
rememberRoleRefsForTarget({
|
|
1729
1971
|
cdpUrl: opts.cdpUrl,
|
|
1730
1972
|
targetId: opts.targetId,
|
|
@@ -1734,8 +1976,8 @@ function storeRoleRefsForTarget(opts) {
|
|
|
1734
1976
|
});
|
|
1735
1977
|
}
|
|
1736
1978
|
function restoreRoleRefsForTarget(opts) {
|
|
1737
|
-
const targetId = opts.targetId?.trim()
|
|
1738
|
-
if (
|
|
1979
|
+
const targetId = opts.targetId?.trim() ?? "";
|
|
1980
|
+
if (targetId === "") return;
|
|
1739
1981
|
const entry = roleRefsByTarget.get(roleRefsKey(opts.cdpUrl, targetId));
|
|
1740
1982
|
if (!entry) return;
|
|
1741
1983
|
const state = ensurePageState(opts.page);
|
|
@@ -1757,8 +1999,12 @@ async function connectBrowser(cdpUrl, authToken) {
|
|
|
1757
1999
|
const timeout = 5e3 + attempt * 2e3;
|
|
1758
2000
|
const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
|
|
1759
2001
|
const headers = getHeadersWithAuth(endpoint);
|
|
1760
|
-
if (authToken &&
|
|
1761
|
-
|
|
2002
|
+
if (authToken !== void 0 && authToken !== "" && !headers.Authorization)
|
|
2003
|
+
headers.Authorization = `Bearer ${authToken}`;
|
|
2004
|
+
const browser = await withNoProxyForCdpUrl(
|
|
2005
|
+
endpoint,
|
|
2006
|
+
() => chromium.connectOverCDP(endpoint, { timeout, headers })
|
|
2007
|
+
);
|
|
1762
2008
|
const onDisconnected = () => {
|
|
1763
2009
|
if (cachedByCdpUrl.get(normalized)?.browser === browser) {
|
|
1764
2010
|
cachedByCdpUrl.delete(normalized);
|
|
@@ -1812,7 +2058,9 @@ function cdpSocketNeedsAttach(wsUrl) {
|
|
|
1812
2058
|
async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
1813
2059
|
const httpBase = normalizeCdpHttpBaseForJsonEndpoints(cdpUrl);
|
|
1814
2060
|
const ctrl = new AbortController();
|
|
1815
|
-
const t = setTimeout(() =>
|
|
2061
|
+
const t = setTimeout(() => {
|
|
2062
|
+
ctrl.abort();
|
|
2063
|
+
}, 2e3);
|
|
1816
2064
|
let targets;
|
|
1817
2065
|
try {
|
|
1818
2066
|
const res = await fetch(`${httpBase}/json/list`, { signal: ctrl.signal });
|
|
@@ -1824,9 +2072,12 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1824
2072
|
clearTimeout(t);
|
|
1825
2073
|
}
|
|
1826
2074
|
if (!Array.isArray(targets)) return;
|
|
1827
|
-
const target = targets.find((entry) =>
|
|
1828
|
-
|
|
1829
|
-
|
|
2075
|
+
const target = targets.find((entry) => {
|
|
2076
|
+
const e = entry;
|
|
2077
|
+
return (e.id ?? "").trim() === targetId;
|
|
2078
|
+
});
|
|
2079
|
+
const wsUrlRaw = (target?.webSocketDebuggerUrl ?? "").trim();
|
|
2080
|
+
if (wsUrlRaw === "") return;
|
|
1830
2081
|
const wsUrl = normalizeCdpWsUrl(wsUrlRaw, httpBase);
|
|
1831
2082
|
const needsAttach = cdpSocketNeedsAttach(wsUrl);
|
|
1832
2083
|
await new Promise((resolve2) => {
|
|
@@ -1862,10 +2113,18 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1862
2113
|
if (!needsAttach) return;
|
|
1863
2114
|
try {
|
|
1864
2115
|
const msg = JSON.parse(String(event.data));
|
|
1865
|
-
|
|
1866
|
-
|
|
2116
|
+
const result = msg.result;
|
|
2117
|
+
if (msg.id !== void 0 && result?.sessionId !== void 0) {
|
|
2118
|
+
const sessionId = result.sessionId;
|
|
2119
|
+
ws.send(JSON.stringify({ id: nextId++, sessionId, method: "Runtime.terminateExecution" }));
|
|
1867
2120
|
try {
|
|
1868
|
-
ws.send(
|
|
2121
|
+
ws.send(
|
|
2122
|
+
JSON.stringify({
|
|
2123
|
+
id: nextId++,
|
|
2124
|
+
method: "Target.detachFromTarget",
|
|
2125
|
+
params: { sessionId }
|
|
2126
|
+
})
|
|
2127
|
+
);
|
|
1869
2128
|
} catch {
|
|
1870
2129
|
}
|
|
1871
2130
|
setTimeout(finish, 300);
|
|
@@ -1873,8 +2132,12 @@ async function tryTerminateExecutionViaCdp(cdpUrl, targetId) {
|
|
|
1873
2132
|
} catch {
|
|
1874
2133
|
}
|
|
1875
2134
|
};
|
|
1876
|
-
ws.onerror = () =>
|
|
1877
|
-
|
|
2135
|
+
ws.onerror = () => {
|
|
2136
|
+
finish();
|
|
2137
|
+
};
|
|
2138
|
+
ws.onclose = () => {
|
|
2139
|
+
finish();
|
|
2140
|
+
};
|
|
1878
2141
|
});
|
|
1879
2142
|
}
|
|
1880
2143
|
async function forceDisconnectPlaywrightForTarget(opts) {
|
|
@@ -1886,15 +2149,15 @@ async function forceDisconnectPlaywrightForTarget(opts) {
|
|
|
1886
2149
|
if (cur.onDisconnected && typeof cur.browser.off === "function") {
|
|
1887
2150
|
cur.browser.off("disconnected", cur.onDisconnected);
|
|
1888
2151
|
}
|
|
1889
|
-
const targetId = opts.targetId?.trim()
|
|
1890
|
-
if (targetId) {
|
|
2152
|
+
const targetId = opts.targetId?.trim() ?? "";
|
|
2153
|
+
if (targetId !== "") {
|
|
1891
2154
|
await tryTerminateExecutionViaCdp(normalized, targetId).catch(() => {
|
|
1892
2155
|
});
|
|
1893
2156
|
}
|
|
1894
2157
|
cur.browser.close().catch(() => {
|
|
1895
2158
|
});
|
|
1896
2159
|
}
|
|
1897
|
-
|
|
2160
|
+
function getAllPages(browser) {
|
|
1898
2161
|
return browser.contexts().flatMap((c) => c.pages());
|
|
1899
2162
|
}
|
|
1900
2163
|
async function pageTargetId(page) {
|
|
@@ -1902,7 +2165,7 @@ async function pageTargetId(page) {
|
|
|
1902
2165
|
try {
|
|
1903
2166
|
const info = await session.send("Target.getTargetInfo");
|
|
1904
2167
|
const targetInfo = info.targetInfo;
|
|
1905
|
-
return
|
|
2168
|
+
return (targetInfo?.targetId ?? "").trim() || null;
|
|
1906
2169
|
} finally {
|
|
1907
2170
|
await session.detach().catch(() => {
|
|
1908
2171
|
});
|
|
@@ -1923,21 +2186,15 @@ function matchPageByTargetList(pages, targets, targetId) {
|
|
|
1923
2186
|
return null;
|
|
1924
2187
|
}
|
|
1925
2188
|
async function findPageByTargetIdViaTargetList(pages, targetId, cdpUrl) {
|
|
1926
|
-
const targets = await fetchJsonForCdp(
|
|
2189
|
+
const targets = await fetchJsonForCdp(
|
|
2190
|
+
appendCdpPath2(normalizeCdpHttpBaseForJsonEndpoints(cdpUrl), "/json/list"),
|
|
2191
|
+
2e3
|
|
2192
|
+
);
|
|
1927
2193
|
if (!Array.isArray(targets)) return null;
|
|
1928
2194
|
return matchPageByTargetList(pages, targets, targetId);
|
|
1929
2195
|
}
|
|
1930
2196
|
async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
1931
|
-
const pages =
|
|
1932
|
-
const isExtensionRelay = cdpUrl ? await isExtensionRelayCdpEndpoint(cdpUrl).catch(() => false) : false;
|
|
1933
|
-
if (cdpUrl && isExtensionRelay) {
|
|
1934
|
-
try {
|
|
1935
|
-
const matched = await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
|
|
1936
|
-
if (matched) return matched;
|
|
1937
|
-
} catch {
|
|
1938
|
-
}
|
|
1939
|
-
return pages.length === 1 ? pages[0] ?? null : null;
|
|
1940
|
-
}
|
|
2197
|
+
const pages = getAllPages(browser);
|
|
1941
2198
|
let resolvedViaCdp = false;
|
|
1942
2199
|
for (const page of pages) {
|
|
1943
2200
|
let tid = null;
|
|
@@ -1947,9 +2204,9 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
1947
2204
|
} catch {
|
|
1948
2205
|
tid = null;
|
|
1949
2206
|
}
|
|
1950
|
-
if (tid && tid === targetId) return page;
|
|
2207
|
+
if (tid !== null && tid !== "" && tid === targetId) return page;
|
|
1951
2208
|
}
|
|
1952
|
-
if (cdpUrl) {
|
|
2209
|
+
if (cdpUrl !== void 0 && cdpUrl !== "") {
|
|
1953
2210
|
try {
|
|
1954
2211
|
return await findPageByTargetIdViaTargetList(pages, targetId, cdpUrl);
|
|
1955
2212
|
} catch {
|
|
@@ -1960,14 +2217,16 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
|
1960
2217
|
}
|
|
1961
2218
|
async function getPageForTargetId(opts) {
|
|
1962
2219
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1963
|
-
const pages =
|
|
2220
|
+
const pages = getAllPages(browser);
|
|
1964
2221
|
if (!pages.length) throw new Error("No pages available in the connected browser.");
|
|
1965
2222
|
const first = pages[0];
|
|
1966
|
-
if (
|
|
2223
|
+
if (opts.targetId === void 0 || opts.targetId === "") return first;
|
|
1967
2224
|
const found = await findPageByTargetId(browser, opts.targetId, opts.cdpUrl);
|
|
1968
2225
|
if (!found) {
|
|
1969
2226
|
if (pages.length === 1) return first;
|
|
1970
|
-
throw new BrowserTabNotFoundError(
|
|
2227
|
+
throw new BrowserTabNotFoundError(
|
|
2228
|
+
`Tab not found (targetId: ${opts.targetId}). Call browser.tabs() to list open tabs.`
|
|
2229
|
+
);
|
|
1971
2230
|
}
|
|
1972
2231
|
return found;
|
|
1973
2232
|
}
|
|
@@ -2001,7 +2260,7 @@ function resolveInteractionTimeoutMs(timeoutMs) {
|
|
|
2001
2260
|
function resolveBoundedDelayMs(value, label, maxMs) {
|
|
2002
2261
|
const normalized = Math.floor(value ?? 0);
|
|
2003
2262
|
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
2004
|
-
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
2263
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
|
|
2005
2264
|
return normalized;
|
|
2006
2265
|
}
|
|
2007
2266
|
async function getRestoredPageForTarget(opts) {
|
|
@@ -2012,17 +2271,17 @@ async function getRestoredPageForTarget(opts) {
|
|
|
2012
2271
|
}
|
|
2013
2272
|
function refLocator(page, ref) {
|
|
2014
2273
|
const normalized = ref.startsWith("@") ? ref.slice(1) : ref.startsWith("ref=") ? ref.slice(4) : ref;
|
|
2015
|
-
if (
|
|
2274
|
+
if (normalized.trim() === "") throw new Error("ref is required");
|
|
2016
2275
|
if (/^e\d+$/.test(normalized)) {
|
|
2017
2276
|
const state = pageStates.get(page);
|
|
2018
2277
|
if (state?.roleRefsMode === "aria") {
|
|
2019
|
-
return (state.roleRefsFrameSelector ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
|
|
2278
|
+
return (state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page).locator(`aria-ref=${normalized}`);
|
|
2020
2279
|
}
|
|
2021
2280
|
const info = state?.roleRefs?.[normalized];
|
|
2022
2281
|
if (!info) throw new Error(`Unknown ref "${normalized}". Run a new snapshot and use a ref from that snapshot.`);
|
|
2023
|
-
const locAny = state
|
|
2282
|
+
const locAny = state.roleRefsFrameSelector !== void 0 && state.roleRefsFrameSelector !== "" ? page.frameLocator(state.roleRefsFrameSelector) : page;
|
|
2024
2283
|
const role = info.role;
|
|
2025
|
-
const locator = info.name ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
|
|
2284
|
+
const locator = info.name !== void 0 && info.name !== "" ? locAny.getByRole(role, { name: info.name, exact: true }) : locAny.getByRole(role);
|
|
2026
2285
|
return info.nth !== void 0 ? locator.nth(info.nth) : locator;
|
|
2027
2286
|
}
|
|
2028
2287
|
return page.locator(`aria-ref=${normalized}`);
|
|
@@ -2030,15 +2289,21 @@ function refLocator(page, ref) {
|
|
|
2030
2289
|
function toAIFriendlyError(error, selector) {
|
|
2031
2290
|
const message = error instanceof Error ? error.message : String(error);
|
|
2032
2291
|
if (message.includes("strict mode violation")) {
|
|
2033
|
-
const countMatch =
|
|
2292
|
+
const countMatch = /resolved to (\d+) elements/.exec(message);
|
|
2034
2293
|
const count = countMatch ? countMatch[1] : "multiple";
|
|
2035
|
-
return new Error(
|
|
2294
|
+
return new Error(
|
|
2295
|
+
`Selector "${selector}" matched ${count} elements. Run a new snapshot to get updated refs, or use a different ref.`
|
|
2296
|
+
);
|
|
2036
2297
|
}
|
|
2037
2298
|
if ((message.includes("Timeout") || message.includes("waiting for")) && (message.includes("to be visible") || message.includes("not visible"))) {
|
|
2038
|
-
return new Error(
|
|
2299
|
+
return new Error(
|
|
2300
|
+
`Element "${selector}" not found or not visible. Run a new snapshot to see current page elements.`
|
|
2301
|
+
);
|
|
2039
2302
|
}
|
|
2040
2303
|
if (message.includes("intercepts pointer events") || message.includes("not visible") || message.includes("not receive pointer events")) {
|
|
2041
|
-
return new Error(
|
|
2304
|
+
return new Error(
|
|
2305
|
+
`Element "${selector}" is not interactable (hidden or covered). Try scrolling it into view, closing overlays, or re-snapshotting.`
|
|
2306
|
+
);
|
|
2042
2307
|
}
|
|
2043
2308
|
return error instanceof Error ? error : new Error(message);
|
|
2044
2309
|
}
|
|
@@ -2046,437 +2311,151 @@ function normalizeTimeoutMs(timeoutMs, fallback, maxMs = 12e4) {
|
|
|
2046
2311
|
return Math.max(500, Math.min(maxMs, timeoutMs ?? fallback));
|
|
2047
2312
|
}
|
|
2048
2313
|
|
|
2049
|
-
// src/
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
"
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
"columnheader",
|
|
2074
|
-
"rowheader",
|
|
2075
|
-
"listitem",
|
|
2076
|
-
"article",
|
|
2077
|
-
"region",
|
|
2078
|
-
"main",
|
|
2079
|
-
"navigation"
|
|
2080
|
-
]);
|
|
2081
|
-
var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
2082
|
-
"generic",
|
|
2083
|
-
"group",
|
|
2084
|
-
"list",
|
|
2085
|
-
"table",
|
|
2086
|
-
"row",
|
|
2087
|
-
"rowgroup",
|
|
2088
|
-
"grid",
|
|
2089
|
-
"treegrid",
|
|
2090
|
-
"menu",
|
|
2091
|
-
"menubar",
|
|
2092
|
-
"toolbar",
|
|
2093
|
-
"tablist",
|
|
2094
|
-
"tree",
|
|
2095
|
-
"directory",
|
|
2096
|
-
"document",
|
|
2097
|
-
"application",
|
|
2098
|
-
"presentation",
|
|
2099
|
-
"none"
|
|
2100
|
-
]);
|
|
2101
|
-
function getIndentLevel(line) {
|
|
2102
|
-
const match = line.match(/^(\s*)/);
|
|
2103
|
-
return match ? Math.floor(match[1].length / 2) : 0;
|
|
2104
|
-
}
|
|
2105
|
-
function matchInteractiveSnapshotLine(line, options) {
|
|
2106
|
-
const depth = getIndentLevel(line);
|
|
2107
|
-
if (options.maxDepth !== void 0 && depth > options.maxDepth) {
|
|
2108
|
-
return null;
|
|
2109
|
-
}
|
|
2110
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
2111
|
-
if (!match) {
|
|
2112
|
-
return null;
|
|
2113
|
-
}
|
|
2114
|
-
const [, , roleRaw, name, suffix] = match;
|
|
2115
|
-
if (roleRaw.startsWith("/")) {
|
|
2116
|
-
return null;
|
|
2117
|
-
}
|
|
2118
|
-
const role = roleRaw.toLowerCase();
|
|
2119
|
-
return {
|
|
2120
|
-
roleRaw,
|
|
2121
|
-
role,
|
|
2122
|
-
...name ? { name } : {},
|
|
2123
|
-
suffix
|
|
2124
|
-
};
|
|
2125
|
-
}
|
|
2126
|
-
function createRoleNameTracker() {
|
|
2127
|
-
const counts = /* @__PURE__ */ new Map();
|
|
2128
|
-
const refsByKey = /* @__PURE__ */ new Map();
|
|
2129
|
-
return {
|
|
2130
|
-
counts,
|
|
2131
|
-
refsByKey,
|
|
2132
|
-
getKey(role, name) {
|
|
2133
|
-
return `${role}:${name ?? ""}`;
|
|
2134
|
-
},
|
|
2135
|
-
getNextIndex(role, name) {
|
|
2136
|
-
const key = this.getKey(role, name);
|
|
2137
|
-
const current = counts.get(key) ?? 0;
|
|
2138
|
-
counts.set(key, current + 1);
|
|
2139
|
-
return current;
|
|
2140
|
-
},
|
|
2141
|
-
trackRef(role, name, ref) {
|
|
2142
|
-
const key = this.getKey(role, name);
|
|
2143
|
-
const list = refsByKey.get(key) ?? [];
|
|
2144
|
-
list.push(ref);
|
|
2145
|
-
refsByKey.set(key, list);
|
|
2146
|
-
},
|
|
2147
|
-
getDuplicateKeys() {
|
|
2148
|
-
const out = /* @__PURE__ */ new Set();
|
|
2149
|
-
for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
|
|
2150
|
-
return out;
|
|
2314
|
+
// src/actions/evaluate.ts
|
|
2315
|
+
async function evaluateInAllFramesViaPlaywright(opts) {
|
|
2316
|
+
const fnText = opts.fn.trim();
|
|
2317
|
+
if (!fnText) throw new Error("function is required");
|
|
2318
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2319
|
+
const frames = page.frames();
|
|
2320
|
+
const results = [];
|
|
2321
|
+
for (const frame of frames) {
|
|
2322
|
+
try {
|
|
2323
|
+
const result = await frame.evaluate((fnBody) => {
|
|
2324
|
+
"use strict";
|
|
2325
|
+
try {
|
|
2326
|
+
const candidate = (0, eval)("(" + fnBody + ")");
|
|
2327
|
+
return typeof candidate === "function" ? candidate() : candidate;
|
|
2328
|
+
} catch (err) {
|
|
2329
|
+
throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
|
|
2330
|
+
}
|
|
2331
|
+
}, fnText);
|
|
2332
|
+
results.push({
|
|
2333
|
+
frameUrl: frame.url(),
|
|
2334
|
+
frameName: frame.name(),
|
|
2335
|
+
result
|
|
2336
|
+
});
|
|
2337
|
+
} catch {
|
|
2151
2338
|
}
|
|
2152
|
-
}
|
|
2339
|
+
}
|
|
2340
|
+
return results;
|
|
2153
2341
|
}
|
|
2154
|
-
function
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2342
|
+
async function awaitEvalWithAbort(evalPromise, abortPromise) {
|
|
2343
|
+
if (!abortPromise) return await evalPromise;
|
|
2344
|
+
try {
|
|
2345
|
+
return await Promise.race([evalPromise, abortPromise]);
|
|
2346
|
+
} catch (err) {
|
|
2347
|
+
evalPromise.catch(() => {
|
|
2348
|
+
});
|
|
2349
|
+
throw err;
|
|
2159
2350
|
}
|
|
2160
2351
|
}
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
|
|
2164
|
-
|
|
2165
|
-
|
|
2166
|
-
|
|
2167
|
-
|
|
2168
|
-
|
|
2169
|
-
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
|
|
2174
|
-
|
|
2175
|
-
|
|
2176
|
-
for (let j = i + 1; j < lines.length; j++) {
|
|
2177
|
-
if (getIndentLevel(lines[j]) <= currentIndent) break;
|
|
2178
|
-
if (lines[j]?.includes("[ref=")) {
|
|
2179
|
-
hasRelevantChildren = true;
|
|
2180
|
-
break;
|
|
2181
|
-
}
|
|
2182
|
-
}
|
|
2183
|
-
if (hasRelevantChildren) result.push(line);
|
|
2184
|
-
}
|
|
2185
|
-
return result.join("\n");
|
|
2186
|
-
}
|
|
2187
|
-
function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
2188
|
-
const lines = ariaSnapshot.split("\n");
|
|
2189
|
-
const refs = {};
|
|
2190
|
-
const tracker = createRoleNameTracker();
|
|
2191
|
-
let counter = 0;
|
|
2192
|
-
const nextRef = () => {
|
|
2193
|
-
counter++;
|
|
2194
|
-
return `e${counter}`;
|
|
2195
|
-
};
|
|
2196
|
-
if (options.interactive) {
|
|
2197
|
-
const result2 = [];
|
|
2198
|
-
for (const line of lines) {
|
|
2199
|
-
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
2200
|
-
if (!parsed) continue;
|
|
2201
|
-
const { roleRaw, role, name, suffix } = parsed;
|
|
2202
|
-
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
2203
|
-
const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
|
|
2204
|
-
const ref = nextRef();
|
|
2205
|
-
const nth = tracker.getNextIndex(role, name);
|
|
2206
|
-
tracker.trackRef(role, name, ref);
|
|
2207
|
-
refs[ref] = { role, name, nth };
|
|
2208
|
-
let enhanced = `${prefix}${roleRaw}`;
|
|
2209
|
-
if (name) enhanced += ` "${name}"`;
|
|
2210
|
-
enhanced += ` [ref=${ref}]`;
|
|
2211
|
-
if (nth > 0) enhanced += ` [nth=${nth}]`;
|
|
2212
|
-
if (suffix.includes("[")) enhanced += suffix;
|
|
2213
|
-
result2.push(enhanced);
|
|
2214
|
-
}
|
|
2215
|
-
removeNthFromNonDuplicates(refs, tracker);
|
|
2216
|
-
return { snapshot: result2.join("\n") || "(no interactive elements)", refs };
|
|
2217
|
-
}
|
|
2218
|
-
const result = [];
|
|
2219
|
-
for (const line of lines) {
|
|
2220
|
-
const depth = getIndentLevel(line);
|
|
2221
|
-
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
2222
|
-
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
2223
|
-
if (!match) {
|
|
2224
|
-
result.push(line);
|
|
2225
|
-
continue;
|
|
2226
|
-
}
|
|
2227
|
-
const [, prefix, roleRaw, name, suffix] = match;
|
|
2228
|
-
if (roleRaw.startsWith("/")) {
|
|
2229
|
-
result.push(line);
|
|
2230
|
-
continue;
|
|
2231
|
-
}
|
|
2232
|
-
const role = roleRaw.toLowerCase();
|
|
2233
|
-
const isInteractive = INTERACTIVE_ROLES.has(role);
|
|
2234
|
-
const isContent = CONTENT_ROLES.has(role);
|
|
2235
|
-
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
2236
|
-
if (options.compact && isStructural && !name) continue;
|
|
2237
|
-
if (!(isInteractive || isContent && name)) {
|
|
2238
|
-
result.push(line);
|
|
2239
|
-
continue;
|
|
2240
|
-
}
|
|
2241
|
-
const ref = nextRef();
|
|
2242
|
-
const nth = tracker.getNextIndex(role, name);
|
|
2243
|
-
tracker.trackRef(role, name, ref);
|
|
2244
|
-
refs[ref] = { role, name, nth };
|
|
2245
|
-
let enhanced = `${prefix}${roleRaw}`;
|
|
2246
|
-
if (name) enhanced += ` "${name}"`;
|
|
2247
|
-
enhanced += ` [ref=${ref}]`;
|
|
2248
|
-
if (nth > 0) enhanced += ` [nth=${nth}]`;
|
|
2249
|
-
if (suffix) enhanced += suffix;
|
|
2250
|
-
result.push(enhanced);
|
|
2251
|
-
}
|
|
2252
|
-
removeNthFromNonDuplicates(refs, tracker);
|
|
2253
|
-
const tree = result.join("\n") || "(empty)";
|
|
2254
|
-
return { snapshot: options.compact ? compactTree(tree) : tree, refs };
|
|
2255
|
-
}
|
|
2256
|
-
function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
2257
|
-
const lines = String(aiSnapshot ?? "").split("\n");
|
|
2258
|
-
const refs = {};
|
|
2259
|
-
function parseAiSnapshotRef(suffix) {
|
|
2260
|
-
const match = suffix.match(/\[ref=(e\d+)\]/i);
|
|
2261
|
-
return match ? match[1] : null;
|
|
2262
|
-
}
|
|
2263
|
-
if (options.interactive) {
|
|
2264
|
-
const out2 = [];
|
|
2265
|
-
for (const line of lines) {
|
|
2266
|
-
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
2267
|
-
if (!parsed) continue;
|
|
2268
|
-
const { roleRaw, role, name, suffix } = parsed;
|
|
2269
|
-
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
2270
|
-
const ref = parseAiSnapshotRef(suffix);
|
|
2271
|
-
if (!ref) continue;
|
|
2272
|
-
const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
|
|
2273
|
-
refs[ref] = { role, ...name ? { name } : {} };
|
|
2274
|
-
out2.push(`${prefix}${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
|
|
2352
|
+
var BROWSER_EVALUATOR = new Function(
|
|
2353
|
+
"args",
|
|
2354
|
+
`
|
|
2355
|
+
"use strict";
|
|
2356
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2357
|
+
try {
|
|
2358
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2359
|
+
var result = typeof candidate === "function" ? candidate() : candidate;
|
|
2360
|
+
if (result && typeof result.then === "function") {
|
|
2361
|
+
return Promise.race([
|
|
2362
|
+
result,
|
|
2363
|
+
new Promise(function(_, reject) {
|
|
2364
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2365
|
+
})
|
|
2366
|
+
]);
|
|
2275
2367
|
}
|
|
2276
|
-
return
|
|
2368
|
+
return result;
|
|
2369
|
+
} catch (err) {
|
|
2370
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2277
2371
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
2284
|
-
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
|
|
2290
|
-
|
|
2372
|
+
`
|
|
2373
|
+
);
|
|
2374
|
+
var ELEMENT_EVALUATOR = new Function(
|
|
2375
|
+
"el",
|
|
2376
|
+
"args",
|
|
2377
|
+
`
|
|
2378
|
+
"use strict";
|
|
2379
|
+
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
2380
|
+
try {
|
|
2381
|
+
var candidate = eval("(" + fnBody + ")");
|
|
2382
|
+
var result = typeof candidate === "function" ? candidate(el) : candidate;
|
|
2383
|
+
if (result && typeof result.then === "function") {
|
|
2384
|
+
return Promise.race([
|
|
2385
|
+
result,
|
|
2386
|
+
new Promise(function(_, reject) {
|
|
2387
|
+
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
2388
|
+
})
|
|
2389
|
+
]);
|
|
2291
2390
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
2295
|
-
const ref = parseAiSnapshotRef(suffix);
|
|
2296
|
-
if (ref) refs[ref] = { role, ...name ? { name } : {} };
|
|
2297
|
-
out.push(line);
|
|
2298
|
-
}
|
|
2299
|
-
const tree = out.join("\n") || "(empty)";
|
|
2300
|
-
return { snapshot: options.compact ? compactTree(tree) : tree, refs };
|
|
2301
|
-
}
|
|
2302
|
-
function getRoleSnapshotStats(snapshot, refs) {
|
|
2303
|
-
const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
|
|
2304
|
-
return {
|
|
2305
|
-
lines: snapshot.split("\n").length,
|
|
2306
|
-
chars: snapshot.length,
|
|
2307
|
-
refs: Object.keys(refs).length,
|
|
2308
|
-
interactive
|
|
2309
|
-
};
|
|
2310
|
-
}
|
|
2311
|
-
|
|
2312
|
-
// src/snapshot/ai-snapshot.ts
|
|
2313
|
-
async function snapshotAi(opts) {
|
|
2314
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2315
|
-
ensurePageState(page);
|
|
2316
|
-
const maybe = page;
|
|
2317
|
-
if (!maybe._snapshotForAI) {
|
|
2318
|
-
throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
|
|
2319
|
-
}
|
|
2320
|
-
const sourceUrl = page.url();
|
|
2321
|
-
const result = await maybe._snapshotForAI({
|
|
2322
|
-
timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
|
|
2323
|
-
track: "response"
|
|
2324
|
-
});
|
|
2325
|
-
let snapshot = String(result?.full ?? "");
|
|
2326
|
-
const maxChars = opts.maxChars;
|
|
2327
|
-
const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
|
|
2328
|
-
let truncated = false;
|
|
2329
|
-
if (limit && snapshot.length > limit) {
|
|
2330
|
-
const lastNewline = snapshot.lastIndexOf("\n", limit);
|
|
2331
|
-
const cutoff = lastNewline > 0 ? lastNewline : limit;
|
|
2332
|
-
snapshot = `${snapshot.slice(0, cutoff)}
|
|
2333
|
-
|
|
2334
|
-
[...TRUNCATED - page too large]`;
|
|
2335
|
-
truncated = true;
|
|
2391
|
+
return result;
|
|
2392
|
+
} catch (err) {
|
|
2393
|
+
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
2336
2394
|
}
|
|
2337
|
-
|
|
2338
|
-
|
|
2339
|
-
|
|
2340
|
-
|
|
2341
|
-
|
|
2342
|
-
refs: built.refs,
|
|
2343
|
-
mode: "aria"
|
|
2344
|
-
});
|
|
2345
|
-
return {
|
|
2346
|
-
snapshot: built.snapshot,
|
|
2347
|
-
refs: built.refs,
|
|
2348
|
-
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
2349
|
-
...truncated ? { truncated } : {},
|
|
2350
|
-
untrusted: true,
|
|
2351
|
-
contentMeta: {
|
|
2352
|
-
sourceUrl,
|
|
2353
|
-
contentType: "browser-snapshot",
|
|
2354
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2355
|
-
}
|
|
2356
|
-
};
|
|
2357
|
-
}
|
|
2358
|
-
|
|
2359
|
-
// src/snapshot/aria-snapshot.ts
|
|
2360
|
-
async function snapshotRole(opts) {
|
|
2395
|
+
`
|
|
2396
|
+
);
|
|
2397
|
+
async function evaluateViaPlaywright(opts) {
|
|
2398
|
+
const fnText = opts.fn.trim();
|
|
2399
|
+
if (!fnText) throw new Error("function is required");
|
|
2361
2400
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
2362
2401
|
ensurePageState(page);
|
|
2363
|
-
|
|
2364
|
-
|
|
2365
|
-
|
|
2366
|
-
|
|
2367
|
-
|
|
2368
|
-
|
|
2369
|
-
|
|
2370
|
-
|
|
2371
|
-
|
|
2372
|
-
|
|
2373
|
-
|
|
2374
|
-
|
|
2375
|
-
|
|
2376
|
-
cdpUrl: opts.cdpUrl,
|
|
2377
|
-
targetId: opts.targetId,
|
|
2378
|
-
refs: built2.refs,
|
|
2379
|
-
mode: "aria"
|
|
2402
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
2403
|
+
const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
2404
|
+
let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
|
|
2405
|
+
evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
|
|
2406
|
+
const signal = opts.signal;
|
|
2407
|
+
let abortListener;
|
|
2408
|
+
let abortReject;
|
|
2409
|
+
let abortPromise;
|
|
2410
|
+
if (signal !== void 0) {
|
|
2411
|
+
abortPromise = new Promise((_, reject) => {
|
|
2412
|
+
abortReject = reject;
|
|
2413
|
+
});
|
|
2414
|
+
abortPromise.catch(() => {
|
|
2380
2415
|
});
|
|
2381
|
-
return {
|
|
2382
|
-
snapshot: built2.snapshot,
|
|
2383
|
-
refs: built2.refs,
|
|
2384
|
-
stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
|
|
2385
|
-
untrusted: true,
|
|
2386
|
-
contentMeta: {
|
|
2387
|
-
sourceUrl,
|
|
2388
|
-
contentType: "browser-snapshot",
|
|
2389
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2390
|
-
}
|
|
2391
|
-
};
|
|
2392
2416
|
}
|
|
2393
|
-
|
|
2394
|
-
|
|
2395
|
-
|
|
2396
|
-
|
|
2397
|
-
|
|
2398
|
-
|
|
2399
|
-
|
|
2400
|
-
|
|
2401
|
-
|
|
2402
|
-
|
|
2403
|
-
frameSelector: frameSelector || void 0,
|
|
2404
|
-
mode: "role"
|
|
2405
|
-
});
|
|
2406
|
-
return {
|
|
2407
|
-
snapshot: built.snapshot,
|
|
2408
|
-
refs: built.refs,
|
|
2409
|
-
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
2410
|
-
untrusted: true,
|
|
2411
|
-
contentMeta: {
|
|
2412
|
-
sourceUrl,
|
|
2413
|
-
contentType: "browser-snapshot",
|
|
2414
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2417
|
+
if (signal !== void 0) {
|
|
2418
|
+
const disconnect = () => {
|
|
2419
|
+
forceDisconnectPlaywrightForTarget({
|
|
2420
|
+
cdpUrl: opts.cdpUrl,
|
|
2421
|
+
targetId: opts.targetId}).catch(() => {
|
|
2422
|
+
});
|
|
2423
|
+
};
|
|
2424
|
+
if (signal.aborted) {
|
|
2425
|
+
disconnect();
|
|
2426
|
+
throw signal.reason ?? new Error("aborted");
|
|
2415
2427
|
}
|
|
2416
|
-
|
|
2417
|
-
|
|
2418
|
-
|
|
2419
|
-
|
|
2420
|
-
|
|
2421
|
-
|
|
2422
|
-
|
|
2423
|
-
|
|
2424
|
-
await session.send("Accessibility.enable").catch(() => {
|
|
2425
|
-
});
|
|
2426
|
-
return await session.send("Accessibility.getFullAXTree");
|
|
2427
|
-
});
|
|
2428
|
-
return {
|
|
2429
|
-
nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit),
|
|
2430
|
-
untrusted: true,
|
|
2431
|
-
contentMeta: {
|
|
2432
|
-
sourceUrl,
|
|
2433
|
-
contentType: "browser-aria-tree",
|
|
2434
|
-
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
2428
|
+
abortListener = () => {
|
|
2429
|
+
disconnect();
|
|
2430
|
+
abortReject?.(signal.reason ?? new Error("aborted"));
|
|
2431
|
+
};
|
|
2432
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
2433
|
+
if (signal.aborted) {
|
|
2434
|
+
abortListener();
|
|
2435
|
+
throw signal.reason ?? new Error("aborted");
|
|
2435
2436
|
}
|
|
2436
|
-
}
|
|
2437
|
-
|
|
2438
|
-
|
|
2439
|
-
|
|
2440
|
-
|
|
2441
|
-
|
|
2442
|
-
|
|
2443
|
-
|
|
2444
|
-
}
|
|
2445
|
-
|
|
2446
|
-
|
|
2447
|
-
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
2448
|
-
const referenced = /* @__PURE__ */ new Set();
|
|
2449
|
-
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
|
|
2450
|
-
const root = nodes.find((n) => n.nodeId && !referenced.has(n.nodeId)) ?? nodes[0];
|
|
2451
|
-
if (!root?.nodeId) return [];
|
|
2452
|
-
const out = [];
|
|
2453
|
-
const stack = [{ id: root.nodeId, depth: 0 }];
|
|
2454
|
-
while (stack.length && out.length < limit) {
|
|
2455
|
-
const popped = stack.pop();
|
|
2456
|
-
if (!popped) break;
|
|
2457
|
-
const { id, depth } = popped;
|
|
2458
|
-
const n = byId.get(id);
|
|
2459
|
-
if (!n) continue;
|
|
2460
|
-
const role = axValue(n.role);
|
|
2461
|
-
const name = axValue(n.name);
|
|
2462
|
-
const value = axValue(n.value);
|
|
2463
|
-
const description = axValue(n.description);
|
|
2464
|
-
const ref = `ax${out.length + 1}`;
|
|
2465
|
-
out.push({
|
|
2466
|
-
ref,
|
|
2467
|
-
role: role || "unknown",
|
|
2468
|
-
name: name || "",
|
|
2469
|
-
...value ? { value } : {},
|
|
2470
|
-
...description ? { description } : {},
|
|
2471
|
-
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
|
|
2472
|
-
depth
|
|
2473
|
-
});
|
|
2474
|
-
const children = (n.childIds ?? []).filter((c) => byId.has(c));
|
|
2475
|
-
for (let i = children.length - 1; i >= 0; i--) {
|
|
2476
|
-
if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
|
|
2437
|
+
}
|
|
2438
|
+
try {
|
|
2439
|
+
if (opts.ref !== void 0 && opts.ref !== "") {
|
|
2440
|
+
const locator = refLocator(page, opts.ref);
|
|
2441
|
+
return await awaitEvalWithAbort(
|
|
2442
|
+
locator.evaluate(ELEMENT_EVALUATOR, {
|
|
2443
|
+
fnBody: fnText,
|
|
2444
|
+
timeoutMs: evaluateTimeout
|
|
2445
|
+
}),
|
|
2446
|
+
abortPromise
|
|
2447
|
+
);
|
|
2477
2448
|
}
|
|
2449
|
+
return await awaitEvalWithAbort(
|
|
2450
|
+
page.evaluate(BROWSER_EVALUATOR, {
|
|
2451
|
+
fnBody: fnText,
|
|
2452
|
+
timeoutMs: evaluateTimeout
|
|
2453
|
+
}),
|
|
2454
|
+
abortPromise
|
|
2455
|
+
);
|
|
2456
|
+
} finally {
|
|
2457
|
+
if (signal && abortListener) signal.removeEventListener("abort", abortListener);
|
|
2478
2458
|
}
|
|
2479
|
-
return out;
|
|
2480
2459
|
}
|
|
2481
2460
|
|
|
2482
2461
|
// src/security.ts
|
|
@@ -2493,11 +2472,7 @@ function withBrowserNavigationPolicy(ssrfPolicy) {
|
|
|
2493
2472
|
var NETWORK_NAVIGATION_PROTOCOLS = /* @__PURE__ */ new Set(["http:", "https:"]);
|
|
2494
2473
|
var SAFE_NON_NETWORK_URLS = /* @__PURE__ */ new Set(["about:blank"]);
|
|
2495
2474
|
var PROXY_ENV_KEYS = ["HTTP_PROXY", "HTTPS_PROXY", "ALL_PROXY", "http_proxy", "https_proxy", "all_proxy"];
|
|
2496
|
-
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set([
|
|
2497
|
-
"localhost",
|
|
2498
|
-
"localhost.localdomain",
|
|
2499
|
-
"metadata.google.internal"
|
|
2500
|
-
]);
|
|
2475
|
+
var BLOCKED_HOSTNAMES = /* @__PURE__ */ new Set(["localhost", "localhost.localdomain", "metadata.google.internal"]);
|
|
2501
2476
|
function isAllowedNonNetworkNavigationUrl(parsed) {
|
|
2502
2477
|
return SAFE_NON_NETWORK_URLS.has(parsed.href);
|
|
2503
2478
|
}
|
|
@@ -2512,7 +2487,7 @@ function hasProxyEnvConfigured2(env = process.env) {
|
|
|
2512
2487
|
return false;
|
|
2513
2488
|
}
|
|
2514
2489
|
function normalizeHostname(hostname) {
|
|
2515
|
-
let h =
|
|
2490
|
+
let h = hostname.trim().toLowerCase();
|
|
2516
2491
|
if (h.startsWith("[") && h.endsWith("]")) h = h.slice(1, -1);
|
|
2517
2492
|
if (h.endsWith(".")) h = h.slice(0, -1);
|
|
2518
2493
|
return h;
|
|
@@ -2531,13 +2506,7 @@ var BLOCKED_IPV4_RANGES = /* @__PURE__ */ new Set([
|
|
|
2531
2506
|
"private",
|
|
2532
2507
|
"reserved"
|
|
2533
2508
|
]);
|
|
2534
|
-
var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set([
|
|
2535
|
-
"unspecified",
|
|
2536
|
-
"loopback",
|
|
2537
|
-
"linkLocal",
|
|
2538
|
-
"uniqueLocal",
|
|
2539
|
-
"multicast"
|
|
2540
|
-
]);
|
|
2509
|
+
var BLOCKED_IPV6_RANGES = /* @__PURE__ */ new Set(["unspecified", "loopback", "linkLocal", "uniqueLocal", "multicast"]);
|
|
2541
2510
|
var RFC2544_BENCHMARK_PREFIX = [ipaddr.IPv4.parse("198.18.0.0"), 15];
|
|
2542
2511
|
var EMBEDDED_IPV4_SENTINEL_RULES = [
|
|
2543
2512
|
// IPv4-compatible (::a.b.c.d)
|
|
@@ -2586,12 +2555,12 @@ function parseIpv6WithEmbeddedIpv4(raw) {
|
|
|
2586
2555
|
}
|
|
2587
2556
|
function normalizeIpParseInput(raw) {
|
|
2588
2557
|
const trimmed = raw?.trim();
|
|
2589
|
-
if (
|
|
2558
|
+
if (trimmed === void 0 || trimmed === "") return;
|
|
2590
2559
|
return stripIpv6Brackets(trimmed);
|
|
2591
2560
|
}
|
|
2592
2561
|
function parseCanonicalIpAddress(raw) {
|
|
2593
2562
|
const normalized = normalizeIpParseInput(raw);
|
|
2594
|
-
if (
|
|
2563
|
+
if (normalized === void 0) return;
|
|
2595
2564
|
if (ipaddr.IPv4.isValid(normalized)) {
|
|
2596
2565
|
if (!ipaddr.IPv4.isValidFourPartDecimal(normalized)) return;
|
|
2597
2566
|
return ipaddr.IPv4.parse(normalized);
|
|
@@ -2601,20 +2570,20 @@ function parseCanonicalIpAddress(raw) {
|
|
|
2601
2570
|
}
|
|
2602
2571
|
function parseLooseIpAddress(raw) {
|
|
2603
2572
|
const normalized = normalizeIpParseInput(raw);
|
|
2604
|
-
if (
|
|
2573
|
+
if (normalized === void 0) return;
|
|
2605
2574
|
if (ipaddr.isValid(normalized)) return ipaddr.parse(normalized);
|
|
2606
2575
|
return parseIpv6WithEmbeddedIpv4(normalized);
|
|
2607
2576
|
}
|
|
2608
2577
|
function isCanonicalDottedDecimalIPv4(raw) {
|
|
2609
|
-
const trimmed = raw
|
|
2610
|
-
if (
|
|
2578
|
+
const trimmed = raw.trim();
|
|
2579
|
+
if (trimmed === "") return false;
|
|
2611
2580
|
const normalized = stripIpv6Brackets(trimmed);
|
|
2612
2581
|
if (!normalized) return false;
|
|
2613
2582
|
return ipaddr.IPv4.isValidFourPartDecimal(normalized);
|
|
2614
2583
|
}
|
|
2615
2584
|
function isLegacyIpv4Literal(raw) {
|
|
2616
|
-
const trimmed = raw
|
|
2617
|
-
if (
|
|
2585
|
+
const trimmed = raw.trim();
|
|
2586
|
+
if (trimmed === "") return false;
|
|
2618
2587
|
const normalized = stripIpv6Brackets(trimmed);
|
|
2619
2588
|
if (!normalized || normalized.includes(":")) return false;
|
|
2620
2589
|
if (isCanonicalDottedDecimalIPv4(normalized)) return false;
|
|
@@ -2640,12 +2609,7 @@ function isBlockedSpecialUseIpv6Address(address) {
|
|
|
2640
2609
|
return (address.parts[0] & 65472) === 65216;
|
|
2641
2610
|
}
|
|
2642
2611
|
function decodeIpv4FromHextets(high, low) {
|
|
2643
|
-
const octets = [
|
|
2644
|
-
high >>> 8 & 255,
|
|
2645
|
-
high & 255,
|
|
2646
|
-
low >>> 8 & 255,
|
|
2647
|
-
low & 255
|
|
2648
|
-
];
|
|
2612
|
+
const octets = [high >>> 8 & 255, high & 255, low >>> 8 & 255, low & 255];
|
|
2649
2613
|
return ipaddr.IPv4.parse(octets.join("."));
|
|
2650
2614
|
}
|
|
2651
2615
|
function extractEmbeddedIpv4FromIpv6(address) {
|
|
@@ -2692,9 +2656,7 @@ function normalizeHostnameSet(values) {
|
|
|
2692
2656
|
function normalizeHostnameAllowlist(values) {
|
|
2693
2657
|
if (!values || values.length === 0) return [];
|
|
2694
2658
|
return Array.from(
|
|
2695
|
-
new Set(
|
|
2696
|
-
values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0)
|
|
2697
|
-
)
|
|
2659
|
+
new Set(values.map((v) => normalizeHostname(v)).filter((v) => v !== "*" && v !== "*." && v.length > 0))
|
|
2698
2660
|
);
|
|
2699
2661
|
}
|
|
2700
2662
|
function isHostnameAllowedByPattern(hostname, pattern) {
|
|
@@ -2729,19 +2691,25 @@ function createPinnedLookup(params) {
|
|
|
2729
2691
|
family: address.includes(":") ? 6 : 4
|
|
2730
2692
|
}));
|
|
2731
2693
|
let index = 0;
|
|
2732
|
-
return ((
|
|
2733
|
-
const
|
|
2734
|
-
|
|
2735
|
-
const
|
|
2736
|
-
if (
|
|
2737
|
-
|
|
2738
|
-
|
|
2694
|
+
return ((_host, ...rest) => {
|
|
2695
|
+
const second = rest[0];
|
|
2696
|
+
const third = rest[1];
|
|
2697
|
+
const cb = typeof second === "function" ? second : typeof third === "function" ? third : void 0;
|
|
2698
|
+
if (cb === void 0) return;
|
|
2699
|
+
const normalized = normalizeHostname(_host);
|
|
2700
|
+
if (normalized === "" || normalized !== normalizedHost) {
|
|
2701
|
+
if (typeof second === "function" || second === void 0) {
|
|
2702
|
+
fallback(_host, cb);
|
|
2703
|
+
return;
|
|
2704
|
+
}
|
|
2705
|
+
fallback(_host, second, cb);
|
|
2706
|
+
return;
|
|
2739
2707
|
}
|
|
2740
|
-
const opts = typeof
|
|
2741
|
-
const requestedFamily = typeof
|
|
2708
|
+
const opts = typeof second === "object" ? second : {};
|
|
2709
|
+
const requestedFamily = typeof second === "number" ? second : typeof opts.family === "number" ? opts.family : 0;
|
|
2742
2710
|
const candidates = requestedFamily === 4 || requestedFamily === 6 ? records.filter((entry) => entry.family === requestedFamily) : records;
|
|
2743
2711
|
const usable = candidates.length > 0 ? candidates : records;
|
|
2744
|
-
if (opts.all) {
|
|
2712
|
+
if (opts.all === true) {
|
|
2745
2713
|
cb(null, usable);
|
|
2746
2714
|
return;
|
|
2747
2715
|
}
|
|
@@ -2759,9 +2727,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2759
2727
|
const isExplicitlyAllowed = allowedHostnames.has(normalized);
|
|
2760
2728
|
const skipPrivateNetworkChecks = allowPrivateNetwork || isExplicitlyAllowed;
|
|
2761
2729
|
if (!matchesHostnameAllowlist(normalized, hostnameAllowlist)) {
|
|
2762
|
-
throw new InvalidBrowserNavigationUrlError(
|
|
2763
|
-
`Navigation blocked: hostname "${hostname}" is not in the allowlist.`
|
|
2764
|
-
);
|
|
2730
|
+
throw new InvalidBrowserNavigationUrlError(`Navigation blocked: hostname "${hostname}" is not in the allowlist.`);
|
|
2765
2731
|
}
|
|
2766
2732
|
if (!skipPrivateNetworkChecks) {
|
|
2767
2733
|
if (isBlockedHostnameOrIp(normalized, params.policy)) {
|
|
@@ -2779,7 +2745,7 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2779
2745
|
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
2780
2746
|
);
|
|
2781
2747
|
}
|
|
2782
|
-
if (
|
|
2748
|
+
if (results.length === 0) {
|
|
2783
2749
|
throw new InvalidBrowserNavigationUrlError(
|
|
2784
2750
|
`Navigation to internal/loopback address blocked: unable to resolve "${hostname}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
|
|
2785
2751
|
);
|
|
@@ -2806,8 +2772,8 @@ async function resolvePinnedHostnameWithPolicy(hostname, params = {}) {
|
|
|
2806
2772
|
};
|
|
2807
2773
|
}
|
|
2808
2774
|
async function assertBrowserNavigationAllowed(opts) {
|
|
2809
|
-
const rawUrl =
|
|
2810
|
-
if (
|
|
2775
|
+
const rawUrl = opts.url.trim();
|
|
2776
|
+
if (rawUrl === "") throw new InvalidBrowserNavigationUrlError("url is required");
|
|
2811
2777
|
let parsed;
|
|
2812
2778
|
try {
|
|
2813
2779
|
parsed = new URL(rawUrl);
|
|
@@ -2836,7 +2802,7 @@ async function assertSafeOutputPath(path2, allowedRoots) {
|
|
|
2836
2802
|
if (normalized.includes("..")) {
|
|
2837
2803
|
throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
|
|
2838
2804
|
}
|
|
2839
|
-
if (allowedRoots
|
|
2805
|
+
if (allowedRoots !== void 0 && allowedRoots.length > 0) {
|
|
2840
2806
|
const resolved = resolve(normalized);
|
|
2841
2807
|
let parentReal;
|
|
2842
2808
|
try {
|
|
@@ -2894,8 +2860,8 @@ async function resolveStrictExistingUploadPaths(params) {
|
|
|
2894
2860
|
}
|
|
2895
2861
|
}
|
|
2896
2862
|
function sanitizeUntrustedFileName(fileName, fallbackName) {
|
|
2897
|
-
const trimmed =
|
|
2898
|
-
if (
|
|
2863
|
+
const trimmed = fileName.trim();
|
|
2864
|
+
if (trimmed === "") return fallbackName;
|
|
2899
2865
|
let base = posix.basename(trimmed);
|
|
2900
2866
|
base = win32.basename(base);
|
|
2901
2867
|
let cleaned = "";
|
|
@@ -2929,16 +2895,17 @@ async function writeViaSiblingTempPath(params) {
|
|
|
2929
2895
|
await rename(tempPath, targetPath);
|
|
2930
2896
|
renameSucceeded = true;
|
|
2931
2897
|
} finally {
|
|
2932
|
-
if (!renameSucceeded)
|
|
2933
|
-
|
|
2898
|
+
if (!renameSucceeded)
|
|
2899
|
+
await rm(tempPath, { force: true }).catch(() => {
|
|
2900
|
+
});
|
|
2934
2901
|
}
|
|
2935
2902
|
}
|
|
2936
2903
|
function isAbsolute(p) {
|
|
2937
2904
|
return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
|
|
2938
2905
|
}
|
|
2939
2906
|
async function assertBrowserNavigationResultAllowed(opts) {
|
|
2940
|
-
const rawUrl =
|
|
2941
|
-
if (
|
|
2907
|
+
const rawUrl = opts.url.trim();
|
|
2908
|
+
if (rawUrl === "") return;
|
|
2942
2909
|
let parsed;
|
|
2943
2910
|
try {
|
|
2944
2911
|
parsed = new URL(rawUrl);
|
|
@@ -2968,18 +2935,20 @@ function requiresInspectableBrowserNavigationRedirects(ssrfPolicy) {
|
|
|
2968
2935
|
var MAX_CLICK_DELAY_MS = 5e3;
|
|
2969
2936
|
var CHECKABLE_ROLES = /* @__PURE__ */ new Set(["menuitemcheckbox", "menuitemradio", "checkbox", "switch"]);
|
|
2970
2937
|
function resolveLocator(page, resolved) {
|
|
2971
|
-
|
|
2938
|
+
if (resolved.ref !== void 0 && resolved.ref !== "") return refLocator(page, resolved.ref);
|
|
2939
|
+
const sel = resolved.selector ?? "";
|
|
2940
|
+
return page.locator(sel);
|
|
2972
2941
|
}
|
|
2973
2942
|
async function clickViaPlaywright(opts) {
|
|
2974
2943
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
2975
2944
|
const page = await getRestoredPageForTarget(opts);
|
|
2976
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2945
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
2977
2946
|
const locator = resolveLocator(page, resolved);
|
|
2978
2947
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
2979
2948
|
let checkableRole = false;
|
|
2980
|
-
if (resolved.ref) {
|
|
2949
|
+
if (resolved.ref !== void 0 && resolved.ref !== "") {
|
|
2981
2950
|
const refId = parseRoleRef(resolved.ref);
|
|
2982
|
-
if (refId) {
|
|
2951
|
+
if (refId !== null) {
|
|
2983
2952
|
const state = ensurePageState(page);
|
|
2984
2953
|
const info = state.roleRefs?.[refId];
|
|
2985
2954
|
if (info && CHECKABLE_ROLES.has(info.role)) checkableRole = true;
|
|
@@ -2992,15 +2961,15 @@ async function clickViaPlaywright(opts) {
|
|
|
2992
2961
|
await new Promise((resolve2) => setTimeout(resolve2, delayMs));
|
|
2993
2962
|
}
|
|
2994
2963
|
let ariaCheckedBefore;
|
|
2995
|
-
if (checkableRole &&
|
|
2964
|
+
if (checkableRole && opts.doubleClick !== true) {
|
|
2996
2965
|
ariaCheckedBefore = await locator.getAttribute("aria-checked", { timeout }).catch(() => void 0);
|
|
2997
2966
|
}
|
|
2998
|
-
if (opts.doubleClick) {
|
|
2967
|
+
if (opts.doubleClick === true) {
|
|
2999
2968
|
await locator.dblclick({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3000
2969
|
} else {
|
|
3001
2970
|
await locator.click({ timeout, button: opts.button, modifiers: opts.modifiers });
|
|
3002
2971
|
}
|
|
3003
|
-
if (checkableRole &&
|
|
2972
|
+
if (checkableRole && opts.doubleClick !== true && ariaCheckedBefore !== void 0) {
|
|
3004
2973
|
const POLL_INTERVAL_MS = 50;
|
|
3005
2974
|
const POLL_TIMEOUT_MS = 500;
|
|
3006
2975
|
let changed = false;
|
|
@@ -3013,7 +2982,9 @@ async function clickViaPlaywright(opts) {
|
|
|
3013
2982
|
await new Promise((resolve2) => setTimeout(resolve2, POLL_INTERVAL_MS));
|
|
3014
2983
|
}
|
|
3015
2984
|
if (!changed) {
|
|
3016
|
-
await locator.evaluate((el) =>
|
|
2985
|
+
await locator.evaluate((el) => {
|
|
2986
|
+
el.click();
|
|
2987
|
+
}).catch(() => {
|
|
3017
2988
|
});
|
|
3018
2989
|
}
|
|
3019
2990
|
}
|
|
@@ -3024,7 +2995,7 @@ async function clickViaPlaywright(opts) {
|
|
|
3024
2995
|
async function hoverViaPlaywright(opts) {
|
|
3025
2996
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3026
2997
|
const page = await getRestoredPageForTarget(opts);
|
|
3027
|
-
const label = resolved.ref ?? resolved.selector;
|
|
2998
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3028
2999
|
const locator = resolveLocator(page, resolved);
|
|
3029
3000
|
try {
|
|
3030
3001
|
await locator.hover({ timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
@@ -3034,28 +3005,28 @@ async function hoverViaPlaywright(opts) {
|
|
|
3034
3005
|
}
|
|
3035
3006
|
async function typeViaPlaywright(opts) {
|
|
3036
3007
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3037
|
-
const text =
|
|
3008
|
+
const text = opts.text;
|
|
3038
3009
|
const page = await getRestoredPageForTarget(opts);
|
|
3039
|
-
const label = resolved.ref ?? resolved.selector;
|
|
3010
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3040
3011
|
const locator = resolveLocator(page, resolved);
|
|
3041
3012
|
const timeout = resolveInteractionTimeoutMs(opts.timeoutMs);
|
|
3042
3013
|
try {
|
|
3043
|
-
if (opts.slowly) {
|
|
3014
|
+
if (opts.slowly === true) {
|
|
3044
3015
|
await locator.click({ timeout });
|
|
3045
3016
|
await locator.pressSequentially(text, { timeout, delay: 75 });
|
|
3046
3017
|
} else {
|
|
3047
3018
|
await locator.fill(text, { timeout });
|
|
3048
3019
|
}
|
|
3049
|
-
if (opts.submit) await locator.press("Enter", { timeout });
|
|
3020
|
+
if (opts.submit === true) await locator.press("Enter", { timeout });
|
|
3050
3021
|
} catch (err) {
|
|
3051
3022
|
throw toAIFriendlyError(err, label);
|
|
3052
3023
|
}
|
|
3053
3024
|
}
|
|
3054
3025
|
async function selectOptionViaPlaywright(opts) {
|
|
3055
3026
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3056
|
-
if (
|
|
3027
|
+
if (opts.values.length === 0) throw new Error("values are required");
|
|
3057
3028
|
const page = await getRestoredPageForTarget(opts);
|
|
3058
|
-
const label = resolved.ref ?? resolved.selector;
|
|
3029
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3059
3030
|
const locator = resolveLocator(page, resolved);
|
|
3060
3031
|
try {
|
|
3061
3032
|
await locator.selectOption(opts.values, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
@@ -3069,8 +3040,8 @@ async function dragViaPlaywright(opts) {
|
|
|
3069
3040
|
const page = await getRestoredPageForTarget(opts);
|
|
3070
3041
|
const startLocator = resolveLocator(page, resolvedStart);
|
|
3071
3042
|
const endLocator = resolveLocator(page, resolvedEnd);
|
|
3072
|
-
const startLabel = resolvedStart.ref ?? resolvedStart.selector;
|
|
3073
|
-
const endLabel = resolvedEnd.ref ?? resolvedEnd.selector;
|
|
3043
|
+
const startLabel = resolvedStart.ref ?? resolvedStart.selector ?? "";
|
|
3044
|
+
const endLabel = resolvedEnd.ref ?? resolvedEnd.selector ?? "";
|
|
3074
3045
|
try {
|
|
3075
3046
|
await startLocator.dragTo(endLocator, { timeout: resolveInteractionTimeoutMs(opts.timeoutMs) });
|
|
3076
3047
|
} catch (err) {
|
|
@@ -3106,7 +3077,7 @@ async function fillFormViaPlaywright(opts) {
|
|
|
3106
3077
|
async function scrollIntoViewViaPlaywright(opts) {
|
|
3107
3078
|
const resolved = requireRefOrSelector(opts.ref, opts.selector);
|
|
3108
3079
|
const page = await getRestoredPageForTarget(opts);
|
|
3109
|
-
const label = resolved.ref ?? resolved.selector;
|
|
3080
|
+
const label = resolved.ref ?? resolved.selector ?? "";
|
|
3110
3081
|
const locator = resolveLocator(page, resolved);
|
|
3111
3082
|
try {
|
|
3112
3083
|
await locator.scrollIntoViewIfNeeded({ timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
|
|
@@ -3174,7 +3145,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3174
3145
|
const armId = state.armIdUpload;
|
|
3175
3146
|
page.waitForEvent("filechooser", { timeout }).then(async (fileChooser) => {
|
|
3176
3147
|
if (state.armIdUpload !== armId) return;
|
|
3177
|
-
if (
|
|
3148
|
+
if (opts.paths === void 0 || opts.paths.length === 0) {
|
|
3178
3149
|
try {
|
|
3179
3150
|
await page.keyboard.press("Escape");
|
|
3180
3151
|
} catch {
|
|
@@ -3195,7 +3166,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3195
3166
|
await fileChooser.setFiles(uploadPathsResult.paths);
|
|
3196
3167
|
try {
|
|
3197
3168
|
const input = typeof fileChooser.element === "function" ? await Promise.resolve(fileChooser.element()) : null;
|
|
3198
|
-
if (input) {
|
|
3169
|
+
if (input !== null) {
|
|
3199
3170
|
await input.evaluate((el) => {
|
|
3200
3171
|
el.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3201
3172
|
el.dispatchEvent(new Event("change", { bubbles: true }));
|
|
@@ -3209,7 +3180,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3209
3180
|
|
|
3210
3181
|
// src/actions/keyboard.ts
|
|
3211
3182
|
async function pressKeyViaPlaywright(opts) {
|
|
3212
|
-
const key =
|
|
3183
|
+
const key = opts.key.trim();
|
|
3213
3184
|
if (!key) throw new Error("key is required");
|
|
3214
3185
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3215
3186
|
ensurePageState(page);
|
|
@@ -3222,9 +3193,9 @@ function isRetryableNavigateError(err) {
|
|
|
3222
3193
|
return msg.includes("frame has been detached") || msg.includes("target page, context or browser has been closed");
|
|
3223
3194
|
}
|
|
3224
3195
|
async function navigateViaPlaywright(opts) {
|
|
3225
|
-
const url =
|
|
3196
|
+
const url = opts.url.trim();
|
|
3226
3197
|
if (!url) throw new Error("url is required");
|
|
3227
|
-
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3198
|
+
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3228
3199
|
await assertBrowserNavigationAllowed({ url, ...withBrowserNavigationPolicy(policy) });
|
|
3229
3200
|
const timeout = Math.max(1e3, Math.min(12e4, opts.timeoutMs ?? 2e4));
|
|
3230
3201
|
let page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
@@ -3243,23 +3214,27 @@ async function navigateViaPlaywright(opts) {
|
|
|
3243
3214
|
ensurePageState(page);
|
|
3244
3215
|
response = await navigate();
|
|
3245
3216
|
}
|
|
3246
|
-
await assertBrowserNavigationRedirectChainAllowed({
|
|
3217
|
+
await assertBrowserNavigationRedirectChainAllowed({
|
|
3218
|
+
request: response?.request(),
|
|
3219
|
+
...withBrowserNavigationPolicy(policy)
|
|
3220
|
+
});
|
|
3247
3221
|
const finalUrl = page.url();
|
|
3248
3222
|
await assertBrowserNavigationResultAllowed({ url: finalUrl, ...withBrowserNavigationPolicy(policy) });
|
|
3249
3223
|
return { url: finalUrl };
|
|
3250
3224
|
}
|
|
3251
3225
|
async function listPagesViaPlaywright(opts) {
|
|
3252
3226
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
3253
|
-
const pages =
|
|
3227
|
+
const pages = getAllPages(browser);
|
|
3254
3228
|
const results = [];
|
|
3255
3229
|
for (const page of pages) {
|
|
3256
3230
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3257
|
-
if (tid)
|
|
3258
|
-
|
|
3259
|
-
|
|
3260
|
-
|
|
3261
|
-
|
|
3262
|
-
|
|
3231
|
+
if (tid !== null && tid !== "")
|
|
3232
|
+
results.push({
|
|
3233
|
+
targetId: tid,
|
|
3234
|
+
title: await page.title().catch(() => ""),
|
|
3235
|
+
url: page.url(),
|
|
3236
|
+
type: "page"
|
|
3237
|
+
});
|
|
3263
3238
|
}
|
|
3264
3239
|
return results;
|
|
3265
3240
|
}
|
|
@@ -3270,7 +3245,7 @@ async function createPageViaPlaywright(opts) {
|
|
|
3270
3245
|
const page = await context.newPage();
|
|
3271
3246
|
ensurePageState(page);
|
|
3272
3247
|
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
3273
|
-
const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3248
|
+
const policy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
3274
3249
|
if (targetUrl !== "about:blank") {
|
|
3275
3250
|
const navigationPolicy = withBrowserNavigationPolicy(policy);
|
|
3276
3251
|
await assertBrowserNavigationAllowed({ url: targetUrl, ...navigationPolicy });
|
|
@@ -3281,7 +3256,7 @@ async function createPageViaPlaywright(opts) {
|
|
|
3281
3256
|
await assertBrowserNavigationResultAllowed({ url: page.url(), ...navigationPolicy });
|
|
3282
3257
|
}
|
|
3283
3258
|
const tid = await pageTargetId(page).catch(() => null);
|
|
3284
|
-
if (
|
|
3259
|
+
if (tid === null || tid === "") throw new Error("Failed to get targetId for new page");
|
|
3285
3260
|
return {
|
|
3286
3261
|
targetId: tid,
|
|
3287
3262
|
title: await page.title().catch(() => ""),
|
|
@@ -3331,7 +3306,7 @@ var MAX_WAIT_TIME_MS = 3e4;
|
|
|
3331
3306
|
function resolveBoundedDelayMs2(value, label, maxMs) {
|
|
3332
3307
|
const normalized = Math.floor(value ?? 0);
|
|
3333
3308
|
if (!Number.isFinite(normalized) || normalized < 0) throw new Error(`${label} must be >= 0`);
|
|
3334
|
-
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${maxMs}ms`);
|
|
3309
|
+
if (normalized > maxMs) throw new Error(`${label} exceeds maximum of ${String(maxMs)}ms`);
|
|
3335
3310
|
return normalized;
|
|
3336
3311
|
}
|
|
3337
3312
|
async function waitForViaPlaywright(opts) {
|
|
@@ -3341,164 +3316,26 @@ async function waitForViaPlaywright(opts) {
|
|
|
3341
3316
|
if (typeof opts.timeMs === "number" && Number.isFinite(opts.timeMs)) {
|
|
3342
3317
|
await page.waitForTimeout(resolveBoundedDelayMs2(opts.timeMs, "wait timeMs", MAX_WAIT_TIME_MS));
|
|
3343
3318
|
}
|
|
3344
|
-
if (opts.text) {
|
|
3345
|
-
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
3346
|
-
}
|
|
3347
|
-
if (opts.textGone) {
|
|
3348
|
-
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
3349
|
-
}
|
|
3350
|
-
if (opts.selector) {
|
|
3351
|
-
const selector = String(opts.selector).trim();
|
|
3352
|
-
if (selector) await page.locator(selector).first().waitFor({ state: "visible", timeout });
|
|
3353
|
-
}
|
|
3354
|
-
if (opts.url) {
|
|
3355
|
-
const url = String(opts.url).trim();
|
|
3356
|
-
if (url) await page.waitForURL(url, { timeout });
|
|
3357
|
-
}
|
|
3358
|
-
if (opts.loadState) {
|
|
3359
|
-
await page.waitForLoadState(opts.loadState, { timeout });
|
|
3360
|
-
}
|
|
3361
|
-
if (opts.fn) {
|
|
3362
|
-
const fn = String(opts.fn).trim();
|
|
3363
|
-
if (fn) await page.waitForFunction(fn, void 0, { timeout });
|
|
3364
|
-
}
|
|
3365
|
-
}
|
|
3366
|
-
|
|
3367
|
-
// src/actions/evaluate.ts
|
|
3368
|
-
async function evaluateInAllFramesViaPlaywright(opts) {
|
|
3369
|
-
const fnText = String(opts.fn ?? "").trim();
|
|
3370
|
-
if (!fnText) throw new Error("function is required");
|
|
3371
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3372
|
-
const frames = page.frames();
|
|
3373
|
-
const results = [];
|
|
3374
|
-
for (const frame of frames) {
|
|
3375
|
-
try {
|
|
3376
|
-
const result = await frame.evaluate(
|
|
3377
|
-
// eslint-disable-next-line no-eval
|
|
3378
|
-
(fnBody) => {
|
|
3379
|
-
"use strict";
|
|
3380
|
-
try {
|
|
3381
|
-
const candidate = (0, eval)("(" + fnBody + ")");
|
|
3382
|
-
return typeof candidate === "function" ? candidate() : candidate;
|
|
3383
|
-
} catch (err) {
|
|
3384
|
-
throw new Error("Invalid evaluate function: " + (err instanceof Error ? err.message : String(err)));
|
|
3385
|
-
}
|
|
3386
|
-
},
|
|
3387
|
-
fnText
|
|
3388
|
-
);
|
|
3389
|
-
results.push({
|
|
3390
|
-
frameUrl: frame.url(),
|
|
3391
|
-
frameName: frame.name(),
|
|
3392
|
-
result
|
|
3393
|
-
});
|
|
3394
|
-
} catch {
|
|
3395
|
-
}
|
|
3396
|
-
}
|
|
3397
|
-
return results;
|
|
3398
|
-
}
|
|
3399
|
-
async function awaitEvalWithAbort(evalPromise, abortPromise) {
|
|
3400
|
-
if (!abortPromise) return await evalPromise;
|
|
3401
|
-
try {
|
|
3402
|
-
return await Promise.race([evalPromise, abortPromise]);
|
|
3403
|
-
} catch (err) {
|
|
3404
|
-
evalPromise.catch(() => {
|
|
3405
|
-
});
|
|
3406
|
-
throw err;
|
|
3407
|
-
}
|
|
3408
|
-
}
|
|
3409
|
-
var BROWSER_EVALUATOR = new Function("args", `
|
|
3410
|
-
"use strict";
|
|
3411
|
-
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
3412
|
-
try {
|
|
3413
|
-
var candidate = eval("(" + fnBody + ")");
|
|
3414
|
-
var result = typeof candidate === "function" ? candidate() : candidate;
|
|
3415
|
-
if (result && typeof result.then === "function") {
|
|
3416
|
-
return Promise.race([
|
|
3417
|
-
result,
|
|
3418
|
-
new Promise(function(_, reject) {
|
|
3419
|
-
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
3420
|
-
})
|
|
3421
|
-
]);
|
|
3422
|
-
}
|
|
3423
|
-
return result;
|
|
3424
|
-
} catch (err) {
|
|
3425
|
-
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
3426
|
-
}
|
|
3427
|
-
`);
|
|
3428
|
-
var ELEMENT_EVALUATOR = new Function("el", "args", `
|
|
3429
|
-
"use strict";
|
|
3430
|
-
var fnBody = args.fnBody, timeoutMs = args.timeoutMs;
|
|
3431
|
-
try {
|
|
3432
|
-
var candidate = eval("(" + fnBody + ")");
|
|
3433
|
-
var result = typeof candidate === "function" ? candidate(el) : candidate;
|
|
3434
|
-
if (result && typeof result.then === "function") {
|
|
3435
|
-
return Promise.race([
|
|
3436
|
-
result,
|
|
3437
|
-
new Promise(function(_, reject) {
|
|
3438
|
-
setTimeout(function() { reject(new Error("evaluate timed out after " + timeoutMs + "ms")); }, timeoutMs);
|
|
3439
|
-
})
|
|
3440
|
-
]);
|
|
3441
|
-
}
|
|
3442
|
-
return result;
|
|
3443
|
-
} catch (err) {
|
|
3444
|
-
throw new Error("Invalid evaluate function: " + (err && err.message ? err.message : String(err)));
|
|
3445
|
-
}
|
|
3446
|
-
`);
|
|
3447
|
-
async function evaluateViaPlaywright(opts) {
|
|
3448
|
-
const fnText = String(opts.fn ?? "").trim();
|
|
3449
|
-
if (!fnText) throw new Error("function is required");
|
|
3450
|
-
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3451
|
-
ensurePageState(page);
|
|
3452
|
-
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3453
|
-
const outerTimeout = normalizeTimeoutMs(opts.timeoutMs, 2e4);
|
|
3454
|
-
let evaluateTimeout = Math.max(1e3, Math.min(12e4, outerTimeout - 500));
|
|
3455
|
-
evaluateTimeout = Math.min(evaluateTimeout, outerTimeout);
|
|
3456
|
-
const signal = opts.signal;
|
|
3457
|
-
let abortListener;
|
|
3458
|
-
let abortReject;
|
|
3459
|
-
let abortPromise;
|
|
3460
|
-
if (signal) {
|
|
3461
|
-
abortPromise = new Promise((_, reject) => {
|
|
3462
|
-
abortReject = reject;
|
|
3463
|
-
});
|
|
3464
|
-
abortPromise.catch(() => {
|
|
3465
|
-
});
|
|
3466
|
-
}
|
|
3467
|
-
if (signal) {
|
|
3468
|
-
const disconnect = () => {
|
|
3469
|
-
forceDisconnectPlaywrightForTarget({
|
|
3470
|
-
cdpUrl: opts.cdpUrl,
|
|
3471
|
-
targetId: opts.targetId}).catch(() => {
|
|
3472
|
-
});
|
|
3473
|
-
};
|
|
3474
|
-
if (signal.aborted) {
|
|
3475
|
-
disconnect();
|
|
3476
|
-
throw signal.reason ?? new Error("aborted");
|
|
3477
|
-
}
|
|
3478
|
-
abortListener = () => {
|
|
3479
|
-
disconnect();
|
|
3480
|
-
abortReject?.(signal.reason ?? new Error("aborted"));
|
|
3481
|
-
};
|
|
3482
|
-
signal.addEventListener("abort", abortListener, { once: true });
|
|
3483
|
-
if (signal.aborted) {
|
|
3484
|
-
abortListener();
|
|
3485
|
-
throw signal.reason ?? new Error("aborted");
|
|
3486
|
-
}
|
|
3319
|
+
if (opts.text !== void 0 && opts.text !== "") {
|
|
3320
|
+
await page.getByText(opts.text).first().waitFor({ state: "visible", timeout });
|
|
3487
3321
|
}
|
|
3488
|
-
|
|
3489
|
-
|
|
3490
|
-
|
|
3491
|
-
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
3498
|
-
|
|
3499
|
-
|
|
3500
|
-
|
|
3501
|
-
|
|
3322
|
+
if (opts.textGone !== void 0 && opts.textGone !== "") {
|
|
3323
|
+
await page.getByText(opts.textGone).first().waitFor({ state: "hidden", timeout });
|
|
3324
|
+
}
|
|
3325
|
+
if (opts.selector !== void 0 && opts.selector !== "") {
|
|
3326
|
+
const selector = opts.selector.trim();
|
|
3327
|
+
if (selector !== "") await page.locator(selector).first().waitFor({ state: "visible", timeout });
|
|
3328
|
+
}
|
|
3329
|
+
if (opts.url !== void 0 && opts.url !== "") {
|
|
3330
|
+
const url = opts.url.trim();
|
|
3331
|
+
if (url !== "") await page.waitForURL(url, { timeout });
|
|
3332
|
+
}
|
|
3333
|
+
if (opts.loadState !== void 0) {
|
|
3334
|
+
await page.waitForLoadState(opts.loadState, { timeout });
|
|
3335
|
+
}
|
|
3336
|
+
if (opts.fn !== void 0 && opts.fn !== "") {
|
|
3337
|
+
const fn = opts.fn.trim();
|
|
3338
|
+
if (fn !== "") await page.waitForFunction(fn, void 0, { timeout });
|
|
3502
3339
|
}
|
|
3503
3340
|
}
|
|
3504
3341
|
|
|
@@ -3506,7 +3343,7 @@ async function evaluateViaPlaywright(opts) {
|
|
|
3506
3343
|
var MAX_BATCH_DEPTH = 5;
|
|
3507
3344
|
var MAX_BATCH_ACTIONS = 100;
|
|
3508
3345
|
async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, depth = 0) {
|
|
3509
|
-
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${MAX_BATCH_DEPTH}`);
|
|
3346
|
+
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
|
|
3510
3347
|
const effectiveTargetId = action.targetId ?? targetId;
|
|
3511
3348
|
switch (action.kind) {
|
|
3512
3349
|
case "click":
|
|
@@ -3598,7 +3435,8 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
|
|
|
3598
3435
|
});
|
|
3599
3436
|
break;
|
|
3600
3437
|
case "wait":
|
|
3601
|
-
if (action.fn
|
|
3438
|
+
if (action.fn !== void 0 && action.fn !== "" && !evaluateEnabled)
|
|
3439
|
+
throw new Error("wait --fn is disabled by config (browser.evaluateEnabled=false)");
|
|
3602
3440
|
await waitForViaPlaywright({
|
|
3603
3441
|
cdpUrl,
|
|
3604
3442
|
targetId: effectiveTargetId,
|
|
@@ -3639,13 +3477,14 @@ async function executeSingleAction(action, cdpUrl, targetId, evaluateEnabled, de
|
|
|
3639
3477
|
});
|
|
3640
3478
|
break;
|
|
3641
3479
|
default:
|
|
3642
|
-
throw new Error(`Unsupported batch action kind: ${action.kind}`);
|
|
3480
|
+
throw new Error(`Unsupported batch action kind: ${String(action.kind)}`);
|
|
3643
3481
|
}
|
|
3644
3482
|
}
|
|
3645
3483
|
async function batchViaPlaywright(opts) {
|
|
3646
3484
|
const depth = opts.depth ?? 0;
|
|
3647
|
-
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${MAX_BATCH_DEPTH}`);
|
|
3648
|
-
if (opts.actions.length > MAX_BATCH_ACTIONS)
|
|
3485
|
+
if (depth > MAX_BATCH_DEPTH) throw new Error(`Batch nesting depth exceeds maximum of ${String(MAX_BATCH_DEPTH)}`);
|
|
3486
|
+
if (opts.actions.length > MAX_BATCH_ACTIONS)
|
|
3487
|
+
throw new Error(`Batch exceeds maximum of ${String(MAX_BATCH_ACTIONS)} actions`);
|
|
3649
3488
|
const results = [];
|
|
3650
3489
|
const evaluateEnabled = opts.evaluateEnabled !== false;
|
|
3651
3490
|
for (const action of opts.actions) {
|
|
@@ -3725,7 +3564,7 @@ async function downloadViaPlaywright(opts) {
|
|
|
3725
3564
|
const state = ensurePageState(page);
|
|
3726
3565
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3727
3566
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
3728
|
-
const outPath =
|
|
3567
|
+
const outPath = opts.path.trim();
|
|
3729
3568
|
if (!outPath) throw new Error("path is required");
|
|
3730
3569
|
state.armIdDownload = bumpDownloadArmId();
|
|
3731
3570
|
const armId = state.armIdDownload;
|
|
@@ -3733,378 +3572,909 @@ async function downloadViaPlaywright(opts) {
|
|
|
3733
3572
|
try {
|
|
3734
3573
|
const locator = refLocator(page, opts.ref);
|
|
3735
3574
|
try {
|
|
3736
|
-
await locator.click({ timeout });
|
|
3737
|
-
} catch (err) {
|
|
3738
|
-
throw toAIFriendlyError(err, opts.ref);
|
|
3575
|
+
await locator.click({ timeout });
|
|
3576
|
+
} catch (err) {
|
|
3577
|
+
throw toAIFriendlyError(err, opts.ref);
|
|
3578
|
+
}
|
|
3579
|
+
return await awaitDownloadPayload({ waiter, state, armId, outPath });
|
|
3580
|
+
} catch (err) {
|
|
3581
|
+
waiter.cancel();
|
|
3582
|
+
throw err;
|
|
3583
|
+
}
|
|
3584
|
+
}
|
|
3585
|
+
async function waitForDownloadViaPlaywright(opts) {
|
|
3586
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3587
|
+
const state = ensurePageState(page);
|
|
3588
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
3589
|
+
state.armIdDownload = bumpDownloadArmId();
|
|
3590
|
+
const armId = state.armIdDownload;
|
|
3591
|
+
const waiter = createPageDownloadWaiter(page, timeout);
|
|
3592
|
+
try {
|
|
3593
|
+
const download = await waiter.promise;
|
|
3594
|
+
if (state.armIdDownload !== armId) throw new Error("Download was superseded by another waiter");
|
|
3595
|
+
const savePath = opts.path ?? download.suggestedFilename();
|
|
3596
|
+
await assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
3597
|
+
return await saveDownloadPayload(download, savePath);
|
|
3598
|
+
} catch (err) {
|
|
3599
|
+
waiter.cancel();
|
|
3600
|
+
throw err;
|
|
3601
|
+
}
|
|
3602
|
+
}
|
|
3603
|
+
async function emulateMediaViaPlaywright(opts) {
|
|
3604
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3605
|
+
ensurePageState(page);
|
|
3606
|
+
await page.emulateMedia({ colorScheme: opts.colorScheme });
|
|
3607
|
+
}
|
|
3608
|
+
async function setDeviceViaPlaywright(opts) {
|
|
3609
|
+
const name = opts.name.trim();
|
|
3610
|
+
if (!name) throw new Error("device name is required");
|
|
3611
|
+
const device = devices[name];
|
|
3612
|
+
if (device === void 0) {
|
|
3613
|
+
throw new Error(`Unknown device "${name}".`);
|
|
3614
|
+
}
|
|
3615
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3616
|
+
ensurePageState(page);
|
|
3617
|
+
if (device.viewport !== null) {
|
|
3618
|
+
await page.setViewportSize({
|
|
3619
|
+
width: device.viewport.width,
|
|
3620
|
+
height: device.viewport.height
|
|
3621
|
+
});
|
|
3622
|
+
}
|
|
3623
|
+
await withPageScopedCdpClient({
|
|
3624
|
+
cdpUrl: opts.cdpUrl,
|
|
3625
|
+
page,
|
|
3626
|
+
targetId: opts.targetId,
|
|
3627
|
+
fn: async (send) => {
|
|
3628
|
+
const locale = device.locale;
|
|
3629
|
+
if (device.userAgent !== "" || locale !== void 0 && locale !== "") {
|
|
3630
|
+
await send("Emulation.setUserAgentOverride", {
|
|
3631
|
+
userAgent: device.userAgent,
|
|
3632
|
+
acceptLanguage: locale
|
|
3633
|
+
});
|
|
3634
|
+
}
|
|
3635
|
+
if (device.viewport !== null) {
|
|
3636
|
+
await send("Emulation.setDeviceMetricsOverride", {
|
|
3637
|
+
mobile: device.isMobile,
|
|
3638
|
+
width: device.viewport.width,
|
|
3639
|
+
height: device.viewport.height,
|
|
3640
|
+
deviceScaleFactor: device.deviceScaleFactor,
|
|
3641
|
+
screenWidth: device.viewport.width,
|
|
3642
|
+
screenHeight: device.viewport.height
|
|
3643
|
+
});
|
|
3644
|
+
}
|
|
3645
|
+
if (device.hasTouch) {
|
|
3646
|
+
await send("Emulation.setTouchEmulationEnabled", { enabled: true });
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
});
|
|
3650
|
+
}
|
|
3651
|
+
async function setExtraHTTPHeadersViaPlaywright(opts) {
|
|
3652
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3653
|
+
ensurePageState(page);
|
|
3654
|
+
await page.context().setExtraHTTPHeaders(opts.headers);
|
|
3655
|
+
}
|
|
3656
|
+
async function setGeolocationViaPlaywright(opts) {
|
|
3657
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3658
|
+
ensurePageState(page);
|
|
3659
|
+
const context = page.context();
|
|
3660
|
+
if (opts.clear === true) {
|
|
3661
|
+
await context.setGeolocation(null);
|
|
3662
|
+
await context.clearPermissions().catch(() => {
|
|
3663
|
+
});
|
|
3664
|
+
return;
|
|
3665
|
+
}
|
|
3666
|
+
if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
|
|
3667
|
+
throw new Error("latitude and longitude are required (or set clear=true)");
|
|
3668
|
+
}
|
|
3669
|
+
await context.setGeolocation({
|
|
3670
|
+
latitude: opts.latitude,
|
|
3671
|
+
longitude: opts.longitude,
|
|
3672
|
+
accuracy: typeof opts.accuracy === "number" ? opts.accuracy : void 0
|
|
3673
|
+
});
|
|
3674
|
+
const origin = (opts.origin !== void 0 && opts.origin !== "" ? opts.origin.trim() : "") || (() => {
|
|
3675
|
+
try {
|
|
3676
|
+
return new URL(page.url()).origin;
|
|
3677
|
+
} catch {
|
|
3678
|
+
return "";
|
|
3679
|
+
}
|
|
3680
|
+
})();
|
|
3681
|
+
if (origin !== "")
|
|
3682
|
+
await context.grantPermissions(["geolocation"], { origin }).catch(() => {
|
|
3683
|
+
});
|
|
3684
|
+
}
|
|
3685
|
+
async function setHttpCredentialsViaPlaywright(opts) {
|
|
3686
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3687
|
+
ensurePageState(page);
|
|
3688
|
+
if (opts.clear === true) {
|
|
3689
|
+
await page.context().setHTTPCredentials(null);
|
|
3690
|
+
return;
|
|
3691
|
+
}
|
|
3692
|
+
const username = opts.username ?? "";
|
|
3693
|
+
const password = opts.password ?? "";
|
|
3694
|
+
if (!username) throw new Error("username is required (or set clear=true)");
|
|
3695
|
+
await page.context().setHTTPCredentials({ username, password });
|
|
3696
|
+
}
|
|
3697
|
+
async function setLocaleViaPlaywright(opts) {
|
|
3698
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3699
|
+
ensurePageState(page);
|
|
3700
|
+
const locale = opts.locale.trim();
|
|
3701
|
+
if (!locale) throw new Error("locale is required");
|
|
3702
|
+
await withPageScopedCdpClient({
|
|
3703
|
+
cdpUrl: opts.cdpUrl,
|
|
3704
|
+
page,
|
|
3705
|
+
targetId: opts.targetId,
|
|
3706
|
+
fn: async (send) => {
|
|
3707
|
+
try {
|
|
3708
|
+
await send("Emulation.setLocaleOverride", { locale });
|
|
3709
|
+
} catch (err) {
|
|
3710
|
+
if (String(err).includes("Another locale override is already in effect")) return;
|
|
3711
|
+
throw err;
|
|
3712
|
+
}
|
|
3713
|
+
}
|
|
3714
|
+
});
|
|
3715
|
+
}
|
|
3716
|
+
async function setOfflineViaPlaywright(opts) {
|
|
3717
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3718
|
+
ensurePageState(page);
|
|
3719
|
+
await page.context().setOffline(opts.offline);
|
|
3720
|
+
}
|
|
3721
|
+
async function setTimezoneViaPlaywright(opts) {
|
|
3722
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3723
|
+
ensurePageState(page);
|
|
3724
|
+
const timezoneId = opts.timezoneId.trim();
|
|
3725
|
+
if (!timezoneId) throw new Error("timezoneId is required");
|
|
3726
|
+
await withPageScopedCdpClient({
|
|
3727
|
+
cdpUrl: opts.cdpUrl,
|
|
3728
|
+
page,
|
|
3729
|
+
targetId: opts.targetId,
|
|
3730
|
+
fn: async (send) => {
|
|
3731
|
+
try {
|
|
3732
|
+
await send("Emulation.setTimezoneOverride", { timezoneId });
|
|
3733
|
+
} catch (err) {
|
|
3734
|
+
const msg = String(err);
|
|
3735
|
+
if (msg.includes("Timezone override is already in effect")) return;
|
|
3736
|
+
if (msg.includes("Invalid timezone")) throw new Error(`Invalid timezone ID: ${timezoneId}`, { cause: err });
|
|
3737
|
+
throw err;
|
|
3738
|
+
}
|
|
3739
|
+
}
|
|
3740
|
+
});
|
|
3741
|
+
}
|
|
3742
|
+
|
|
3743
|
+
// src/anti-bot.ts
|
|
3744
|
+
var DETECT_CHALLENGE_SCRIPT = `(function() {
|
|
3745
|
+
var title = (document.title || '').toLowerCase();
|
|
3746
|
+
|
|
3747
|
+
// Cloudflare JS challenge
|
|
3748
|
+
if (title === 'just a moment...'
|
|
3749
|
+
|| document.querySelector('#challenge-running, #cf-please-wait, #challenge-form')
|
|
3750
|
+
|| title.indexOf('checking your browser') !== -1) {
|
|
3751
|
+
return { kind: 'cloudflare-js', message: 'Cloudflare JS challenge' };
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
// Cloudflare block page (needs body text \u2014 read lazily)
|
|
3755
|
+
var body = null;
|
|
3756
|
+
function getBody() { if (body === null) body = (document.body && document.body.textContent) || ''; return body; }
|
|
3757
|
+
|
|
3758
|
+
if (title.indexOf('attention required') !== -1
|
|
3759
|
+
|| (document.querySelector('.cf-error-details') && getBody().indexOf('blocked') !== -1)) {
|
|
3760
|
+
return { kind: 'cloudflare-block', message: 'Cloudflare block page' };
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
// Cloudflare Turnstile
|
|
3764
|
+
if (document.querySelector('.cf-turnstile, iframe[src*="challenges.cloudflare.com"]')) {
|
|
3765
|
+
return { kind: 'cloudflare-turnstile', message: 'Cloudflare Turnstile challenge' };
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
// hCaptcha
|
|
3769
|
+
if (document.querySelector('.h-captcha, iframe[src*="hcaptcha.com"]')) {
|
|
3770
|
+
return { kind: 'hcaptcha', message: 'hCaptcha challenge' };
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
// reCAPTCHA
|
|
3774
|
+
if (document.querySelector('.g-recaptcha, iframe[src*="google.com/recaptcha"]')) {
|
|
3775
|
+
return { kind: 'recaptcha', message: 'reCAPTCHA challenge' };
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
// Generic access-denied / rate-limit pages (only read body for short pages)
|
|
3779
|
+
var b = getBody();
|
|
3780
|
+
if (b.length < 5000) {
|
|
3781
|
+
if (/access denied|403 forbidden/i.test(title) || /access denied/i.test(b)) {
|
|
3782
|
+
return { kind: 'blocked', message: 'Access denied' };
|
|
3783
|
+
}
|
|
3784
|
+
if (/\\b429\\b/i.test(title) || /too many requests|rate limit/i.test(b)) {
|
|
3785
|
+
return { kind: 'rate-limited', message: 'Rate limited' };
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
return null;
|
|
3790
|
+
})()`;
|
|
3791
|
+
function parseChallengeResult(raw) {
|
|
3792
|
+
if (raw !== null && typeof raw === "object" && "kind" in raw) {
|
|
3793
|
+
return raw;
|
|
3794
|
+
}
|
|
3795
|
+
return null;
|
|
3796
|
+
}
|
|
3797
|
+
async function detectChallengeViaPlaywright(opts) {
|
|
3798
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3799
|
+
ensurePageState(page);
|
|
3800
|
+
return parseChallengeResult(await page.evaluate(DETECT_CHALLENGE_SCRIPT));
|
|
3801
|
+
}
|
|
3802
|
+
async function waitForChallengeViaPlaywright(opts) {
|
|
3803
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3804
|
+
ensurePageState(page);
|
|
3805
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 15e3);
|
|
3806
|
+
const poll = Math.max(250, Math.min(5e3, opts.pollMs ?? 500));
|
|
3807
|
+
const detect = async () => parseChallengeResult(await page.evaluate(DETECT_CHALLENGE_SCRIPT));
|
|
3808
|
+
const initial = await detect();
|
|
3809
|
+
if (initial === null) return { resolved: true, challenge: null };
|
|
3810
|
+
if (initial.kind === "cloudflare-js") {
|
|
3811
|
+
try {
|
|
3812
|
+
await page.waitForFunction(
|
|
3813
|
+
"document.title.toLowerCase() !== 'just a moment...' && !document.querySelector('#challenge-running')",
|
|
3814
|
+
void 0,
|
|
3815
|
+
{ timeout }
|
|
3816
|
+
);
|
|
3817
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => {
|
|
3818
|
+
});
|
|
3819
|
+
const after = await detect();
|
|
3820
|
+
return { resolved: after === null, challenge: after };
|
|
3821
|
+
} catch {
|
|
3822
|
+
const after = await detect();
|
|
3823
|
+
return { resolved: after === null, challenge: after };
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
const deadline = Date.now() + timeout;
|
|
3827
|
+
while (Date.now() < deadline) {
|
|
3828
|
+
await page.waitForTimeout(poll);
|
|
3829
|
+
const current = await detect();
|
|
3830
|
+
if (current === null) return { resolved: true, challenge: null };
|
|
3831
|
+
}
|
|
3832
|
+
const final = await detect();
|
|
3833
|
+
return { resolved: final === null, challenge: final };
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3836
|
+
// src/capture/activity.ts
|
|
3837
|
+
function consolePriority(level) {
|
|
3838
|
+
switch (level) {
|
|
3839
|
+
case "error":
|
|
3840
|
+
return 3;
|
|
3841
|
+
case "warning":
|
|
3842
|
+
case "warn":
|
|
3843
|
+
return 2;
|
|
3844
|
+
case "info":
|
|
3845
|
+
case "log":
|
|
3846
|
+
return 1;
|
|
3847
|
+
case "debug":
|
|
3848
|
+
return 0;
|
|
3849
|
+
default:
|
|
3850
|
+
return 1;
|
|
3851
|
+
}
|
|
3852
|
+
}
|
|
3853
|
+
async function getConsoleMessagesViaPlaywright(opts) {
|
|
3854
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3855
|
+
const messages = opts.level !== void 0 && opts.level !== "" ? state.console.filter((msg) => consolePriority(msg.type) >= consolePriority(opts.level ?? "")) : [...state.console];
|
|
3856
|
+
if (opts.clear === true) state.console = [];
|
|
3857
|
+
return messages;
|
|
3858
|
+
}
|
|
3859
|
+
async function getPageErrorsViaPlaywright(opts) {
|
|
3860
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3861
|
+
const errors = [...state.errors];
|
|
3862
|
+
if (opts.clear === true) state.errors = [];
|
|
3863
|
+
return { errors };
|
|
3864
|
+
}
|
|
3865
|
+
async function getNetworkRequestsViaPlaywright(opts) {
|
|
3866
|
+
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
3867
|
+
const raw = [...state.requests];
|
|
3868
|
+
const filter = typeof opts.filter === "string" ? opts.filter.trim() : "";
|
|
3869
|
+
const requests = filter ? raw.filter((r) => r.url.includes(filter)) : raw;
|
|
3870
|
+
if (opts.clear === true) {
|
|
3871
|
+
state.requests = [];
|
|
3872
|
+
state.requestIds = /* @__PURE__ */ new WeakMap();
|
|
3873
|
+
}
|
|
3874
|
+
return { requests };
|
|
3875
|
+
}
|
|
3876
|
+
|
|
3877
|
+
// src/capture/pdf.ts
|
|
3878
|
+
async function pdfViaPlaywright(opts) {
|
|
3879
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3880
|
+
ensurePageState(page);
|
|
3881
|
+
return { buffer: await page.pdf({ printBackground: true }) };
|
|
3882
|
+
}
|
|
3883
|
+
|
|
3884
|
+
// src/capture/response.ts
|
|
3885
|
+
function matchUrlPattern(pattern, url) {
|
|
3886
|
+
if (!pattern || !url) return false;
|
|
3887
|
+
if (pattern === url) return true;
|
|
3888
|
+
if (pattern.includes("*")) {
|
|
3889
|
+
const escaped = pattern.replace(/[.+^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*");
|
|
3890
|
+
try {
|
|
3891
|
+
return new RegExp(`^${escaped}$`).test(url);
|
|
3892
|
+
} catch {
|
|
3893
|
+
return false;
|
|
3739
3894
|
}
|
|
3740
|
-
return await awaitDownloadPayload({ waiter, state, armId, outPath });
|
|
3741
|
-
} catch (err) {
|
|
3742
|
-
waiter.cancel();
|
|
3743
|
-
throw err;
|
|
3744
3895
|
}
|
|
3896
|
+
return url.includes(pattern);
|
|
3745
3897
|
}
|
|
3746
|
-
async function
|
|
3898
|
+
async function responseBodyViaPlaywright(opts) {
|
|
3747
3899
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3748
|
-
|
|
3749
|
-
const timeout = normalizeTimeoutMs(opts.timeoutMs, 12e4);
|
|
3750
|
-
|
|
3751
|
-
|
|
3752
|
-
const
|
|
3753
|
-
|
|
3754
|
-
|
|
3755
|
-
|
|
3756
|
-
|
|
3757
|
-
|
|
3758
|
-
|
|
3759
|
-
}
|
|
3760
|
-
|
|
3761
|
-
|
|
3900
|
+
ensurePageState(page);
|
|
3901
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
3902
|
+
const pattern = opts.url.trim();
|
|
3903
|
+
if (!pattern) throw new Error("url is required");
|
|
3904
|
+
const response = await page.waitForResponse((resp) => matchUrlPattern(pattern, resp.url()), { timeout });
|
|
3905
|
+
let body = await response.text();
|
|
3906
|
+
let truncated = false;
|
|
3907
|
+
const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
|
|
3908
|
+
if (body.length > maxChars) {
|
|
3909
|
+
body = body.slice(0, maxChars);
|
|
3910
|
+
truncated = true;
|
|
3911
|
+
}
|
|
3912
|
+
const headers = {};
|
|
3913
|
+
const allHeaders = response.headers();
|
|
3914
|
+
for (const [key, value] of Object.entries(allHeaders)) {
|
|
3915
|
+
headers[key] = value;
|
|
3762
3916
|
}
|
|
3917
|
+
return {
|
|
3918
|
+
url: response.url(),
|
|
3919
|
+
status: response.status(),
|
|
3920
|
+
headers,
|
|
3921
|
+
body,
|
|
3922
|
+
truncated
|
|
3923
|
+
};
|
|
3763
3924
|
}
|
|
3764
|
-
|
|
3925
|
+
|
|
3926
|
+
// src/capture/screenshot.ts
|
|
3927
|
+
async function takeScreenshotViaPlaywright(opts) {
|
|
3765
3928
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3766
3929
|
ensurePageState(page);
|
|
3767
|
-
|
|
3768
|
-
|
|
3769
|
-
|
|
3770
|
-
|
|
3771
|
-
|
|
3772
|
-
const device = devices[name];
|
|
3773
|
-
if (!device) {
|
|
3774
|
-
throw new Error(`Unknown device "${name}".`);
|
|
3930
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3931
|
+
const type = opts.type ?? "png";
|
|
3932
|
+
if (opts.ref !== void 0 && opts.ref !== "") {
|
|
3933
|
+
if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
|
|
3934
|
+
return { buffer: await refLocator(page, opts.ref).screenshot({ type }) };
|
|
3775
3935
|
}
|
|
3936
|
+
if (opts.element !== void 0 && opts.element !== "") {
|
|
3937
|
+
if (opts.fullPage === true) throw new Error("fullPage is not supported for element screenshots");
|
|
3938
|
+
return { buffer: await page.locator(opts.element).first().screenshot({ type }) };
|
|
3939
|
+
}
|
|
3940
|
+
return { buffer: await page.screenshot({ type, fullPage: Boolean(opts.fullPage) }) };
|
|
3941
|
+
}
|
|
3942
|
+
async function screenshotWithLabelsViaPlaywright(opts) {
|
|
3776
3943
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3777
3944
|
ensurePageState(page);
|
|
3778
|
-
|
|
3779
|
-
|
|
3780
|
-
|
|
3781
|
-
|
|
3782
|
-
|
|
3783
|
-
|
|
3784
|
-
|
|
3785
|
-
|
|
3786
|
-
|
|
3787
|
-
|
|
3788
|
-
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
|
|
3793
|
-
|
|
3794
|
-
|
|
3795
|
-
|
|
3796
|
-
if (device.viewport) {
|
|
3797
|
-
await send("Emulation.setDeviceMetricsOverride", {
|
|
3798
|
-
mobile: Boolean(device.isMobile),
|
|
3799
|
-
width: device.viewport.width,
|
|
3800
|
-
height: device.viewport.height,
|
|
3801
|
-
deviceScaleFactor: device.deviceScaleFactor ?? 1,
|
|
3802
|
-
screenWidth: device.viewport.width,
|
|
3803
|
-
screenHeight: device.viewport.height
|
|
3804
|
-
});
|
|
3945
|
+
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
3946
|
+
const maxLabels = typeof opts.maxLabels === "number" && Number.isFinite(opts.maxLabels) ? Math.max(1, Math.floor(opts.maxLabels)) : 150;
|
|
3947
|
+
const type = opts.type ?? "png";
|
|
3948
|
+
const refs = opts.refs.slice(0, maxLabels);
|
|
3949
|
+
const skipped = opts.refs.slice(maxLabels);
|
|
3950
|
+
const viewport = await page.evaluate(() => ({
|
|
3951
|
+
width: window.innerWidth || 0,
|
|
3952
|
+
height: window.innerHeight || 0
|
|
3953
|
+
}));
|
|
3954
|
+
const labels = [];
|
|
3955
|
+
for (let i = 0; i < refs.length; i++) {
|
|
3956
|
+
const ref = refs[i];
|
|
3957
|
+
try {
|
|
3958
|
+
const locator = refLocator(page, ref);
|
|
3959
|
+
const box = await locator.boundingBox({ timeout: 2e3 });
|
|
3960
|
+
if (!box) {
|
|
3961
|
+
skipped.push(ref);
|
|
3962
|
+
continue;
|
|
3805
3963
|
}
|
|
3806
|
-
|
|
3807
|
-
|
|
3964
|
+
const x1 = box.x + box.width;
|
|
3965
|
+
const y1 = box.y + box.height;
|
|
3966
|
+
if (x1 < 0 || box.x > viewport.width || y1 < 0 || box.y > viewport.height) {
|
|
3967
|
+
skipped.push(ref);
|
|
3968
|
+
continue;
|
|
3808
3969
|
}
|
|
3970
|
+
labels.push({ ref, index: i + 1, box });
|
|
3971
|
+
} catch {
|
|
3972
|
+
skipped.push(ref);
|
|
3809
3973
|
}
|
|
3810
|
-
}
|
|
3974
|
+
}
|
|
3975
|
+
try {
|
|
3976
|
+
if (labels.length > 0) {
|
|
3977
|
+
await page.evaluate(
|
|
3978
|
+
(labelData) => {
|
|
3979
|
+
document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
|
|
3980
|
+
el.remove();
|
|
3981
|
+
});
|
|
3982
|
+
const container = document.createElement("div");
|
|
3983
|
+
container.setAttribute("data-browserclaw-labels", "1");
|
|
3984
|
+
container.style.cssText = "position:fixed;top:0;left:0;width:100%;height:100%;pointer-events:none;z-index:2147483647;";
|
|
3985
|
+
for (const { index, box } of labelData) {
|
|
3986
|
+
const border = document.createElement("div");
|
|
3987
|
+
border.style.cssText = `position:absolute;left:${String(box.x)}px;top:${String(box.y)}px;width:${String(box.width)}px;height:${String(box.height)}px;border:2px solid #FF4500;box-sizing:border-box;`;
|
|
3988
|
+
container.appendChild(border);
|
|
3989
|
+
const badge = document.createElement("div");
|
|
3990
|
+
badge.textContent = String(index);
|
|
3991
|
+
badge.style.cssText = `position:absolute;left:${String(box.x)}px;top:${String(Math.max(0, box.y - 18))}px;background:#FF4500;color:#fff;font:bold 12px/16px monospace;padding:0 4px;border-radius:2px;`;
|
|
3992
|
+
container.appendChild(badge);
|
|
3993
|
+
}
|
|
3994
|
+
document.documentElement.appendChild(container);
|
|
3995
|
+
},
|
|
3996
|
+
labels.map((l) => ({ index: l.index, box: l.box }))
|
|
3997
|
+
);
|
|
3998
|
+
}
|
|
3999
|
+
return {
|
|
4000
|
+
buffer: await page.screenshot({ type }),
|
|
4001
|
+
labels,
|
|
4002
|
+
skipped
|
|
4003
|
+
};
|
|
4004
|
+
} finally {
|
|
4005
|
+
await page.evaluate(() => {
|
|
4006
|
+
document.querySelectorAll("[data-browserclaw-labels]").forEach((el) => {
|
|
4007
|
+
el.remove();
|
|
4008
|
+
});
|
|
4009
|
+
}).catch(() => {
|
|
4010
|
+
});
|
|
4011
|
+
}
|
|
3811
4012
|
}
|
|
3812
|
-
async function
|
|
4013
|
+
async function traceStartViaPlaywright(opts) {
|
|
3813
4014
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3814
4015
|
ensurePageState(page);
|
|
3815
|
-
|
|
4016
|
+
const context = page.context();
|
|
4017
|
+
const ctxState = ensureContextState(context);
|
|
4018
|
+
if (ctxState.traceActive) {
|
|
4019
|
+
throw new Error("Trace already running. Stop the current trace before starting a new one.");
|
|
4020
|
+
}
|
|
4021
|
+
await context.tracing.start({
|
|
4022
|
+
screenshots: opts.screenshots ?? true,
|
|
4023
|
+
snapshots: opts.snapshots ?? true,
|
|
4024
|
+
sources: opts.sources ?? false
|
|
4025
|
+
});
|
|
4026
|
+
ctxState.traceActive = true;
|
|
3816
4027
|
}
|
|
3817
|
-
async function
|
|
4028
|
+
async function traceStopViaPlaywright(opts) {
|
|
4029
|
+
await assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
3818
4030
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3819
4031
|
ensurePageState(page);
|
|
3820
4032
|
const context = page.context();
|
|
3821
|
-
|
|
3822
|
-
|
|
3823
|
-
|
|
3824
|
-
});
|
|
3825
|
-
return;
|
|
3826
|
-
}
|
|
3827
|
-
if (typeof opts.latitude !== "number" || typeof opts.longitude !== "number") {
|
|
3828
|
-
throw new Error("latitude and longitude are required (or set clear=true)");
|
|
4033
|
+
const ctxState = ensureContextState(context);
|
|
4034
|
+
if (!ctxState.traceActive) {
|
|
4035
|
+
throw new Error("No active trace. Start a trace before stopping it.");
|
|
3829
4036
|
}
|
|
3830
|
-
await
|
|
3831
|
-
|
|
3832
|
-
|
|
3833
|
-
|
|
3834
|
-
|
|
3835
|
-
const origin = opts.origin?.trim() || (() => {
|
|
3836
|
-
try {
|
|
3837
|
-
return new URL(page.url()).origin;
|
|
3838
|
-
} catch {
|
|
3839
|
-
return "";
|
|
4037
|
+
await writeViaSiblingTempPath({
|
|
4038
|
+
rootDir: dirname(opts.path),
|
|
4039
|
+
targetPath: opts.path,
|
|
4040
|
+
writeTemp: async (tempPath) => {
|
|
4041
|
+
await context.tracing.stop({ path: tempPath });
|
|
3840
4042
|
}
|
|
3841
|
-
})();
|
|
3842
|
-
if (origin) await context.grantPermissions(["geolocation"], { origin }).catch(() => {
|
|
3843
4043
|
});
|
|
4044
|
+
ctxState.traceActive = false;
|
|
3844
4045
|
}
|
|
3845
|
-
|
|
3846
|
-
|
|
3847
|
-
|
|
3848
|
-
|
|
3849
|
-
|
|
3850
|
-
|
|
4046
|
+
|
|
4047
|
+
// src/snapshot/ref-map.ts
|
|
4048
|
+
var INTERACTIVE_ROLES = /* @__PURE__ */ new Set([
|
|
4049
|
+
"button",
|
|
4050
|
+
"link",
|
|
4051
|
+
"textbox",
|
|
4052
|
+
"checkbox",
|
|
4053
|
+
"radio",
|
|
4054
|
+
"combobox",
|
|
4055
|
+
"listbox",
|
|
4056
|
+
"menuitem",
|
|
4057
|
+
"menuitemcheckbox",
|
|
4058
|
+
"menuitemradio",
|
|
4059
|
+
"option",
|
|
4060
|
+
"searchbox",
|
|
4061
|
+
"slider",
|
|
4062
|
+
"spinbutton",
|
|
4063
|
+
"switch",
|
|
4064
|
+
"tab",
|
|
4065
|
+
"treeitem"
|
|
4066
|
+
]);
|
|
4067
|
+
var CONTENT_ROLES = /* @__PURE__ */ new Set([
|
|
4068
|
+
"heading",
|
|
4069
|
+
"cell",
|
|
4070
|
+
"gridcell",
|
|
4071
|
+
"columnheader",
|
|
4072
|
+
"rowheader",
|
|
4073
|
+
"listitem",
|
|
4074
|
+
"article",
|
|
4075
|
+
"region",
|
|
4076
|
+
"main",
|
|
4077
|
+
"navigation"
|
|
4078
|
+
]);
|
|
4079
|
+
var STRUCTURAL_ROLES = /* @__PURE__ */ new Set([
|
|
4080
|
+
"generic",
|
|
4081
|
+
"group",
|
|
4082
|
+
"list",
|
|
4083
|
+
"table",
|
|
4084
|
+
"row",
|
|
4085
|
+
"rowgroup",
|
|
4086
|
+
"grid",
|
|
4087
|
+
"treegrid",
|
|
4088
|
+
"menu",
|
|
4089
|
+
"menubar",
|
|
4090
|
+
"toolbar",
|
|
4091
|
+
"tablist",
|
|
4092
|
+
"tree",
|
|
4093
|
+
"directory",
|
|
4094
|
+
"document",
|
|
4095
|
+
"application",
|
|
4096
|
+
"presentation",
|
|
4097
|
+
"none"
|
|
4098
|
+
]);
|
|
4099
|
+
function getIndentLevel(line) {
|
|
4100
|
+
const match = /^(\s*)/.exec(line);
|
|
4101
|
+
return match ? Math.floor(match[1].length / 2) : 0;
|
|
4102
|
+
}
|
|
4103
|
+
function matchInteractiveSnapshotLine(line, options) {
|
|
4104
|
+
const depth = getIndentLevel(line);
|
|
4105
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) {
|
|
4106
|
+
return null;
|
|
3851
4107
|
}
|
|
3852
|
-
const
|
|
3853
|
-
|
|
3854
|
-
|
|
3855
|
-
|
|
4108
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
4109
|
+
if (!match) {
|
|
4110
|
+
return null;
|
|
4111
|
+
}
|
|
4112
|
+
const [, , roleRaw, name, suffix] = match;
|
|
4113
|
+
if (roleRaw.startsWith("/")) {
|
|
4114
|
+
return null;
|
|
4115
|
+
}
|
|
4116
|
+
const role = roleRaw.toLowerCase();
|
|
4117
|
+
return {
|
|
4118
|
+
roleRaw,
|
|
4119
|
+
role,
|
|
4120
|
+
...name ? { name } : {},
|
|
4121
|
+
suffix
|
|
4122
|
+
};
|
|
3856
4123
|
}
|
|
3857
|
-
|
|
3858
|
-
const
|
|
3859
|
-
|
|
3860
|
-
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
3868
|
-
|
|
3869
|
-
|
|
3870
|
-
|
|
3871
|
-
|
|
3872
|
-
|
|
4124
|
+
function createRoleNameTracker() {
|
|
4125
|
+
const counts = /* @__PURE__ */ new Map();
|
|
4126
|
+
const refsByKey = /* @__PURE__ */ new Map();
|
|
4127
|
+
return {
|
|
4128
|
+
counts,
|
|
4129
|
+
refsByKey,
|
|
4130
|
+
getKey(role, name) {
|
|
4131
|
+
return `${role}:${name ?? ""}`;
|
|
4132
|
+
},
|
|
4133
|
+
getNextIndex(role, name) {
|
|
4134
|
+
const key = this.getKey(role, name);
|
|
4135
|
+
const current = counts.get(key) ?? 0;
|
|
4136
|
+
counts.set(key, current + 1);
|
|
4137
|
+
return current;
|
|
4138
|
+
},
|
|
4139
|
+
trackRef(role, name, ref) {
|
|
4140
|
+
const key = this.getKey(role, name);
|
|
4141
|
+
const list = refsByKey.get(key) ?? [];
|
|
4142
|
+
list.push(ref);
|
|
4143
|
+
refsByKey.set(key, list);
|
|
4144
|
+
},
|
|
4145
|
+
getDuplicateKeys() {
|
|
4146
|
+
const out = /* @__PURE__ */ new Set();
|
|
4147
|
+
for (const [key, refs] of refsByKey) if (refs.length > 1) out.add(key);
|
|
4148
|
+
return out;
|
|
3873
4149
|
}
|
|
3874
|
-
}
|
|
4150
|
+
};
|
|
3875
4151
|
}
|
|
3876
|
-
|
|
3877
|
-
const
|
|
3878
|
-
|
|
3879
|
-
|
|
4152
|
+
function removeNthFromNonDuplicates(refs, tracker) {
|
|
4153
|
+
const duplicates = tracker.getDuplicateKeys();
|
|
4154
|
+
for (const [ref, data] of Object.entries(refs)) {
|
|
4155
|
+
const key = tracker.getKey(data.role, data.name);
|
|
4156
|
+
if (!duplicates.has(key)) delete refs[ref].nth;
|
|
4157
|
+
}
|
|
3880
4158
|
}
|
|
3881
|
-
|
|
3882
|
-
const
|
|
3883
|
-
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
|
|
3887
|
-
|
|
3888
|
-
|
|
3889
|
-
|
|
3890
|
-
|
|
3891
|
-
|
|
3892
|
-
|
|
3893
|
-
|
|
3894
|
-
|
|
3895
|
-
|
|
3896
|
-
|
|
3897
|
-
|
|
4159
|
+
function compactTree(tree) {
|
|
4160
|
+
const lines = tree.split("\n");
|
|
4161
|
+
const result = [];
|
|
4162
|
+
for (let i = 0; i < lines.length; i++) {
|
|
4163
|
+
const line = lines[i];
|
|
4164
|
+
if (line.includes("[ref=")) {
|
|
4165
|
+
result.push(line);
|
|
4166
|
+
continue;
|
|
4167
|
+
}
|
|
4168
|
+
if (line.includes(":") && !line.trimEnd().endsWith(":")) {
|
|
4169
|
+
result.push(line);
|
|
4170
|
+
continue;
|
|
4171
|
+
}
|
|
4172
|
+
const currentIndent = getIndentLevel(line);
|
|
4173
|
+
let hasRelevantChildren = false;
|
|
4174
|
+
for (let j = i + 1; j < lines.length; j++) {
|
|
4175
|
+
if (getIndentLevel(lines[j]) <= currentIndent) break;
|
|
4176
|
+
if (lines[j]?.includes("[ref=")) {
|
|
4177
|
+
hasRelevantChildren = true;
|
|
4178
|
+
break;
|
|
3898
4179
|
}
|
|
3899
4180
|
}
|
|
3900
|
-
|
|
4181
|
+
if (hasRelevantChildren) result.push(line);
|
|
4182
|
+
}
|
|
4183
|
+
return result.join("\n");
|
|
3901
4184
|
}
|
|
3902
|
-
|
|
3903
|
-
|
|
3904
|
-
|
|
3905
|
-
const
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
3910
|
-
|
|
3911
|
-
|
|
4185
|
+
function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
4186
|
+
const lines = ariaSnapshot.split("\n");
|
|
4187
|
+
const refs = {};
|
|
4188
|
+
const tracker = createRoleNameTracker();
|
|
4189
|
+
let counter = 0;
|
|
4190
|
+
const nextRef = () => {
|
|
4191
|
+
counter++;
|
|
4192
|
+
return `e${String(counter)}`;
|
|
4193
|
+
};
|
|
4194
|
+
if (options.interactive === true) {
|
|
4195
|
+
const result2 = [];
|
|
4196
|
+
for (const line of lines) {
|
|
4197
|
+
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
4198
|
+
if (!parsed) continue;
|
|
4199
|
+
const { roleRaw, role, name, suffix } = parsed;
|
|
4200
|
+
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
4201
|
+
const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
|
|
4202
|
+
const ref = nextRef();
|
|
4203
|
+
const nth = tracker.getNextIndex(role, name);
|
|
4204
|
+
tracker.trackRef(role, name, ref);
|
|
4205
|
+
refs[ref] = { role, name, nth };
|
|
4206
|
+
let enhanced = `${prefix}${roleRaw}`;
|
|
4207
|
+
if (name !== void 0 && name !== "") enhanced += ` "${name}"`;
|
|
4208
|
+
enhanced += ` [ref=${ref}]`;
|
|
4209
|
+
if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
|
|
4210
|
+
if (suffix.includes("[")) enhanced += suffix;
|
|
4211
|
+
result2.push(enhanced);
|
|
4212
|
+
}
|
|
4213
|
+
removeNthFromNonDuplicates(refs, tracker);
|
|
4214
|
+
return { snapshot: result2.join("\n") || "(no interactive elements)", refs };
|
|
3912
4215
|
}
|
|
3913
|
-
|
|
3914
|
-
|
|
3915
|
-
|
|
4216
|
+
const result = [];
|
|
4217
|
+
for (const line of lines) {
|
|
4218
|
+
const depth = getIndentLevel(line);
|
|
4219
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
4220
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
4221
|
+
if (!match) {
|
|
4222
|
+
result.push(line);
|
|
4223
|
+
continue;
|
|
4224
|
+
}
|
|
4225
|
+
const [, prefix, roleRaw, name, suffix] = match;
|
|
4226
|
+
if (roleRaw.startsWith("/")) {
|
|
4227
|
+
result.push(line);
|
|
4228
|
+
continue;
|
|
4229
|
+
}
|
|
4230
|
+
const role = roleRaw.toLowerCase();
|
|
4231
|
+
const isInteractive = INTERACTIVE_ROLES.has(role);
|
|
4232
|
+
const isContent = CONTENT_ROLES.has(role);
|
|
4233
|
+
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
4234
|
+
if (options.compact === true && isStructural && name === "") continue;
|
|
4235
|
+
if (!(isInteractive || isContent && name !== "")) {
|
|
4236
|
+
result.push(line);
|
|
4237
|
+
continue;
|
|
4238
|
+
}
|
|
4239
|
+
const ref = nextRef();
|
|
4240
|
+
const nth = tracker.getNextIndex(role, name);
|
|
4241
|
+
tracker.trackRef(role, name, ref);
|
|
4242
|
+
refs[ref] = { role, name, nth };
|
|
4243
|
+
let enhanced = `${prefix}${roleRaw}`;
|
|
4244
|
+
if (name !== "") enhanced += ` "${name}"`;
|
|
4245
|
+
enhanced += ` [ref=${ref}]`;
|
|
4246
|
+
if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
|
|
4247
|
+
if (suffix !== "") enhanced += suffix;
|
|
4248
|
+
result.push(enhanced);
|
|
3916
4249
|
}
|
|
3917
|
-
|
|
4250
|
+
removeNthFromNonDuplicates(refs, tracker);
|
|
4251
|
+
const tree = result.join("\n") || "(empty)";
|
|
4252
|
+
return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
|
|
3918
4253
|
}
|
|
3919
|
-
|
|
3920
|
-
const
|
|
3921
|
-
|
|
3922
|
-
|
|
3923
|
-
|
|
3924
|
-
|
|
3925
|
-
|
|
3926
|
-
|
|
3927
|
-
|
|
3928
|
-
|
|
3929
|
-
|
|
3930
|
-
|
|
3931
|
-
|
|
3932
|
-
|
|
3933
|
-
|
|
3934
|
-
|
|
3935
|
-
const
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
skipped.push(ref);
|
|
3939
|
-
continue;
|
|
3940
|
-
}
|
|
3941
|
-
const x1 = box.x + box.width;
|
|
3942
|
-
const y1 = box.y + box.height;
|
|
3943
|
-
if (x1 < 0 || box.x > viewport.width || y1 < 0 || box.y > viewport.height) {
|
|
3944
|
-
skipped.push(ref);
|
|
3945
|
-
continue;
|
|
3946
|
-
}
|
|
3947
|
-
labels.push({ ref, index: i + 1, box });
|
|
3948
|
-
} catch {
|
|
3949
|
-
skipped.push(ref);
|
|
4254
|
+
function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
4255
|
+
const lines = aiSnapshot.split("\n");
|
|
4256
|
+
const refs = {};
|
|
4257
|
+
function parseAiSnapshotRef(suffix) {
|
|
4258
|
+
const match = /\[ref=(e\d+)\]/i.exec(suffix);
|
|
4259
|
+
return match ? match[1] : null;
|
|
4260
|
+
}
|
|
4261
|
+
if (options.interactive === true) {
|
|
4262
|
+
const out2 = [];
|
|
4263
|
+
for (const line of lines) {
|
|
4264
|
+
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
4265
|
+
if (!parsed) continue;
|
|
4266
|
+
const { roleRaw, role, name, suffix } = parsed;
|
|
4267
|
+
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
4268
|
+
const ref = parseAiSnapshotRef(suffix);
|
|
4269
|
+
if (ref === null) continue;
|
|
4270
|
+
const prefix = /^(\s*-\s*)/.exec(line)?.[1] ?? "";
|
|
4271
|
+
refs[ref] = { role, ...name !== void 0 && name !== "" ? { name } : {} };
|
|
4272
|
+
out2.push(`${prefix}${roleRaw}${name !== void 0 && name !== "" ? ` "${name}"` : ""}${suffix}`);
|
|
3950
4273
|
}
|
|
4274
|
+
return { snapshot: out2.join("\n") || "(no interactive elements)", refs };
|
|
3951
4275
|
}
|
|
3952
|
-
|
|
3953
|
-
|
|
3954
|
-
|
|
3955
|
-
|
|
3956
|
-
|
|
3957
|
-
|
|
3958
|
-
|
|
3959
|
-
|
|
3960
|
-
const border = document.createElement("div");
|
|
3961
|
-
border.style.cssText = `position:absolute;left:${box.x}px;top:${box.y}px;width:${box.width}px;height:${box.height}px;border:2px solid #FF4500;box-sizing:border-box;`;
|
|
3962
|
-
container.appendChild(border);
|
|
3963
|
-
const badge = document.createElement("div");
|
|
3964
|
-
badge.textContent = String(index);
|
|
3965
|
-
badge.style.cssText = `position:absolute;left:${box.x}px;top:${Math.max(0, box.y - 18)}px;background:#FF4500;color:#fff;font:bold 12px/16px monospace;padding:0 4px;border-radius:2px;`;
|
|
3966
|
-
container.appendChild(badge);
|
|
3967
|
-
}
|
|
3968
|
-
document.documentElement.appendChild(container);
|
|
3969
|
-
}, labels.map((l) => ({ index: l.index, box: l.box })));
|
|
4276
|
+
const out = [];
|
|
4277
|
+
for (const line of lines) {
|
|
4278
|
+
const depth = getIndentLevel(line);
|
|
4279
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) continue;
|
|
4280
|
+
const match = /^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/.exec(line);
|
|
4281
|
+
if (!match) {
|
|
4282
|
+
out.push(line);
|
|
4283
|
+
continue;
|
|
3970
4284
|
}
|
|
3971
|
-
|
|
3972
|
-
|
|
3973
|
-
|
|
3974
|
-
|
|
3975
|
-
}
|
|
3976
|
-
|
|
3977
|
-
|
|
3978
|
-
|
|
3979
|
-
|
|
3980
|
-
}
|
|
4285
|
+
const [, , roleRaw, name, suffix] = match;
|
|
4286
|
+
if (roleRaw.startsWith("/")) {
|
|
4287
|
+
out.push(line);
|
|
4288
|
+
continue;
|
|
4289
|
+
}
|
|
4290
|
+
const role = roleRaw.toLowerCase();
|
|
4291
|
+
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
4292
|
+
if (options.compact === true && isStructural && name === "") continue;
|
|
4293
|
+
const ref = parseAiSnapshotRef(suffix);
|
|
4294
|
+
if (ref !== null) refs[ref] = { role, ...name !== "" ? { name } : {} };
|
|
4295
|
+
out.push(line);
|
|
3981
4296
|
}
|
|
4297
|
+
const tree = out.join("\n") || "(empty)";
|
|
4298
|
+
return { snapshot: options.compact === true ? compactTree(tree) : tree, refs };
|
|
3982
4299
|
}
|
|
3983
|
-
|
|
3984
|
-
|
|
3985
|
-
|
|
3986
|
-
|
|
3987
|
-
|
|
3988
|
-
|
|
4300
|
+
function getRoleSnapshotStats(snapshot, refs) {
|
|
4301
|
+
const interactive = Object.values(refs).filter((r) => INTERACTIVE_ROLES.has(r.role)).length;
|
|
4302
|
+
return {
|
|
4303
|
+
lines: snapshot.split("\n").length,
|
|
4304
|
+
chars: snapshot.length,
|
|
4305
|
+
refs: Object.keys(refs).length,
|
|
4306
|
+
interactive
|
|
4307
|
+
};
|
|
3989
4308
|
}
|
|
3990
|
-
|
|
4309
|
+
|
|
4310
|
+
// src/snapshot/ai-snapshot.ts
|
|
4311
|
+
async function snapshotAi(opts) {
|
|
3991
4312
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3992
4313
|
ensurePageState(page);
|
|
3993
|
-
const
|
|
3994
|
-
|
|
3995
|
-
|
|
3996
|
-
throw new Error("Trace already running. Stop the current trace before starting a new one.");
|
|
4314
|
+
const maybe = page;
|
|
4315
|
+
if (!maybe._snapshotForAI) {
|
|
4316
|
+
throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
|
|
3997
4317
|
}
|
|
3998
|
-
|
|
3999
|
-
|
|
4000
|
-
|
|
4001
|
-
|
|
4318
|
+
const sourceUrl = page.url();
|
|
4319
|
+
const result = await maybe._snapshotForAI({
|
|
4320
|
+
timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
|
|
4321
|
+
track: "response"
|
|
4002
4322
|
});
|
|
4003
|
-
|
|
4004
|
-
|
|
4005
|
-
|
|
4006
|
-
|
|
4007
|
-
|
|
4008
|
-
|
|
4009
|
-
|
|
4010
|
-
|
|
4011
|
-
|
|
4012
|
-
|
|
4323
|
+
let snapshot = String(result.full);
|
|
4324
|
+
const maxChars = opts.maxChars;
|
|
4325
|
+
const limit = typeof maxChars === "number" && Number.isFinite(maxChars) && maxChars > 0 ? Math.floor(maxChars) : void 0;
|
|
4326
|
+
let truncated = false;
|
|
4327
|
+
if (limit !== void 0 && snapshot.length > limit) {
|
|
4328
|
+
const lastNewline = snapshot.lastIndexOf("\n", limit);
|
|
4329
|
+
const cutoff = lastNewline > 0 ? lastNewline : limit;
|
|
4330
|
+
snapshot = `${snapshot.slice(0, cutoff)}
|
|
4331
|
+
|
|
4332
|
+
[...TRUNCATED - page too large]`;
|
|
4333
|
+
truncated = true;
|
|
4013
4334
|
}
|
|
4014
|
-
|
|
4015
|
-
|
|
4016
|
-
|
|
4017
|
-
|
|
4018
|
-
|
|
4019
|
-
|
|
4335
|
+
const built = buildRoleSnapshotFromAiSnapshot(snapshot, opts.options);
|
|
4336
|
+
storeRoleRefsForTarget({
|
|
4337
|
+
page,
|
|
4338
|
+
cdpUrl: opts.cdpUrl,
|
|
4339
|
+
targetId: opts.targetId,
|
|
4340
|
+
refs: built.refs,
|
|
4341
|
+
mode: "aria"
|
|
4020
4342
|
});
|
|
4021
|
-
|
|
4343
|
+
return {
|
|
4344
|
+
snapshot: built.snapshot,
|
|
4345
|
+
refs: built.refs,
|
|
4346
|
+
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
4347
|
+
...truncated ? { truncated } : {},
|
|
4348
|
+
untrusted: true,
|
|
4349
|
+
contentMeta: {
|
|
4350
|
+
sourceUrl,
|
|
4351
|
+
contentType: "browser-snapshot",
|
|
4352
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4353
|
+
}
|
|
4354
|
+
};
|
|
4022
4355
|
}
|
|
4023
4356
|
|
|
4024
|
-
// src/
|
|
4025
|
-
function
|
|
4026
|
-
|
|
4027
|
-
|
|
4028
|
-
|
|
4029
|
-
|
|
4030
|
-
|
|
4031
|
-
|
|
4032
|
-
}
|
|
4033
|
-
|
|
4357
|
+
// src/snapshot/aria-snapshot.ts
|
|
4358
|
+
async function snapshotRole(opts) {
|
|
4359
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4360
|
+
ensurePageState(page);
|
|
4361
|
+
const sourceUrl = page.url();
|
|
4362
|
+
if (opts.refsMode === "aria") {
|
|
4363
|
+
if (opts.selector !== void 0 && opts.selector.trim() !== "" || opts.frameSelector !== void 0 && opts.frameSelector.trim() !== "") {
|
|
4364
|
+
throw new Error("refs=aria does not support selector/frame snapshots yet.");
|
|
4365
|
+
}
|
|
4366
|
+
const maybe = page;
|
|
4367
|
+
if (!maybe._snapshotForAI) {
|
|
4368
|
+
throw new Error("refs=aria requires Playwright _snapshotForAI support.");
|
|
4034
4369
|
}
|
|
4370
|
+
const result = await maybe._snapshotForAI({ timeout: 5e3, track: "response" });
|
|
4371
|
+
const built2 = buildRoleSnapshotFromAiSnapshot(String(result.full), opts.options);
|
|
4372
|
+
storeRoleRefsForTarget({
|
|
4373
|
+
page,
|
|
4374
|
+
cdpUrl: opts.cdpUrl,
|
|
4375
|
+
targetId: opts.targetId,
|
|
4376
|
+
refs: built2.refs,
|
|
4377
|
+
mode: "aria"
|
|
4378
|
+
});
|
|
4379
|
+
return {
|
|
4380
|
+
snapshot: built2.snapshot,
|
|
4381
|
+
refs: built2.refs,
|
|
4382
|
+
stats: getRoleSnapshotStats(built2.snapshot, built2.refs),
|
|
4383
|
+
untrusted: true,
|
|
4384
|
+
contentMeta: {
|
|
4385
|
+
sourceUrl,
|
|
4386
|
+
contentType: "browser-snapshot",
|
|
4387
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4388
|
+
}
|
|
4389
|
+
};
|
|
4035
4390
|
}
|
|
4036
|
-
|
|
4391
|
+
const frameSelector = opts.frameSelector?.trim() ?? "";
|
|
4392
|
+
const selector = opts.selector?.trim() ?? "";
|
|
4393
|
+
const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
|
|
4394
|
+
const ariaSnapshot = await locator.ariaSnapshot({ timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3) });
|
|
4395
|
+
const built = buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, opts.options);
|
|
4396
|
+
storeRoleRefsForTarget({
|
|
4397
|
+
page,
|
|
4398
|
+
cdpUrl: opts.cdpUrl,
|
|
4399
|
+
targetId: opts.targetId,
|
|
4400
|
+
refs: built.refs,
|
|
4401
|
+
frameSelector: frameSelector !== "" ? frameSelector : void 0,
|
|
4402
|
+
mode: "role"
|
|
4403
|
+
});
|
|
4404
|
+
return {
|
|
4405
|
+
snapshot: built.snapshot,
|
|
4406
|
+
refs: built.refs,
|
|
4407
|
+
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
4408
|
+
untrusted: true,
|
|
4409
|
+
contentMeta: {
|
|
4410
|
+
sourceUrl,
|
|
4411
|
+
contentType: "browser-snapshot",
|
|
4412
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4413
|
+
}
|
|
4414
|
+
};
|
|
4037
4415
|
}
|
|
4038
|
-
async function
|
|
4416
|
+
async function snapshotAria(opts) {
|
|
4417
|
+
const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
|
|
4039
4418
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4040
4419
|
ensurePageState(page);
|
|
4041
|
-
const
|
|
4042
|
-
const
|
|
4043
|
-
|
|
4044
|
-
|
|
4045
|
-
|
|
4046
|
-
|
|
4047
|
-
);
|
|
4048
|
-
let body = await response.text();
|
|
4049
|
-
let truncated = false;
|
|
4050
|
-
const maxChars = typeof opts.maxChars === "number" && Number.isFinite(opts.maxChars) ? Math.max(1, Math.min(5e6, Math.floor(opts.maxChars))) : 2e5;
|
|
4051
|
-
if (body.length > maxChars) {
|
|
4052
|
-
body = body.slice(0, maxChars);
|
|
4053
|
-
truncated = true;
|
|
4054
|
-
}
|
|
4055
|
-
const headers = {};
|
|
4056
|
-
const allHeaders = response.headers();
|
|
4057
|
-
for (const [key, value] of Object.entries(allHeaders)) {
|
|
4058
|
-
headers[key] = value;
|
|
4059
|
-
}
|
|
4420
|
+
const sourceUrl = page.url();
|
|
4421
|
+
const res = await withPlaywrightPageCdpSession(page, async (session) => {
|
|
4422
|
+
await session.send("Accessibility.enable").catch(() => {
|
|
4423
|
+
});
|
|
4424
|
+
return await session.send("Accessibility.getFullAXTree");
|
|
4425
|
+
});
|
|
4060
4426
|
return {
|
|
4061
|
-
|
|
4062
|
-
|
|
4063
|
-
|
|
4064
|
-
|
|
4065
|
-
|
|
4427
|
+
nodes: formatAriaNodes(Array.isArray(res.nodes) ? res.nodes : [], limit),
|
|
4428
|
+
untrusted: true,
|
|
4429
|
+
contentMeta: {
|
|
4430
|
+
sourceUrl,
|
|
4431
|
+
contentType: "browser-aria-tree",
|
|
4432
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
4433
|
+
}
|
|
4066
4434
|
};
|
|
4067
4435
|
}
|
|
4068
|
-
|
|
4069
|
-
|
|
4070
|
-
|
|
4071
|
-
|
|
4072
|
-
|
|
4073
|
-
|
|
4074
|
-
case "warning":
|
|
4075
|
-
case "warn":
|
|
4076
|
-
return 2;
|
|
4077
|
-
case "info":
|
|
4078
|
-
case "log":
|
|
4079
|
-
return 1;
|
|
4080
|
-
case "debug":
|
|
4081
|
-
return 0;
|
|
4082
|
-
default:
|
|
4083
|
-
return 1;
|
|
4084
|
-
}
|
|
4085
|
-
}
|
|
4086
|
-
async function getConsoleMessagesViaPlaywright(opts) {
|
|
4087
|
-
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
4088
|
-
const messages = opts.level ? state.console.filter((msg) => consolePriority(msg.type) >= consolePriority(opts.level)) : [...state.console];
|
|
4089
|
-
if (opts.clear) state.console = [];
|
|
4090
|
-
return messages;
|
|
4091
|
-
}
|
|
4092
|
-
async function getPageErrorsViaPlaywright(opts) {
|
|
4093
|
-
const state = ensurePageState(await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId }));
|
|
4094
|
-
const errors = [...state.errors];
|
|
4095
|
-
if (opts.clear) state.errors = [];
|
|
4096
|
-
return { errors };
|
|
4436
|
+
function axValue(v) {
|
|
4437
|
+
if (!v || typeof v !== "object") return "";
|
|
4438
|
+
const value = v.value;
|
|
4439
|
+
if (typeof value === "string") return value;
|
|
4440
|
+
if (typeof value === "number" || typeof value === "boolean") return String(value);
|
|
4441
|
+
return "";
|
|
4097
4442
|
}
|
|
4098
|
-
|
|
4099
|
-
const
|
|
4100
|
-
const
|
|
4101
|
-
const
|
|
4102
|
-
const
|
|
4103
|
-
|
|
4104
|
-
|
|
4105
|
-
|
|
4443
|
+
function formatAriaNodes(nodes, limit) {
|
|
4444
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4445
|
+
for (const n of nodes) if (n.nodeId) byId.set(n.nodeId, n);
|
|
4446
|
+
const referenced = /* @__PURE__ */ new Set();
|
|
4447
|
+
for (const n of nodes) for (const c of n.childIds ?? []) referenced.add(c);
|
|
4448
|
+
const root = nodes.find((n) => n.nodeId !== "" && !referenced.has(n.nodeId)) ?? nodes[0];
|
|
4449
|
+
if (root.nodeId === "") return [];
|
|
4450
|
+
const out = [];
|
|
4451
|
+
const stack = [{ id: root.nodeId, depth: 0 }];
|
|
4452
|
+
while (stack.length && out.length < limit) {
|
|
4453
|
+
const popped = stack.pop();
|
|
4454
|
+
if (!popped) break;
|
|
4455
|
+
const { id, depth } = popped;
|
|
4456
|
+
const n = byId.get(id);
|
|
4457
|
+
if (!n) continue;
|
|
4458
|
+
const role = axValue(n.role);
|
|
4459
|
+
const name = axValue(n.name);
|
|
4460
|
+
const value = axValue(n.value);
|
|
4461
|
+
const description = axValue(n.description);
|
|
4462
|
+
const ref = `ax${String(out.length + 1)}`;
|
|
4463
|
+
out.push({
|
|
4464
|
+
ref,
|
|
4465
|
+
role: role || "unknown",
|
|
4466
|
+
name: name || "",
|
|
4467
|
+
...value ? { value } : {},
|
|
4468
|
+
...description ? { description } : {},
|
|
4469
|
+
...typeof n.backendDOMNodeId === "number" ? { backendDOMNodeId: n.backendDOMNodeId } : {},
|
|
4470
|
+
depth
|
|
4471
|
+
});
|
|
4472
|
+
const children = (n.childIds ?? []).filter((c) => byId.has(c));
|
|
4473
|
+
for (let i = children.length - 1; i >= 0; i--) {
|
|
4474
|
+
if (children[i]) stack.push({ id: children[i], depth: depth + 1 });
|
|
4475
|
+
}
|
|
4106
4476
|
}
|
|
4107
|
-
return
|
|
4477
|
+
return out;
|
|
4108
4478
|
}
|
|
4109
4479
|
|
|
4110
4480
|
// src/storage/index.ts
|
|
@@ -4117,9 +4487,9 @@ async function cookiesSetViaPlaywright(opts) {
|
|
|
4117
4487
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4118
4488
|
ensurePageState(page);
|
|
4119
4489
|
const cookie = opts.cookie;
|
|
4120
|
-
if (
|
|
4121
|
-
const hasUrl = typeof cookie.url === "string" && cookie.url.trim();
|
|
4122
|
-
const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() && typeof cookie.path === "string" && cookie.path.trim();
|
|
4490
|
+
if (cookie.name === "") throw new Error("cookie name and value are required");
|
|
4491
|
+
const hasUrl = typeof cookie.url === "string" && cookie.url.trim() !== "";
|
|
4492
|
+
const hasDomainPath = typeof cookie.domain === "string" && cookie.domain.trim() !== "" && typeof cookie.path === "string" && cookie.path.trim() !== "";
|
|
4123
4493
|
if (!hasUrl && !hasDomainPath) throw new Error("cookie requires url, or domain+path");
|
|
4124
4494
|
await page.context().addCookies([cookie]);
|
|
4125
4495
|
}
|
|
@@ -4135,33 +4505,33 @@ async function storageGetViaPlaywright(opts) {
|
|
|
4135
4505
|
values: await page.evaluate(
|
|
4136
4506
|
({ kind, key }) => {
|
|
4137
4507
|
const store = kind === "session" ? window.sessionStorage : window.localStorage;
|
|
4138
|
-
if (key) {
|
|
4508
|
+
if (key !== void 0 && key !== "") {
|
|
4139
4509
|
const value = store.getItem(key);
|
|
4140
4510
|
return value === null ? {} : { [key]: value };
|
|
4141
4511
|
}
|
|
4142
4512
|
const out = {};
|
|
4143
4513
|
for (let i = 0; i < store.length; i++) {
|
|
4144
4514
|
const k = store.key(i);
|
|
4145
|
-
if (
|
|
4515
|
+
if (k === null || k === "") continue;
|
|
4146
4516
|
const v = store.getItem(k);
|
|
4147
4517
|
if (v !== null) out[k] = v;
|
|
4148
4518
|
}
|
|
4149
4519
|
return out;
|
|
4150
4520
|
},
|
|
4151
4521
|
{ kind: opts.kind, key: opts.key }
|
|
4152
|
-
)
|
|
4522
|
+
)
|
|
4153
4523
|
};
|
|
4154
4524
|
}
|
|
4155
4525
|
async function storageSetViaPlaywright(opts) {
|
|
4156
|
-
const key =
|
|
4157
|
-
if (
|
|
4526
|
+
const key = opts.key;
|
|
4527
|
+
if (key === "") throw new Error("key is required");
|
|
4158
4528
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
4159
4529
|
ensurePageState(page);
|
|
4160
4530
|
await page.evaluate(
|
|
4161
4531
|
({ kind, key: k, value }) => {
|
|
4162
4532
|
(kind === "session" ? window.sessionStorage : window.localStorage).setItem(k, value);
|
|
4163
4533
|
},
|
|
4164
|
-
{ kind: opts.kind, key, value:
|
|
4534
|
+
{ kind: opts.kind, key, value: opts.value }
|
|
4165
4535
|
);
|
|
4166
4536
|
}
|
|
4167
4537
|
async function storageClearViaPlaywright(opts) {
|
|
@@ -4228,8 +4598,10 @@ var CrawlPage = class {
|
|
|
4228
4598
|
}
|
|
4229
4599
|
});
|
|
4230
4600
|
}
|
|
4231
|
-
if (opts?.selector || opts?.frameSelector) {
|
|
4232
|
-
throw new Error(
|
|
4601
|
+
if (opts?.selector !== void 0 && opts.selector !== "" || opts?.frameSelector !== void 0 && opts.frameSelector !== "") {
|
|
4602
|
+
throw new Error(
|
|
4603
|
+
'selector and frameSelector are only supported in role mode. Use { mode: "role" } or omit these options.'
|
|
4604
|
+
);
|
|
4233
4605
|
}
|
|
4234
4606
|
return snapshotAi({
|
|
4235
4607
|
cdpUrl: this.cdpUrl,
|
|
@@ -5065,6 +5437,53 @@ var CrawlPage = class {
|
|
|
5065
5437
|
name
|
|
5066
5438
|
});
|
|
5067
5439
|
}
|
|
5440
|
+
// ── Anti-Bot ──────────────────────────────────────────────────
|
|
5441
|
+
/**
|
|
5442
|
+
* Detect whether the page is showing an anti-bot challenge
|
|
5443
|
+
* (Cloudflare, hCaptcha, reCAPTCHA, access-denied, rate-limit, etc.).
|
|
5444
|
+
*
|
|
5445
|
+
* Returns `null` if no challenge is detected.
|
|
5446
|
+
*
|
|
5447
|
+
* @example
|
|
5448
|
+
* ```ts
|
|
5449
|
+
* const challenge = await page.detectChallenge();
|
|
5450
|
+
* if (challenge) {
|
|
5451
|
+
* console.log(challenge.kind); // 'cloudflare-js'
|
|
5452
|
+
* console.log(challenge.message); // 'Cloudflare JS challenge'
|
|
5453
|
+
* }
|
|
5454
|
+
* ```
|
|
5455
|
+
*/
|
|
5456
|
+
async detectChallenge() {
|
|
5457
|
+
return detectChallengeViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
|
|
5458
|
+
}
|
|
5459
|
+
/**
|
|
5460
|
+
* Wait for an anti-bot challenge to resolve on its own.
|
|
5461
|
+
*
|
|
5462
|
+
* Cloudflare JS challenges typically auto-resolve in ~5 seconds.
|
|
5463
|
+
* CAPTCHA challenges will only resolve if solved in a visible browser window.
|
|
5464
|
+
*
|
|
5465
|
+
* @param opts.timeoutMs - Maximum wait time (default: `15000`)
|
|
5466
|
+
* @param opts.pollMs - Poll interval (default: `500`)
|
|
5467
|
+
* @returns Whether the challenge resolved, and the remaining challenge info if not
|
|
5468
|
+
*
|
|
5469
|
+
* @example
|
|
5470
|
+
* ```ts
|
|
5471
|
+
* await page.goto('https://example.com');
|
|
5472
|
+
* const challenge = await page.detectChallenge();
|
|
5473
|
+
* if (challenge?.kind === 'cloudflare-js') {
|
|
5474
|
+
* const { resolved } = await page.waitForChallenge({ timeoutMs: 20000 });
|
|
5475
|
+
* if (!resolved) throw new Error('Challenge did not resolve');
|
|
5476
|
+
* }
|
|
5477
|
+
* ```
|
|
5478
|
+
*/
|
|
5479
|
+
async waitForChallenge(opts) {
|
|
5480
|
+
return waitForChallengeViaPlaywright({
|
|
5481
|
+
cdpUrl: this.cdpUrl,
|
|
5482
|
+
targetId: this.targetId,
|
|
5483
|
+
timeoutMs: opts?.timeoutMs,
|
|
5484
|
+
pollMs: opts?.pollMs
|
|
5485
|
+
});
|
|
5486
|
+
}
|
|
5068
5487
|
};
|
|
5069
5488
|
var BrowserClaw = class _BrowserClaw {
|
|
5070
5489
|
cdpUrl;
|
|
@@ -5100,8 +5519,8 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5100
5519
|
*/
|
|
5101
5520
|
static async launch(opts = {}) {
|
|
5102
5521
|
const chrome = await launchChrome(opts);
|
|
5103
|
-
const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
|
|
5104
|
-
const ssrfPolicy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
5522
|
+
const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
|
|
5523
|
+
const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
5105
5524
|
return new _BrowserClaw(cdpUrl, chrome, ssrfPolicy);
|
|
5106
5525
|
}
|
|
5107
5526
|
/**
|
|
@@ -5123,7 +5542,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5123
5542
|
throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
|
|
5124
5543
|
}
|
|
5125
5544
|
await connectBrowser(cdpUrl, opts?.authToken);
|
|
5126
|
-
const ssrfPolicy = opts?.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
5545
|
+
const ssrfPolicy = opts?.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
5127
5546
|
return new _BrowserClaw(cdpUrl, null, ssrfPolicy);
|
|
5128
5547
|
}
|
|
5129
5548
|
/**
|
|
@@ -5149,10 +5568,10 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5149
5568
|
*/
|
|
5150
5569
|
async currentPage() {
|
|
5151
5570
|
const { browser } = await connectBrowser(this.cdpUrl);
|
|
5152
|
-
const pages =
|
|
5571
|
+
const pages = getAllPages(browser);
|
|
5153
5572
|
if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
|
|
5154
5573
|
const tid = await pageTargetId(pages[0]).catch(() => null);
|
|
5155
|
-
if (
|
|
5574
|
+
if (tid === null || tid === "") throw new Error("Failed to get targetId for the current page.");
|
|
5156
5575
|
return new CrawlPage(this.cdpUrl, tid, this.ssrfPolicy);
|
|
5157
5576
|
}
|
|
5158
5577
|
/**
|
|
@@ -5210,6 +5629,6 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5210
5629
|
}
|
|
5211
5630
|
};
|
|
5212
5631
|
|
|
5213
|
-
export { BrowserClaw, BrowserTabNotFoundError, CrawlPage, InvalidBrowserNavigationUrlError, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
|
|
5632
|
+
export { BrowserClaw, BrowserTabNotFoundError, CrawlPage, InvalidBrowserNavigationUrlError, STEALTH_SCRIPT, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, detectChallengeViaPlaywright, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, waitForChallengeViaPlaywright, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
|
|
5214
5633
|
//# sourceMappingURL=index.js.map
|
|
5215
5634
|
//# sourceMappingURL=index.js.map
|