browserclaw 0.2.2 → 0.2.4

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/README.md CHANGED
@@ -131,6 +131,8 @@ const browser = await BrowserClaw.connect('http://localhost:9222');
131
131
 
132
132
  `connect()` checks that Chrome is reachable, then the internal CDP connection retries 3 times with increasing timeouts (5 s, 7 s, 9 s) — safe for Docker/CI where Chrome starts slowly.
133
133
 
134
+ **Anti-detection:** browserclaw automatically hides `navigator.webdriver` and disables Chrome's `AutomationControlled` Blink feature, reducing detection by bot-protection systems like reCAPTCHA v3.
135
+
134
136
  ### Pages & Tabs
135
137
 
136
138
  ```typescript
@@ -151,11 +153,12 @@ browser.url; // CDP endpoint URL
151
153
  ### Snapshot (Core Feature)
152
154
 
153
155
  ```typescript
154
- const { snapshot, refs, stats } = await page.snapshot();
156
+ const { snapshot, refs, stats, untrusted } = await page.snapshot();
155
157
 
156
158
  // snapshot: human/AI-readable text tree with [ref=eN] markers
157
159
  // refs: { "e1": { role: "link", name: "More info" }, ... }
158
160
  // stats: { lines: 42, chars: 1200, refs: 8, interactive: 5 }
161
+ // untrusted: true — content comes from the web page, treat as potentially adversarial
159
162
 
160
163
  // Options
