browserclaw 0.2.3 → 0.2.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +191 -50
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +49 -4
- package/dist/index.d.ts +49 -4
- package/dist/index.js +192 -51
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -6,6 +6,7 @@ var fs = require('fs');
|
|
|
6
6
|
var net = require('net');
|
|
7
7
|
var child_process = require('child_process');
|
|
8
8
|
var playwrightCore = require('playwright-core');
|
|
9
|
+
var promises = require('dns/promises');
|
|
9
10
|
|
|
10
11
|
function _interopDefault (e) { return e && e.__esModule ? e : { default: e }; }
|
|
11
12
|
|
|
@@ -272,12 +273,12 @@ function resolveBrowserExecutable(opts) {
|
|
|
272
273
|
return null;
|
|
273
274
|
}
|
|
274
275
|
async function ensurePortAvailable(port) {
|
|
275
|
-
await new Promise((
|
|
276
|
+
await new Promise((resolve2, reject) => {
|
|
276
277
|
const tester = net__default.default.createServer().once("error", (err) => {
|
|
277
278
|
if (err.code === "EADDRINUSE") reject(new Error(`Port ${port} is already in use`));
|
|
278
279
|
else reject(err);
|
|
279
280
|
}).once("listening", () => {
|
|
280
|
-
tester.close(() =>
|
|
281
|
+
tester.close(() => resolve2());
|
|
281
282
|
}).listen(port);
|
|
282
283
|
});
|
|
283
284
|
}
|
|
@@ -347,11 +348,13 @@ function resolveUserDataDir(profileName) {
|
|
|
347
348
|
const configDir = process.env.XDG_CONFIG_HOME ?? path__default.default.join(os__default.default.homedir(), ".config");
|
|
348
349
|
return path__default.default.join(configDir, "browserclaw", "profiles", profileName, "user-data");
|
|
349
350
|
}
|
|
350
|
-
async function isChromeReachable(cdpUrl, timeoutMs = 500) {
|
|
351
|
+
async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
|
|
351
352
|
const ctrl = new AbortController();
|
|
352
353
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
353
354
|
try {
|
|
354
|
-
const
|
|
355
|
+
const headers = {};
|
|
356
|
+
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
357
|
+
const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal, headers });
|
|
355
358
|
return res.ok;
|
|
356
359
|
} catch {
|
|
357
360
|
return false;
|
|
@@ -359,11 +362,13 @@ async function isChromeReachable(cdpUrl, timeoutMs = 500) {
|
|
|
359
362
|
clearTimeout(t);
|
|
360
363
|
}
|
|
361
364
|
}
|
|
362
|
-
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500) {
|
|
365
|
+
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
|
|
363
366
|
const ctrl = new AbortController();
|
|
364
367
|
const t = setTimeout(() => ctrl.abort(), timeoutMs);
|
|
365
368
|
try {
|
|
366
|
-
const
|
|
369
|
+
const headers = {};
|
|
370
|
+
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
371
|
+
const res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal, headers });
|
|
367
372
|
if (!res.ok) return null;
|
|
368
373
|
const data = await res.json();
|
|
369
374
|
return String(data?.webSocketDebuggerUrl ?? "").trim() || null;
|
|
@@ -629,7 +634,7 @@ function restoreRoleRefsForTarget(opts) {
|
|
|
629
634
|
state.roleRefsFrameSelector = entry.frameSelector;
|
|
630
635
|
state.roleRefsMode = entry.mode;
|
|
631
636
|
}
|
|
632
|
-
async function connectBrowser(cdpUrl) {
|
|
637
|
+
async function connectBrowser(cdpUrl, authToken) {
|
|
633
638
|
const normalized = normalizeCdpUrl(cdpUrl);
|
|
634
639
|
if (cached?.cdpUrl === normalized) return cached;
|
|
635
640
|
const existing = connectingByUrl.get(normalized);
|
|
@@ -639,9 +644,11 @@ async function connectBrowser(cdpUrl) {
|
|
|
639
644
|
for (let attempt = 0; attempt < 3; attempt++) {
|
|
640
645
|
try {
|
|
641
646
|
const timeout = 5e3 + attempt * 2e3;
|
|
642
|
-
const endpoint = await getChromeWebSocketUrl(normalized, timeout).catch(() => null) ?? normalized;
|
|
643
|
-
const
|
|
644
|
-
|
|
647
|
+
const endpoint = await getChromeWebSocketUrl(normalized, timeout, authToken).catch(() => null) ?? normalized;
|
|
648
|
+
const headers = {};
|
|
649
|
+
if (authToken) headers["Authorization"] = `Bearer ${authToken}`;
|
|
650
|
+
const browser = await playwrightCore.chromium.connectOverCDP(endpoint, { timeout, headers });
|
|
651
|
+
const connected = { browser, cdpUrl: normalized, authToken };
|
|
645
652
|
cached = connected;
|
|
646
653
|
observeBrowser(browser);
|
|
647
654
|
browser.on("disconnected", () => {
|
|
@@ -694,14 +701,26 @@ async function pageTargetId(page) {
|
|
|
694
701
|
}
|
|
695
702
|
async function findPageByTargetId(browser, targetId, cdpUrl) {
|
|
696
703
|
const pages = await getAllPages(browser);
|
|
704
|
+
let resolvedViaCdp = false;
|
|
697
705
|
for (const page of pages) {
|
|
698
|
-
|
|
706
|
+
let tid = null;
|
|
707
|
+
try {
|
|
708
|
+
tid = await pageTargetId(page);
|
|
709
|
+
resolvedViaCdp = true;
|
|
710
|
+
} catch {
|
|
711
|
+
tid = null;
|
|
712
|
+
}
|
|
699
713
|
if (tid && tid === targetId) return page;
|
|
700
714
|
}
|
|
715
|
+
if (!resolvedViaCdp && pages.length === 1) {
|
|
716
|
+
return pages[0];
|
|
717
|
+
}
|
|
701
718
|
if (cdpUrl) {
|
|
702
719
|
try {
|
|
703
720
|
const listUrl = `${cdpUrl.replace(/\/+$/, "").replace(/^ws:/, "http:").replace(/\/cdp$/, "")}/json/list`;
|
|
704
|
-
const
|
|
721
|
+
const headers = {};
|
|
722
|
+
if (cached?.authToken) headers["Authorization"] = `Bearer ${cached.authToken}`;
|
|
723
|
+
const response = await fetch(listUrl, { headers });
|
|
705
724
|
if (response.ok) {
|
|
706
725
|
const targets = await response.json();
|
|
707
726
|
const target = targets.find((t) => t.id === targetId);
|
|
@@ -826,6 +845,27 @@ function getIndentLevel(line) {
|
|
|
826
845
|
const match = line.match(/^(\s*)/);
|
|
827
846
|
return match ? Math.floor(match[1].length / 2) : 0;
|
|
828
847
|
}
|
|
848
|
+
function matchInteractiveSnapshotLine(line, options) {
|
|
849
|
+
const depth = getIndentLevel(line);
|
|
850
|
+
if (options.maxDepth !== void 0 && depth > options.maxDepth) {
|
|
851
|
+
return null;
|
|
852
|
+
}
|
|
853
|
+
const match = line.match(/^(\s*-\s*)(\w+)(?:\s+"([^"]*)")?(.*)$/);
|
|
854
|
+
if (!match) {
|
|
855
|
+
return null;
|
|
856
|
+
}
|
|
857
|
+
const [, , roleRaw, name, suffix] = match;
|
|
858
|
+
if (roleRaw.startsWith("/")) {
|
|
859
|
+
return null;
|
|
860
|
+
}
|
|
861
|
+
const role = roleRaw.toLowerCase();
|
|
862
|
+
return {
|
|
863
|
+
roleRaw,
|
|
864
|
+
role,
|
|
865
|
+
...name ? { name } : {},
|
|
866
|
+
suffix
|
|
867
|
+
};
|
|
868
|
+
}
|
|
829
869
|
function createRoleNameTracker() {
|
|
830
870
|
const counts = /* @__PURE__ */ new Map();
|
|
831
871
|
const refsByKey = /* @__PURE__ */ new Map();
|
|
@@ -899,14 +939,11 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
|
899
939
|
if (options.interactive) {
|
|
900
940
|
const result2 = [];
|
|
901
941
|
for (const line of lines) {
|
|
902
|
-
const
|
|
903
|
-
if (
|
|
904
|
-
const
|
|
905
|
-
if (!match) continue;
|
|
906
|
-
const [, prefix, roleRaw, name, suffix] = match;
|
|
907
|
-
if (roleRaw.startsWith("/")) continue;
|
|
908
|
-
const role = roleRaw.toLowerCase();
|
|
942
|
+
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
943
|
+
if (!parsed) continue;
|
|
944
|
+
const { roleRaw, role, name, suffix } = parsed;
|
|
909
945
|
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
946
|
+
const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
|
|
910
947
|
const ref = nextRef();
|
|
911
948
|
const nth = tracker.getNextIndex(role, name);
|
|
912
949
|
tracker.trackRef(role, name, ref);
|
|
@@ -969,16 +1006,13 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
|
969
1006
|
if (options.interactive) {
|
|
970
1007
|
const out2 = [];
|
|
971
1008
|
for (const line of lines) {
|
|
972
|
-
const
|
|
973
|
-
if (
|
|
974
|
-
const
|
|
975
|
-
if (!match) continue;
|
|
976
|
-
const [, prefix, roleRaw, name, suffix] = match;
|
|
977
|
-
if (roleRaw.startsWith("/")) continue;
|
|
978
|
-
const role = roleRaw.toLowerCase();
|
|
1009
|
+
const parsed = matchInteractiveSnapshotLine(line, options);
|
|
1010
|
+
if (!parsed) continue;
|
|
1011
|
+
const { roleRaw, role, name, suffix } = parsed;
|
|
979
1012
|
if (!INTERACTIVE_ROLES.has(role)) continue;
|
|
980
1013
|
const ref = parseAiSnapshotRef(suffix);
|
|
981
1014
|
if (!ref) continue;
|
|
1015
|
+
const prefix = line.match(/^(\s*-\s*)/)?.[1] ?? "";
|
|
982
1016
|
refs[ref] = { role, ...name ? { name } : {} };
|
|
983
1017
|
out2.push(`${prefix}${roleRaw}${name ? ` "${name}"` : ""}${suffix}`);
|
|
984
1018
|
}
|
|
@@ -1026,6 +1060,7 @@ async function snapshotAi(opts) {
|
|
|
1026
1060
|
if (!maybe._snapshotForAI) {
|
|
1027
1061
|
throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
|
|
1028
1062
|
}
|
|
1063
|
+
const sourceUrl = page.url();
|
|
1029
1064
|
const result = await maybe._snapshotForAI({
|
|
1030
1065
|
timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
|
|
1031
1066
|
track: "response"
|
|
@@ -1052,7 +1087,12 @@ async function snapshotAi(opts) {
|
|
|
1052
1087
|
snapshot: built.snapshot,
|
|
1053
1088
|
refs: built.refs,
|
|
1054
1089
|
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
1055
|
-
untrusted: true
|
|
1090
|
+
untrusted: true,
|
|
1091
|
+
contentMeta: {
|
|
1092
|
+
sourceUrl,
|
|
1093
|
+
contentType: "browser-snapshot",
|
|
1094
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1095
|
+
}
|
|
1056
1096
|
};
|
|
1057
1097
|
}
|
|
1058
1098
|
|
|
@@ -1060,6 +1100,7 @@ async function snapshotAi(opts) {
|
|
|
1060
1100
|
async function snapshotRole(opts) {
|
|
1061
1101
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1062
1102
|
ensurePageState(page);
|
|
1103
|
+
const sourceUrl = page.url();
|
|
1063
1104
|
const frameSelector = opts.frameSelector?.trim() || "";
|
|
1064
1105
|
const selector = opts.selector?.trim() || "";
|
|
1065
1106
|
const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
|
|
@@ -1077,19 +1118,33 @@ async function snapshotRole(opts) {
|
|
|
1077
1118
|
snapshot: built.snapshot,
|
|
1078
1119
|
refs: built.refs,
|
|
1079
1120
|
stats: getRoleSnapshotStats(built.snapshot, built.refs),
|
|
1080
|
-
untrusted: true
|
|
1121
|
+
untrusted: true,
|
|
1122
|
+
contentMeta: {
|
|
1123
|
+
sourceUrl,
|
|
1124
|
+
contentType: "browser-snapshot",
|
|
1125
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1126
|
+
}
|
|
1081
1127
|
};
|
|
1082
1128
|
}
|
|
1083
1129
|
async function snapshotAria(opts) {
|
|
1084
1130
|
const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
|
|
1085
1131
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1086
1132
|
ensurePageState(page);
|
|
1133
|
+
const sourceUrl = page.url();
|
|
1087
1134
|
const session = await page.context().newCDPSession(page);
|
|
1088
1135
|
try {
|
|
1089
1136
|
await session.send("Accessibility.enable").catch(() => {
|
|
1090
1137
|
});
|
|
1091
1138
|
const res = await session.send("Accessibility.getFullAXTree");
|
|
1092
|
-
return {
|
|
1139
|
+
return {
|
|
1140
|
+
nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit),
|
|
1141
|
+
untrusted: true,
|
|
1142
|
+
contentMeta: {
|
|
1143
|
+
sourceUrl,
|
|
1144
|
+
contentType: "browser-aria-tree",
|
|
1145
|
+
capturedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1093
1148
|
} finally {
|
|
1094
1149
|
await session.detach().catch(() => {
|
|
1095
1150
|
});
|
|
@@ -1280,7 +1335,7 @@ async function armDialogViaPlaywright(opts) {
|
|
|
1280
1335
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1281
1336
|
ensurePageState(page);
|
|
1282
1337
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
1283
|
-
return new Promise((
|
|
1338
|
+
return new Promise((resolve2, reject) => {
|
|
1284
1339
|
const timer = setTimeout(() => {
|
|
1285
1340
|
page.removeListener("dialog", handler);
|
|
1286
1341
|
reject(new Error(`No dialog appeared within ${timeout}ms`));
|
|
@@ -1293,7 +1348,7 @@ async function armDialogViaPlaywright(opts) {
|
|
|
1293
1348
|
} else {
|
|
1294
1349
|
await dialog.dismiss();
|
|
1295
1350
|
}
|
|
1296
|
-
|
|
1351
|
+
resolve2();
|
|
1297
1352
|
} catch (err) {
|
|
1298
1353
|
reject(err);
|
|
1299
1354
|
}
|
|
@@ -1305,7 +1360,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
1305
1360
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1306
1361
|
ensurePageState(page);
|
|
1307
1362
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
1308
|
-
return new Promise((
|
|
1363
|
+
return new Promise((resolve2, reject) => {
|
|
1309
1364
|
const timer = setTimeout(() => {
|
|
1310
1365
|
page.removeListener("filechooser", handler);
|
|
1311
1366
|
reject(new Error(`No file chooser appeared within ${timeout}ms`));
|
|
@@ -1314,7 +1369,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
1314
1369
|
clearTimeout(timer);
|
|
1315
1370
|
try {
|
|
1316
1371
|
await fc.setFiles(opts.paths ?? []);
|
|
1317
|
-
|
|
1372
|
+
resolve2();
|
|
1318
1373
|
} catch (err) {
|
|
1319
1374
|
reject(err);
|
|
1320
1375
|
}
|
|
@@ -1331,11 +1386,82 @@ async function pressKeyViaPlaywright(opts) {
|
|
|
1331
1386
|
ensurePageState(page);
|
|
1332
1387
|
await page.keyboard.press(key, { delay: Math.max(0, Math.floor(opts.delayMs ?? 0)) });
|
|
1333
1388
|
}
|
|
1389
|
+
function assertSafeOutputPath(path2, allowedRoots) {
|
|
1390
|
+
if (!path2 || typeof path2 !== "string") {
|
|
1391
|
+
throw new Error("Output path is required.");
|
|
1392
|
+
}
|
|
1393
|
+
const normalized = path.normalize(path2);
|
|
1394
|
+
if (normalized.includes("..")) {
|
|
1395
|
+
throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
|
|
1396
|
+
}
|
|
1397
|
+
if (allowedRoots?.length) {
|
|
1398
|
+
const resolved = path.resolve(normalized);
|
|
1399
|
+
const withinRoot = allowedRoots.some((root) => {
|
|
1400
|
+
const normalizedRoot = path.resolve(root);
|
|
1401
|
+
return resolved === normalizedRoot || resolved.startsWith(normalizedRoot + path.sep);
|
|
1402
|
+
});
|
|
1403
|
+
if (!withinRoot) {
|
|
1404
|
+
throw new Error(`Unsafe output path: "${path2}" is outside allowed directories.`);
|
|
1405
|
+
}
|
|
1406
|
+
}
|
|
1407
|
+
}
|
|
1408
|
+
function isInternalIP(ip) {
|
|
1409
|
+
if (/^127\./.test(ip)) return true;
|
|
1410
|
+
if (/^10\./.test(ip)) return true;
|
|
1411
|
+
if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
|
|
1412
|
+
if (/^192\.168\./.test(ip)) return true;
|
|
1413
|
+
if (/^169\.254\./.test(ip)) return true;
|
|
1414
|
+
if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
|
|
1415
|
+
if (ip === "0.0.0.0") return true;
|
|
1416
|
+
const lower = ip.toLowerCase();
|
|
1417
|
+
if (lower === "::1") return true;
|
|
1418
|
+
if (lower.startsWith("fe80:")) return true;
|
|
1419
|
+
if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
|
|
1420
|
+
if (lower.startsWith("::ffff:")) {
|
|
1421
|
+
const v4 = lower.replace(/^::ffff:/, "");
|
|
1422
|
+
return isInternalIP(v4);
|
|
1423
|
+
}
|
|
1424
|
+
return false;
|
|
1425
|
+
}
|
|
1426
|
+
function isInternalUrl(url) {
|
|
1427
|
+
let parsed;
|
|
1428
|
+
try {
|
|
1429
|
+
parsed = new URL(url);
|
|
1430
|
+
} catch {
|
|
1431
|
+
return true;
|
|
1432
|
+
}
|
|
1433
|
+
const hostname = parsed.hostname.toLowerCase();
|
|
1434
|
+
if (hostname === "localhost") return true;
|
|
1435
|
+
if (isInternalIP(hostname)) return true;
|
|
1436
|
+
if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
|
|
1437
|
+
return true;
|
|
1438
|
+
}
|
|
1439
|
+
return false;
|
|
1440
|
+
}
|
|
1441
|
+
async function isInternalUrlResolved(url) {
|
|
1442
|
+
if (isInternalUrl(url)) return true;
|
|
1443
|
+
let parsed;
|
|
1444
|
+
try {
|
|
1445
|
+
parsed = new URL(url);
|
|
1446
|
+
} catch {
|
|
1447
|
+
return true;
|
|
1448
|
+
}
|
|
1449
|
+
try {
|
|
1450
|
+
const { address } = await promises.lookup(parsed.hostname);
|
|
1451
|
+
if (isInternalIP(address)) return true;
|
|
1452
|
+
} catch {
|
|
1453
|
+
return true;
|
|
1454
|
+
}
|
|
1455
|
+
return false;
|
|
1456
|
+
}
|
|
1334
1457
|
|
|
1335
1458
|
// src/actions/navigation.ts
|
|
1336
1459
|
async function navigateViaPlaywright(opts) {
|
|
1337
1460
|
const url = String(opts.url ?? "").trim();
|
|
1338
1461
|
if (!url) throw new Error("url is required");
|
|
1462
|
+
if (!opts.allowInternal && await isInternalUrlResolved(url)) {
|
|
1463
|
+
throw new Error(`Navigation to internal/loopback address blocked: "${url}". Set allowInternal: true if this is intentional.`);
|
|
1464
|
+
}
|
|
1339
1465
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1340
1466
|
ensurePageState(page);
|
|
1341
1467
|
await page.goto(url, { timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
|
|
@@ -1357,11 +1483,14 @@ async function listPagesViaPlaywright(opts) {
|
|
|
1357
1483
|
return results;
|
|
1358
1484
|
}
|
|
1359
1485
|
async function createPageViaPlaywright(opts) {
|
|
1486
|
+
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
1487
|
+
if (targetUrl !== "about:blank" && !opts.allowInternal && await isInternalUrlResolved(targetUrl)) {
|
|
1488
|
+
throw new Error(`Navigation to internal/loopback address blocked: "${targetUrl}". Set allowInternal: true if this is intentional.`);
|
|
1489
|
+
}
|
|
1360
1490
|
const { browser } = await connectBrowser(opts.cdpUrl);
|
|
1361
1491
|
const context = browser.contexts()[0] ?? await browser.newContext();
|
|
1362
1492
|
const page = await context.newPage();
|
|
1363
1493
|
ensurePageState(page);
|
|
1364
|
-
const targetUrl = (opts.url ?? "").trim() || "about:blank";
|
|
1365
1494
|
if (targetUrl !== "about:blank") {
|
|
1366
1495
|
await page.goto(targetUrl, { timeout: normalizeTimeoutMs(void 0, 2e4) });
|
|
1367
1496
|
}
|
|
@@ -1507,6 +1636,7 @@ async function evaluateViaPlaywright(opts) {
|
|
|
1507
1636
|
|
|
1508
1637
|
// src/actions/download.ts
|
|
1509
1638
|
async function downloadViaPlaywright(opts) {
|
|
1639
|
+
assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1510
1640
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1511
1641
|
ensurePageState(page);
|
|
1512
1642
|
restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
|
|
@@ -1533,6 +1663,7 @@ async function waitForDownloadViaPlaywright(opts) {
|
|
|
1533
1663
|
const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
|
|
1534
1664
|
const download = await page.waitForEvent("download", { timeout });
|
|
1535
1665
|
const savePath = opts.path ?? download.suggestedFilename();
|
|
1666
|
+
assertSafeOutputPath(savePath, opts.allowedOutputRoots);
|
|
1536
1667
|
await download.saveAs(savePath);
|
|
1537
1668
|
return {
|
|
1538
1669
|
url: download.url(),
|
|
@@ -1716,6 +1847,7 @@ async function traceStartViaPlaywright(opts) {
|
|
|
1716
1847
|
});
|
|
1717
1848
|
}
|
|
1718
1849
|
async function traceStopViaPlaywright(opts) {
|
|
1850
|
+
assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
|
|
1719
1851
|
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
1720
1852
|
ensurePageState(page);
|
|
1721
1853
|
const context = page.context();
|
|
@@ -1861,10 +1993,12 @@ async function storageClearViaPlaywright(opts) {
|
|
|
1861
1993
|
var CrawlPage = class {
|
|
1862
1994
|
cdpUrl;
|
|
1863
1995
|
targetId;
|
|
1996
|
+
allowInternal;
|
|
1864
1997
|
/** @internal */
|
|
1865
|
-
constructor(cdpUrl, targetId) {
|
|
1998
|
+
constructor(cdpUrl, targetId, allowInternal = false) {
|
|
1866
1999
|
this.cdpUrl = cdpUrl;
|
|
1867
2000
|
this.targetId = targetId;
|
|
2001
|
+
this.allowInternal = allowInternal;
|
|
1868
2002
|
}
|
|
1869
2003
|
/** The CDP target ID for this page. Use this to identify the page in multi-tab scenarios. */
|
|
1870
2004
|
get id() {
|
|
@@ -2196,7 +2330,8 @@ var CrawlPage = class {
|
|
|
2196
2330
|
cdpUrl: this.cdpUrl,
|
|
2197
2331
|
targetId: this.targetId,
|
|
2198
2332
|
url,
|
|
2199
|
-
timeoutMs: opts?.timeoutMs
|
|
2333
|
+
timeoutMs: opts?.timeoutMs,
|
|
2334
|
+
allowInternal: this.allowInternal
|
|
2200
2335
|
});
|
|
2201
2336
|
}
|
|
2202
2337
|
/**
|
|
@@ -2383,12 +2518,14 @@ var CrawlPage = class {
|
|
|
2383
2518
|
* Stop recording a trace and save it to a file.
|
|
2384
2519
|
*
|
|
2385
2520
|
* @param path - File path to save the trace (e.g. `'trace.zip'`)
|
|
2521
|
+
* @param opts - Options (allowedOutputRoots: constrain output to specific directories)
|
|
2386
2522
|
*/
|
|
2387
|
-
async traceStop(path2) {
|
|
2523
|
+
async traceStop(path2, opts) {
|
|
2388
2524
|
return traceStopViaPlaywright({
|
|
2389
2525
|
cdpUrl: this.cdpUrl,
|
|
2390
2526
|
targetId: this.targetId,
|
|
2391
|
-
path: path2
|
|
2527
|
+
path: path2,
|
|
2528
|
+
allowedOutputRoots: opts?.allowedOutputRoots
|
|
2392
2529
|
});
|
|
2393
2530
|
}
|
|
2394
2531
|
/**
|
|
@@ -2576,7 +2713,8 @@ var CrawlPage = class {
|
|
|
2576
2713
|
targetId: this.targetId,
|
|
2577
2714
|
ref,
|
|
2578
2715
|
path: path2,
|
|
2579
|
-
timeoutMs: opts?.timeoutMs
|
|
2716
|
+
timeoutMs: opts?.timeoutMs,
|
|
2717
|
+
allowedOutputRoots: opts?.allowedOutputRoots
|
|
2580
2718
|
});
|
|
2581
2719
|
}
|
|
2582
2720
|
/**
|
|
@@ -2592,7 +2730,8 @@ var CrawlPage = class {
|
|
|
2592
2730
|
cdpUrl: this.cdpUrl,
|
|
2593
2731
|
targetId: this.targetId,
|
|
2594
2732
|
path: opts?.path,
|
|
2595
|
-
timeoutMs: opts?.timeoutMs
|
|
2733
|
+
timeoutMs: opts?.timeoutMs,
|
|
2734
|
+
allowedOutputRoots: opts?.allowedOutputRoots
|
|
2596
2735
|
});
|
|
2597
2736
|
}
|
|
2598
2737
|
// ── Emulation ───────────────────────────────────────────────
|
|
@@ -2722,10 +2861,12 @@ var CrawlPage = class {
|
|
|
2722
2861
|
};
|
|
2723
2862
|
var BrowserClaw = class _BrowserClaw {
|
|
2724
2863
|
cdpUrl;
|
|
2864
|
+
allowInternal;
|
|
2725
2865
|
chrome;
|
|
2726
|
-
constructor(cdpUrl, chrome) {
|
|
2866
|
+
constructor(cdpUrl, chrome, allowInternal = false) {
|
|
2727
2867
|
this.cdpUrl = cdpUrl;
|
|
2728
2868
|
this.chrome = chrome;
|
|
2869
|
+
this.allowInternal = allowInternal;
|
|
2729
2870
|
}
|
|
2730
2871
|
/**
|
|
2731
2872
|
* Launch a new Chrome instance and connect to it.
|
|
@@ -2753,7 +2894,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
2753
2894
|
static async launch(opts = {}) {
|
|
2754
2895
|
const chrome = await launchChrome(opts);
|
|
2755
2896
|
const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
|
|
2756
|
-
return new _BrowserClaw(cdpUrl, chrome);
|
|
2897
|
+
return new _BrowserClaw(cdpUrl, chrome, opts.allowInternal);
|
|
2757
2898
|
}
|
|
2758
2899
|
/**
|
|
2759
2900
|
* Connect to an already-running Chrome instance via its CDP endpoint.
|
|
@@ -2769,12 +2910,12 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
2769
2910
|
* const browser = await BrowserClaw.connect('http://localhost:9222');
|
|
2770
2911
|
* ```
|
|
2771
2912
|
*/
|
|
2772
|
-
static async connect(cdpUrl) {
|
|
2773
|
-
if (!await isChromeReachable(cdpUrl, 3e3)) {
|
|
2913
|
+
static async connect(cdpUrl, opts) {
|
|
2914
|
+
if (!await isChromeReachable(cdpUrl, 3e3, opts?.authToken)) {
|
|
2774
2915
|
throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
|
|
2775
2916
|
}
|
|
2776
|
-
await connectBrowser(cdpUrl);
|
|
2777
|
-
return new _BrowserClaw(cdpUrl, null);
|
|
2917
|
+
await connectBrowser(cdpUrl, opts?.authToken);
|
|
2918
|
+
return new _BrowserClaw(cdpUrl, null, opts?.allowInternal);
|
|
2778
2919
|
}
|
|
2779
2920
|
/**
|
|
2780
2921
|
* Open a URL in a new tab and return the page handle.
|
|
@@ -2789,8 +2930,8 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
2789
2930
|
* ```
|
|
2790
2931
|
*/
|
|
2791
2932
|
async open(url) {
|
|
2792
|
-
const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url });
|
|
2793
|
-
return new CrawlPage(this.cdpUrl, tab.targetId);
|
|
2933
|
+
const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url, allowInternal: this.allowInternal });
|
|
2934
|
+
return new CrawlPage(this.cdpUrl, tab.targetId, this.allowInternal);
|
|
2794
2935
|
}
|
|
2795
2936
|
/**
|
|
2796
2937
|
* Get a CrawlPage handle for the currently active tab.
|
|
@@ -2803,7 +2944,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
2803
2944
|
if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
|
|
2804
2945
|
const tid = await pageTargetId(pages[0]).catch(() => null);
|
|
2805
2946
|
if (!tid) throw new Error("Failed to get targetId for the current page.");
|
|
2806
|
-
return new CrawlPage(this.cdpUrl, tid);
|
|
2947
|
+
return new CrawlPage(this.cdpUrl, tid, this.allowInternal);
|
|
2807
2948
|
}
|
|
2808
2949
|
/**
|
|
2809
2950
|
* List all open tabs.
|
|
@@ -2838,7 +2979,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
2838
2979
|
* @returns CrawlPage for the specified tab
|
|
2839
2980
|
*/
|
|
2840
2981
|
page(targetId) {
|
|
2841
|
-
return new CrawlPage(this.cdpUrl, targetId);
|
|
2982
|
+
return new CrawlPage(this.cdpUrl, targetId, this.allowInternal);
|
|
2842
2983
|
}
|
|
2843
2984
|
/** The CDP endpoint URL for this browser connection. */
|
|
2844
2985
|
get url() {
|