browserclaw 0.3.8 → 0.3.10

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 CHANGED
@@ -382,6 +382,51 @@ async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
382
382
  clearTimeout(t);
383
383
  }
384
384
  }
385
+ async function isChromeCdpReady(cdpUrl, timeoutMs = 500, handshakeTimeoutMs = 800) {
386
+ const wsUrl = await getChromeWebSocketUrl(cdpUrl, timeoutMs);
387
+ if (!wsUrl) return false;
388
+ return await canRunCdpHealthCommand(wsUrl, handshakeTimeoutMs);
389
+ }
390
+ async function canRunCdpHealthCommand(wsUrl, timeoutMs = 800) {
391
+ return new Promise((resolve2) => {
392
+ let settled = false;
393
+ const finish = (value) => {
394
+ if (settled) return;
395
+ settled = true;
396
+ clearTimeout(timer);
397
+ try {
398
+ ws.close();
399
+ } catch {
400
+ }
401
+ resolve2(value);
402
+ };
403
+ const timer = setTimeout(() => finish(false), Math.max(50, timeoutMs + 25));
404
+ let ws;
405
+ try {
406
+ ws = new WebSocket(wsUrl);
407
+ } catch {
408
+ finish(false);
409
+ return;
410
+ }
411
+ ws.onopen = () => {
412
+ try {
413
+ ws.send(JSON.stringify({ id: 1, method: "Browser.getVersion" }));
414
+ } catch {
415
+ finish(false);
416
+ }
417
+ };
418
+ ws.onmessage = (event) => {
419
+ try {
420
+ const parsed = JSON.parse(String(event.data));
421
+ if (parsed?.id !== 1) return;
422
+ finish(Boolean(parsed.result && typeof parsed.result === "object"));
423
+ } catch {
424
+ }
425
+ };
426
+ ws.onerror = () => finish(false);
427
+ ws.onclose = () => finish(false);
428
+ });
429
+ }
385
430
  async function launchChrome(opts = {}) {
386
431
  const cdpPort = opts.cdpPort ?? DEFAULT_CDP_PORT;
387
432
  await ensurePortAvailable(cdpPort);
@@ -456,18 +501,30 @@ async function launchChrome(opts = {}) {
456
501
  }
457
502
  const proc = spawnChrome();
458
503
  const cdpUrl = `http://127.0.0.1:${cdpPort}`;
504
+ const stderrChunks = [];
505
+ const onStderr = (chunk) => {
506
+ stderrChunks.push(chunk);
507
+ };
508
+ proc.stderr?.on("data", onStderr);
459
509
  const readyDeadline = Date.now() + 15e3;
460
510
  while (Date.now() < readyDeadline) {
461
511
  if (await isChromeReachable(cdpUrl, 500)) break;
462
512
  await new Promise((r) => setTimeout(r, 200));
463
513
  }
464
514
  if (!await isChromeReachable(cdpUrl, 500)) {
515
+ const stderrOutput = Buffer.concat(stderrChunks).toString("utf8").trim();
516
+ const stderrHint = stderrOutput ? `
517
+ Chrome stderr:
518
+ ${stderrOutput.slice(0, 2e3)}` : "";
519
+ const sandboxHint = process.platform === "linux" && !opts.noSandbox ? "\nHint: If running in a container or as root, try setting noSandbox: true." : "";
465
520
  try {
466
521
  proc.kill("SIGKILL");
467
522
  } catch {
468
523
  }
469
- throw new Error(`Failed to start Chrome CDP on port ${cdpPort}. Chrome may not have started correctly.`);
524
+ throw new Error(`Failed to start Chrome CDP on port ${cdpPort}.${sandboxHint}${stderrHint}`);
470
525
  }
526
+ proc.stderr?.off("data", onStderr);
527
+ stderrChunks.length = 0;
471
528
  return {
472
529
  pid: proc.pid ?? -1,
473
530
  exe,
@@ -1232,7 +1289,8 @@ async function assertBrowserNavigationAllowed(opts) {
1232
1289
  const hostname = parsed.hostname.toLowerCase();
1233
1290
  if (allowedHostnames.some((h) => h.toLowerCase() === hostname)) return;
1234
1291
  }
1235
- if (await isInternalUrlResolved(rawUrl, opts.lookupFn)) {
1292
+ const ipOpts = { allowRfc2544BenchmarkRange: policy?.allowRfc2544BenchmarkRange };
1293
+ if (await isInternalUrlResolved(rawUrl, opts.lookupFn, ipOpts)) {
1236
1294
  throw new InvalidBrowserNavigationUrlError(
1237
1295
  `Navigation to internal/loopback address blocked: "${rawUrl}". ssrfPolicy.dangerouslyAllowPrivateNetwork is false (strict mode).`
1238
1296
  );
@@ -1347,7 +1405,7 @@ function isUnsupportedIPv4Literal(ip) {
1347
1405
  if (!parts.every(isStrictDecimalOctet)) return true;
1348
1406
  return false;
1349
1407
  }
1350
- function isInternalIP(ip) {
1408
+ function isInternalIP(ip, opts) {
1351
1409
  if (!ip.includes(":") && isUnsupportedIPv4Literal(ip)) return true;
1352
1410
  if (/^127\./.test(ip)) return true;
1353
1411
  if (/^10\./.test(ip)) return true;
@@ -1356,6 +1414,7 @@ function isInternalIP(ip) {
1356
1414
  if (/^169\.254\./.test(ip)) return true;
1357
1415
  if (/^100\.(6[4-9]|[7-9]\d|1[01]\d|12[0-7])\./.test(ip)) return true;
1358
1416
  if (ip === "0.0.0.0") return true;
1417
+ if (!opts?.allowRfc2544BenchmarkRange && /^198\.1[89]\./.test(ip)) return true;
1359
1418
  const lower = ip.toLowerCase();
1360
1419
  if (lower === "::1") return true;
1361
1420
  if (lower.startsWith("fe80:")) return true;
@@ -1364,11 +1423,11 @@ function isInternalIP(ip) {
1364
1423
  const embedded = extractEmbeddedIPv4(lower);
1365
1424
  if (embedded !== null) {
1366
1425
  if (embedded === "") return true;
1367
- return isInternalIP(embedded);
1426
+ return isInternalIP(embedded, opts);
1368
1427
  }
1369
1428
  return false;
1370
1429
  }
1371
- function isInternalUrl(url) {
1430
+ function isInternalUrl(url, opts) {
1372
1431
  let parsed;
1373
1432
  try {
1374
1433
  parsed = new URL(url);
@@ -1377,7 +1436,7 @@ function isInternalUrl(url) {
1377
1436
  }
1378
1437
  const hostname = parsed.hostname.toLowerCase();
1379
1438
  if (hostname === "localhost") return true;
1380
- if (isInternalIP(hostname)) return true;
1439
+ if (isInternalIP(hostname, opts)) return true;
1381
1440
  if (hostname.endsWith(".local") || hostname.endsWith(".internal") || hostname.endsWith(".localhost")) {
1382
1441
  return true;
1383
1442
  }
@@ -1399,8 +1458,8 @@ async function assertSafeUploadPaths(paths) {
1399
1458
  }
1400
1459
  }
1401
1460
  }
1402
- async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
1403
- if (isInternalUrl(url)) return true;
1461
+ async function isInternalUrlResolved(url, lookupFn = promises.lookup, opts) {
1462
+ if (isInternalUrl(url, opts)) return true;
1404
1463
  let parsed;
1405
1464
  try {
1406
1465
  parsed = new URL(url);
@@ -1409,12 +1468,25 @@ async function isInternalUrlResolved(url, lookupFn = promises.lookup) {
1409
1468
  }
1410
1469
  try {
1411
1470
  const { address } = await lookupFn(parsed.hostname);
1412
- if (isInternalIP(address)) return true;
1471
+ if (isInternalIP(address, opts)) return true;
1413
1472
  } catch {
1414
1473
  return true;
1415
1474
  }
1416
1475
  return false;
1417
1476
  }
1477
+ async function assertBrowserNavigationResultAllowed(opts) {
1478
+ const rawUrl = String(opts.url ?? "").trim();
1479
+ if (!rawUrl) return;
1480
+ let parsed;
1481
+ try {
1482
+ parsed = new URL(rawUrl);
1483
+ } catch {
1484
+ return;
1485
+ }
1486
+ if (NETWORK_NAVIGATION_PROTOCOLS.has(parsed.protocol) || SAFE_NON_NETWORK_URLS.has(parsed.href)) {
1487
+ await assertBrowserNavigationAllowed(opts);
1488
+ }
1489
+ }
1418
1490
 
1419
1491
  // src/actions/interaction.ts
1420
1492
  async function clickViaPlaywright(opts) {
@@ -1620,7 +1692,9 @@ async function navigateViaPlaywright(opts) {
1620
1692
  const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
1621
1693
  ensurePageState(page);
1622
1694
  await page.goto(url, { timeout: normalizeTimeoutMs(opts.timeoutMs, 2e4) });
1623
- return { url: page.url() };
1695
+ const finalUrl = page.url();
1696
+ await assertBrowserNavigationResultAllowed({ url: finalUrl, ssrfPolicy: policy });
1697
+ return { url: finalUrl };
1624
1698
  }
1625
1699
  async function listPagesViaPlaywright(opts) {
1626
1700
  const { browser } = await connectBrowser(opts.cdpUrl);
@@ -1639,8 +1713,8 @@ async function listPagesViaPlaywright(opts) {
1639
1713
  }
1640
1714
  async function createPageViaPlaywright(opts) {
1641
1715
  const targetUrl = (opts.url ?? "").trim() || "about:blank";
1716
+ const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
1642
1717
  if (targetUrl !== "about:blank") {
1643
- const policy = opts.allowInternal ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
1644
1718
  await assertBrowserNavigationAllowed({ url: targetUrl, ssrfPolicy: policy });
1645
1719
  }
1646
1720
  const { browser } = await connectBrowser(opts.cdpUrl);
@@ -1649,6 +1723,7 @@ async function createPageViaPlaywright(opts) {
1649
1723
  ensurePageState(page);
1650
1724
  if (targetUrl !== "about:blank") {
1651
1725
  await page.goto(targetUrl, { timeout: normalizeTimeoutMs(void 0, 2e4) });
1726
+ await assertBrowserNavigationResultAllowed({ url: page.url(), ssrfPolicy: policy });
1652
1727
  }
1653
1728
  const tid = await pageTargetId(page).catch(() => null);
1654
1729
  if (!tid) throw new Error("Failed to get targetId for new page");
@@ -3177,6 +3252,7 @@ exports.BrowserClaw = BrowserClaw;
3177
3252
  exports.CrawlPage = CrawlPage;
3178
3253
  exports.InvalidBrowserNavigationUrlError = InvalidBrowserNavigationUrlError;
3179
3254
  exports.assertBrowserNavigationAllowed = assertBrowserNavigationAllowed;
3255
+ exports.isChromeCdpReady = isChromeCdpReady;
3180
3256
  exports.withBrowserNavigationPolicy = withBrowserNavigationPolicy;
3181
3257
  //# sourceMappingURL=index.cjs.map
3182
3258
  //# sourceMappingURL=index.cjs.map