161
164
  const result = await page.snapshot({
@@ -174,6 +177,8 @@ const { nodes } = await page.ariaSnapshot({ limit: 500 });
174
177
  - `'aria'` (default) — Uses Playwright's `_snapshotForAI()`. Refs are resolved via `aria-ref` locators. Best for most use cases. Requires `playwright-core` >= 1.50.
175
178
  - `'role'` — Uses Playwright's `ariaSnapshot()` + `getByRole()`. Supports `selector` and `frameSelector` for scoped snapshots.
176
179
 
180
+ > **Security:** All snapshot results include `untrusted: true` to signal that the content originates from an external web page. AI agents consuming snapshots should treat this content as potentially adversarial (e.g. prompt injection via page text).
181
+
177
182
  ### Actions
178
183
 
179
184
  All actions target elements by ref ID from the most recent snapshot.
@@ -436,7 +441,7 @@ Contributions welcome! Please:
436
441
 
437
442
  ## Acknowledgments
438
443
 
439
- browserclaw is extracted and refined from the browser automation module in [OpenClaw](https://github.com/openclaw/openclaw) by [Peter Steinberger](https://github.com/steipete). The snapshot + ref system, CDP connection management, and Playwright integration originate from that project.
444
+ browserclaw is extracted and refined from the browser automation module in [OpenClaw](https://github.com/openclaw/openclaw), built by [Peter Steinberger](https://github.com/steipete) and an [amazing community of contributors](https://github.com/openclaw/openclaw?tab=readme-ov-file#community). The snapshot + ref system, CDP connection management, and Playwright integration originate from that project.
440
445
 
441
446
  ## License
442
447
 
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((resolve, reject) => {
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(() => resolve());
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 res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal });
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 res = await fetch(`${cdpUrl.replace(/\/+$/, "")}/json/version`, { signal: ctrl.signal });
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;
@@ -391,6 +396,7 @@ async function launchChrome(opts = {}) {
391
396
  "--disable-background-networking",
392
397
  "--disable-component-update",
393
398
  "--disable-features=Translate,MediaRouter",
399
+ "--disable-blink-features=AutomationControlled",
394
400
  "--disable-session-crashed-bubble",
395
401
  "--hide-crash-restore-bubble",
396
402
  "--password-store=basic"
@@ -575,11 +581,26 @@ function ensurePageState(page) {
575
581
  }
576
582
  return state;
577
583
  }
584
+ var STEALTH_SCRIPT = `Object.defineProperty(navigator, 'webdriver', { get: () => undefined })`;
585
+ function applyStealthToPage(page) {
586
+ page.evaluate(STEALTH_SCRIPT).catch((e) => {
587
+ if (process.env.DEBUG) console.warn("[browserclaw] stealth evaluate failed:", e.message);
588
+ });
589
+ }
578
590
  function observeContext(context) {
579
591
  if (observedContexts.has(context)) return;
580
592
  observedContexts.add(context);
581
- for (const page of context.pages()) ensurePageState(page);
582
- context.on("page", (page) => ensurePageState(page));
593
+ context.addInitScript(STEALTH_SCRIPT).catch((e) => {
594
+ if (process.env.DEBUG) console.warn("[browserclaw] stealth initScript failed:", e.message);
595
+ });
596
+ for (const page of context.pages()) {
597
+ ensurePageState(page);
598
+ applyStealthToPage(page);
599
+ }
600
+ context.on("page", (page) => {
601
+ ensurePageState(page);
602
+ applyStealthToPage(page);
603
+ });
583
604
  }
584
605
  function observeBrowser(browser) {
585
606
  for (const context of browser.contexts()) observeContext(context);
@@ -613,7 +634,7 @@ function restoreRoleRefsForTarget(opts) {
613
634
  state.roleRefsFrameSelector = entry.frameSelector;
614
635
  state.roleRefsMode = entry.mode;
615
636
  }
616
- async function connectBrowser(cdpUrl) {
637
+ async function connectBrowser(cdpUrl, authToken) {
617
638
  const normalized = normalizeCdpUrl(cdpUrl);
618
639
  if (cached?.cdpUrl === normalized) return cached;
619
640
  const existing = connectingByUrl.get(normalized);
@@ -623,9 +644,11 @@ async function connectBrowser(cdpUrl) {
623
644
  for (let attempt = 0; attempt < 3; attempt++) {
624
645
  try {
625
646
  const timeout = 5e3 + attempt * 2e3;
626
- const endpoint = await getChromeWebSocketUrl(normalized, timeout).catch(() => null) ?? normalized;
627
- const browser = await playwrightCore.chromium.connectOverCDP(endpoint, { timeout });
628
- const connected = { browser, cdpUrl: normalized };
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 };
629
652
  cached = connected;
630
653
  observeBrowser(browser);
631
654
  browser.on("disconnected", () => {
@@ -685,7 +708,9 @@ async function findPageByTargetId(browser, targetId, cdpUrl) {
685
708
  if (cdpUrl) {
686
709
  try {
687
710
  const listUrl = `${cdpUrl.replace(/\/+$/, "").replace(/^ws:/, "http:").replace(/\/cdp$/, "")}/json/list`;
688
- const response = await fetch(listUrl);
711
+ const headers = {};
712
+ if (cached?.authToken) headers["Authorization"] = `Bearer ${cached.authToken}`;
713
+ const response = await fetch(listUrl, { headers });
689
714
  if (response.ok) {
690
715
  const targets = await response.json();
691
716
  const target = targets.find((t) => t.id === targetId);
@@ -1010,6 +1035,7 @@ async function snapshotAi(opts) {
1010
1035
  if (!maybe._snapshotForAI) {
1011
1036
  throw new Error("Playwright _snapshotForAI is not available. Upgrade playwright-core to >= 1.50.");
1012
1037
  }
1038
+ const sourceUrl = page.url();
1013
1039
  const result = await maybe._snapshotForAI({
1014
1040
  timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3, 6e4),
1015
1041
  track: "response"
@@ -1035,7 +1061,13 @@ async function snapshotAi(opts) {
1035
1061
  return {
1036
1062
  snapshot: built.snapshot,
1037
1063
  refs: built.refs,
1038
- stats: getRoleSnapshotStats(built.snapshot, built.refs)
1064
+ stats: getRoleSnapshotStats(built.snapshot, built.refs),
1065
+ untrusted: true,
1066
+ contentMeta: {
1067
+ sourceUrl,
1068
+ contentType: "browser-snapshot",
1069
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
1070
+ }
1039
1071
  };
1040
1072
  }
1041
1073
 
@@ -1043,6 +1075,7 @@ async function snapshotAi(opts) {
1043
1075
  async function snapshotRole(opts) {
1044
1076
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1045
1077
  ensurePageState(page);
1078
+ const sourceUrl = page.url();
1046
1079
  const frameSelector = opts.frameSelector?.trim() || "";
1047
1080
  const selector = opts.selector?.trim() || "";
1048
1081
  const locator = frameSelector ? selector ? page.frameLocator(frameSelector).locator(selector) : page.frameLocator(frameSelector).locator(":root") : selector ? page.locator(selector) : page.locator(":root");
@@ -1059,19 +1092,34 @@ async function snapshotRole(opts) {
1059
1092
  return {
1060
1093
  snapshot: built.snapshot,
1061
1094
  refs: built.refs,
1062
- stats: getRoleSnapshotStats(built.snapshot, built.refs)
1095
+ stats: getRoleSnapshotStats(built.snapshot, built.refs),
1096
+ untrusted: true,
1097
+ contentMeta: {
1098
+ sourceUrl,
1099
+ contentType: "browser-snapshot",
1100
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
1101
+ }
1063
1102
  };
1064
1103
  }
1065
1104
  async function snapshotAria(opts) {
1066
1105
  const limit = Math.max(1, Math.min(2e3, Math.floor(opts.limit ?? 500)));
1067
1106
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1068
1107
  ensurePageState(page);
1108
+ const sourceUrl = page.url();
1069
1109
  const session = await page.context().newCDPSession(page);
1070
1110
  try {
1071
1111
  await session.send("Accessibility.enable").catch(() => {
1072
1112
  });
1073
1113
  const res = await session.send("Accessibility.getFullAXTree");
1074
- return { nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit) };
1114
+ return {
1115
+ nodes: formatAriaNodes(Array.isArray(res?.nodes) ? res.nodes : [], limit),
1116
+ untrusted: true,
1117
+ contentMeta: {
1118
+ sourceUrl,
1119
+ contentType: "browser-aria-tree",
1120
+ capturedAt: (/* @__PURE__ */ new Date()).toISOString()
1121
+ }
1122
+ };
1075
1123
  } finally {
1076
1124
  await session.detach().catch(() => {
1077
1125
  });
@@ -1262,7 +1310,7 @@ async function armDialogViaPlaywright(opts) {
1262
1310
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1263
1311
  ensurePageState(page);
1264
1312
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
1265
- return new Promise((resolve, reject) => {
1313
+ return new Promise((resolve2, reject) => {
1266
1314
  const timer = setTimeout(() => {
1267
1315
  page.removeListener("dialog", handler);
1268
1316
  reject(new Error(`No dialog appeared within ${timeout}ms`));
@@ -1275,7 +1323,7 @@ async function armDialogViaPlaywright(opts) {
1275
1323
  } else {
1276
1324
  await dialog.dismiss();
1277
1325
  }
1278
- resolve();
1326
+ resolve2();
1279
1327
  } catch (err) {
1280
1328
  reject(err);
1281
1329
  }
@@ -1287,7 +1335,7 @@ async function armFileUploadViaPlaywright(opts) {
1287
1335
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1288
1336
  ensurePageState(page);
1289
1337
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
1290
- return new Promise((resolve, reject) => {
1338
+ return new Promise((resolve2, reject) => {
1291
1339
  const timer = setTimeout(() => {
1292
1340
  page.removeListener("filechooser", handler);
1293
1341
  reject(new Error(`No file chooser appeared within ${timeout}ms`));
@@ -1296,7 +1344,7 @@ async function armFileUploadViaPlaywright(opts) {
1296
1344
  clearTimeout(timer);
1297
1345
  try {
1298
1346
  await fc.setFiles(opts.paths ?? []);
1299
- resolve();
1347
+ resolve2();
1300
1348
  } catch (err) {
1301
1349
  reject(err);
1302
1350
  }
@@ -1313,11 +1361,82 @@ async function pressKeyViaPlaywright(opts) {
1313
1361
  ensurePageState(page);
1314
1362
  await page.keyboard.press(key, { delay: Math.max(0, Math.floor(opts.delayMs ?? 0)) });
1315
1363
  }
1364
+ function assertSafeOutputPath(path2, allowedRoots) {
1365
+ if (!path2 || typeof path2 !== "string") {
1366
+ throw new Error("Output path is required.");
1367
+ }
1368
+ const normalized = path.normalize(path2);
1369
+ if (normalized.includes("..")) {
1370
+ throw new Error(`Unsafe output path: directory traversal detected in "${path2}".`);
1371
+ }
1372
+ if (allowedRoots?.length) {
1373
+ const resolved = path.resolve(normalized);
1374
+ const withinRoot = allowedRoots.some((root) => {
1375
+ const normalizedRoot = path.resolve(root);
1376
+ return resolved === normalizedRoot || resolved.startsWith(normalizedRoot + path.sep);
1377
+ });
1378
+ if (!withinRoot) {
1379
+ throw new Error(`Unsafe output path: "${path2}" is outside allowed directories.`);
1380
+ }
1381
+ }
1382
+ }
1383
+ function isInternalIP(ip) {
1384
+ if (/^127\./.test(ip)) return true;
1385
+ if (/^10\./.test(ip)) return true;
1386
+ if (/^172\.(1[6-9]|2\d|3[01])\./.test(ip)) return true;
1387
+ if (/^192\.168\./.test(ip)) return true;
1388
+ if (/^169\.254\./.test(ip)) return true;
1389
+ if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
1390
+ if (ip === "0.0.0.0") return true;
1391
+ const lower = ip.toLowerCase();
1392
+ if (lower === "::1") return true;
1393
+ if (lower.startsWith("fe80:")) return true;
1394
+ if (lower.startsWith("fc") || lower.startsWith("fd")) return true;
1395
+ if (lower.startsWith("::ffff:")) {
1396
+ const v4 = lower.replace(/^::ffff:/, "");
1397
+ return isInternalIP(v4);
1398
+ }
1399
+ return false;
1400
+ }
1401
+ function isInternalUrl(url) {
1402
+ let parsed;
1403
+ try {
1404
+ parsed = new URL(url);
1405
+ } catch {
1406
+ return true;
1407
+ }
1408
+ const hostname = parsed.hostname.toLowerCase();
1409
+ if (hostname === "localhost") return true;
1410
+ if (isInternalIP(hostname)) return true;
1411
+ if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
1412
+ return true;
1413
+ }
1414
+ return false;
1415
+ }
1416
+ async function isInternalUrlResolved(url) {
1417
+ if (isInternalUrl(url)) return true;
1418
+ let parsed;
1419
+ try {
1420
+ parsed = new URL(url);
1421
+ } catch {
1422
+ return true;
1423
+ }
1424
+ try {
1425
+ const { address } = await promises.lookup(parsed.hostname);
1426
+ if (isInternalIP(address)) return true;
1427
+ } catch {
1428
+ return true;
1429
+ }
1430
+ return false;
1431
+ }
1316
1432
 
1317
1433
  // src/actions/navigation.ts
1318
1434
  async function navigateViaPlaywright(opts) {
1319
1435
  const url = String(opts.url ?? "").trim();
1320
1436
  if (!url) throw new Error("url is required");
1437
+ if (!opts.allowInternal && await isInternalUrlResolved(url)) {
1438
+ throw new Error(`Navigation to internal/loopback address blocked: "${url}". Set allowInternal: true if this is intentional.`);
1439
+ }
1321
1440
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1322
1441
  ensurePageState(page);
1323
1442
  await page.goto(url, { timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
@@ -1339,11 +1458,14 @@ async function listPagesViaPlaywright(opts) {
1339
1458
  return results;
1340
1459
  }
1341
1460
  async function createPageViaPlaywright(opts) {
1461
+ const targetUrl = (opts.url ?? "").trim() || "about:blank";
1462
+ if (targetUrl !== "about:blank" && !opts.allowInternal && await isInternalUrlResolved(targetUrl)) {
1463
+ throw new Error(`Navigation to internal/loopback address blocked: "${targetUrl}". Set allowInternal: true if this is intentional.`);
1464
+ }
1342
1465
  const { browser } = await connectBrowser(opts.cdpUrl);
1343
1466
  const context = browser.contexts()[0] ?? await browser.newContext();
1344
1467
  const page = await context.newPage();
1345
1468
  ensurePageState(page);
1346
- const targetUrl = (opts.url ?? "").trim() || "about:blank";
1347
1469
  if (targetUrl !== "about:blank") {
1348
1470
  await page.goto(targetUrl, { timeout: normalizeTimeoutMs(void 0, 2e4) });
1349
1471
  }
@@ -1489,6 +1611,7 @@ async function evaluateViaPlaywright(opts) {
1489
1611
 
1490
1612
  // src/actions/download.ts
1491
1613
  async function downloadViaPlaywright(opts) {
1614
+ assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
1492
1615
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1493
1616
  ensurePageState(page);
1494
1617
  restoreRoleRefsForTarget({ cdpUrl: opts.cdpUrl, targetId: opts.targetId, page });
@@ -1515,6 +1638,7 @@ async function waitForDownloadViaPlaywright(opts) {
1515
1638
  const timeout = normalizeTimeoutMs(opts.timeoutMs, 3e4, 12e4);
1516
1639
  const download = await page.waitForEvent("download", { timeout });
1517
1640
  const savePath = opts.path ?? download.suggestedFilename();
1641
+ assertSafeOutputPath(savePath, opts.allowedOutputRoots);
1518
1642
  await download.saveAs(savePath);
1519
1643
  return {
1520
1644
  url: download.url(),
@@ -1698,6 +1822,7 @@ async function traceStartViaPlaywright(opts) {
1698
1822
  });
1699
1823
  }
1700
1824
  async function traceStopViaPlaywright(opts) {
1825
+ assertSafeOutputPath(opts.path, opts.allowedOutputRoots);
1701
1826
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1702
1827
  ensurePageState(page);
1703
1828
  const context = page.context();
@@ -1843,10 +1968,12 @@ async function storageClearViaPlaywright(opts) {
1843
1968
  var CrawlPage = class {
1844
1969
  cdpUrl;
1845
1970
  targetId;
1971
+ allowInternal;
1846
1972
  /** @internal */
1847
- constructor(cdpUrl, targetId) {
1973
+ constructor(cdpUrl, targetId, allowInternal = false) {
1848
1974
  this.cdpUrl = cdpUrl;
1849
1975
  this.targetId = targetId;
1976
+ this.allowInternal = allowInternal;
1850
1977
  }
1851
1978
  /** The CDP target ID for this page. Use this to identify the page in multi-tab scenarios. */
1852
1979
  get id() {
@@ -2178,7 +2305,8 @@ var CrawlPage = class {
2178
2305
  cdpUrl: this.cdpUrl,
2179
2306
  targetId: this.targetId,
2180
2307
  url,
2181
- timeoutMs: opts?.timeoutMs
2308
+ timeoutMs: opts?.timeoutMs,
2309
+ allowInternal: this.allowInternal
2182
2310
  });
2183
2311
  }
2184
2312
  /**
@@ -2365,12 +2493,14 @@ var CrawlPage = class {
2365
2493
  * Stop recording a trace and save it to a file.
2366
2494
  *
2367
2495
  * @param path - File path to save the trace (e.g. `'trace.zip'`)
2496
+ * @param opts - Options (allowedOutputRoots: constrain output to specific directories)
2368
2497
  */
2369
- async traceStop(path2) {
2498
+ async traceStop(path2, opts) {
2370
2499
  return traceStopViaPlaywright({
2371
2500
  cdpUrl: this.cdpUrl,
2372
2501
  targetId: this.targetId,
2373
- path: path2
2502
+ path: path2,
2503
+ allowedOutputRoots: opts?.allowedOutputRoots
2374
2504
  });
2375
2505
  }
2376
2506
  /**
@@ -2558,7 +2688,8 @@ var CrawlPage = class {
2558
2688
  targetId: this.targetId,
2559
2689
  ref,
2560
2690
  path: path2,
2561
- timeoutMs: opts?.timeoutMs
2691
+ timeoutMs: opts?.timeoutMs,
2692
+ allowedOutputRoots: opts?.allowedOutputRoots
2562
2693
  });
2563
2694
  }
2564
2695
  /**
@@ -2574,7 +2705,8 @@ var CrawlPage = class {
2574
2705
  cdpUrl: this.cdpUrl,
2575
2706
  targetId: this.targetId,
2576
2707
  path: opts?.path,
2577
- timeoutMs: opts?.timeoutMs
2708
+ timeoutMs: opts?.timeoutMs,
2709
+ allowedOutputRoots: opts?.allowedOutputRoots
2578
2710
  });
2579
2711
  }
2580
2712
  // ── Emulation ───────────────────────────────────────────────
@@ -2704,10 +2836,12 @@ var CrawlPage = class {
2704
2836
  };
2705
2837
  var BrowserClaw = class _BrowserClaw {
2706
2838
  cdpUrl;
2839
+ allowInternal;
2707
2840
  chrome;
2708
- constructor(cdpUrl, chrome) {
2841
+ constructor(cdpUrl, chrome, allowInternal = false) {
2709
2842
  this.cdpUrl = cdpUrl;
2710
2843
  this.chrome = chrome;
2844
+ this.allowInternal = allowInternal;
2711
2845
  }
2712
2846
  /**
2713
2847
  * Launch a new Chrome instance and connect to it.
@@ -2735,7 +2869,7 @@ var BrowserClaw = class _BrowserClaw {
2735
2869
  static async launch(opts = {}) {
2736
2870
  const chrome = await launchChrome(opts);
2737
2871
  const cdpUrl = `http://127.0.0.1:${chrome.cdpPort}`;
2738
- return new _BrowserClaw(cdpUrl, chrome);
2872
+ return new _BrowserClaw(cdpUrl, chrome, opts.allowInternal);
2739
2873
  }
2740
2874
  /**
2741
2875
  * Connect to an already-running Chrome instance via its CDP endpoint.
@@ -2751,12 +2885,12 @@ var BrowserClaw = class _BrowserClaw {
2751
2885
  * const browser = await BrowserClaw.connect('http://localhost:9222');
2752
2886
  * ```
2753
2887
  */
2754
- static async connect(cdpUrl) {
2755
- if (!await isChromeReachable(cdpUrl, 3e3)) {
2888
+ static async connect(cdpUrl, opts) {
2889
+ if (!await isChromeReachable(cdpUrl, 3e3, opts?.authToken)) {
2756
2890
  throw new Error(`Cannot connect to Chrome at ${cdpUrl}. Is Chrome running with --remote-debugging-port?`);
2757
2891
  }
2758
- await connectBrowser(cdpUrl);
2759
- return new _BrowserClaw(cdpUrl, null);
2892
+ await connectBrowser(cdpUrl, opts?.authToken);
2893
+ return new _BrowserClaw(cdpUrl, null, opts?.allowInternal);
2760
2894
  }
2761
2895
  /**
2762
2896
  * Open a URL in a new tab and return the page handle.
@@ -2771,8 +2905,8 @@ var BrowserClaw = class _BrowserClaw {
2771
2905
  * ```
2772
2906
  */
2773
2907
  async open(url) {
2774
- const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url });
2775
- return new CrawlPage(this.cdpUrl, tab.targetId);
2908
+ const tab = await createPageViaPlaywright({ cdpUrl: this.cdpUrl, url, allowInternal: this.allowInternal });
2909
+ return new CrawlPage(this.cdpUrl, tab.targetId, this.allowInternal);
2776
2910
  }
2777
2911
  /**
2778
2912
  * Get a CrawlPage handle for the currently active tab.
@@ -2785,7 +2919,7 @@ var BrowserClaw = class _BrowserClaw {
2785
2919
  if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
2786
2920
  const tid = await pageTargetId(pages[0]).catch(() => null);
2787
2921
  if (!tid) throw new Error("Failed to get targetId for the current page.");
2788
- return new CrawlPage(this.cdpUrl, tid);
2922
+ return new CrawlPage(this.cdpUrl, tid, this.allowInternal);
2789
2923
  }
2790
2924
  /**
2791
2925
  * List all open tabs.
@@ -2820,7 +2954,7 @@ var BrowserClaw = class _BrowserClaw {
2820
2954
  * @returns CrawlPage for the specified tab
2821
2955
  */
2822
2956
  page(targetId) {
2823
- return new CrawlPage(this.cdpUrl, targetId);
2957
+ return new CrawlPage(this.cdpUrl, targetId, this.allowInternal);
2824
2958
  }
2825
2959
  /** The CDP endpoint URL for this browser connection. */
2826
2960
  get url() {