browserclaw 0.11.6 → 0.12.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 +208 -9
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +131 -2
- package/dist/index.d.ts +131 -2
- package/dist/index.js +208 -9
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/dist/index.cjs
CHANGED
|
@@ -1573,6 +1573,7 @@ ${stderrOutput.slice(0, 2e3)}` : "";
|
|
|
1573
1573
|
userDataDir,
|
|
1574
1574
|
cdpPort,
|
|
1575
1575
|
startedAt,
|
|
1576
|
+
launchMs: Date.now() - startedAt,
|
|
1576
1577
|
proc
|
|
1577
1578
|
};
|
|
1578
1579
|
}
|
|
@@ -6327,6 +6328,137 @@ var CrawlPage = class {
|
|
|
6327
6328
|
pollMs: opts?.pollMs
|
|
6328
6329
|
});
|
|
6329
6330
|
}
|
|
6331
|
+
// ── Auth Health ──────────────────────────────────────────────
|
|
6332
|
+
/**
|
|
6333
|
+
* Check whether the current page session appears authenticated.
|
|
6334
|
+
*
|
|
6335
|
+
* Evaluates one or more rules against the page state. All rules must pass
|
|
6336
|
+
* for `authenticated` to be `true`. Returns per-rule details for debugging.
|
|
6337
|
+
*
|
|
6338
|
+
* @param rules - Array of auth check rules (url, cookie, selector, text, textGone, fn)
|
|
6339
|
+
* @returns Authentication status and per-rule check details
|
|
6340
|
+
*
|
|
6341
|
+
* @example
|
|
6342
|
+
* ```ts
|
|
6343
|
+
* // Check by URL and absence of login text
|
|
6344
|
+
* const result = await page.isAuthenticated([
|
|
6345
|
+
* { url: '/dashboard' },
|
|
6346
|
+
* { textGone: 'Sign in' },
|
|
6347
|
+
* ]);
|
|
6348
|
+
* if (!result.authenticated) {
|
|
6349
|
+
* console.log('Auth failed:', result.checks.filter(c => !c.passed));
|
|
6350
|
+
* }
|
|
6351
|
+
*
|
|
6352
|
+
* // Check by cookie presence
|
|
6353
|
+
* const result = await page.isAuthenticated([{ cookie: 'session_id' }]);
|
|
6354
|
+
*
|
|
6355
|
+
* // Check with custom JS function
|
|
6356
|
+
* const result = await page.isAuthenticated([
|
|
6357
|
+
* { fn: '() => !!document.querySelector("[data-user-id]")' },
|
|
6358
|
+
* ]);
|
|
6359
|
+
* ```
|
|
6360
|
+
*/
|
|
6361
|
+
async isAuthenticated(rules) {
|
|
6362
|
+
if (!rules.length) return { authenticated: true, checks: [] };
|
|
6363
|
+
const page = await getRestoredPageForTarget({ cdpUrl: this.cdpUrl, targetId: this.targetId });
|
|
6364
|
+
const checks = [];
|
|
6365
|
+
for (const rule of rules) {
|
|
6366
|
+
if (rule.url !== void 0) {
|
|
6367
|
+
const currentUrl = page.url();
|
|
6368
|
+
const passed = currentUrl.includes(rule.url);
|
|
6369
|
+
checks.push({ rule: "url", passed, detail: passed ? currentUrl : `expected "${rule.url}" in "${currentUrl}"` });
|
|
6370
|
+
}
|
|
6371
|
+
if (rule.cookie !== void 0) {
|
|
6372
|
+
const cookies = await page.context().cookies();
|
|
6373
|
+
const found = cookies.some((c) => c.name === rule.cookie && c.value !== "");
|
|
6374
|
+
checks.push({
|
|
6375
|
+
rule: "cookie",
|
|
6376
|
+
passed: found,
|
|
6377
|
+
detail: found ? `cookie "${rule.cookie}" present` : `cookie "${rule.cookie}" missing or empty`
|
|
6378
|
+
});
|
|
6379
|
+
}
|
|
6380
|
+
if (rule.selector !== void 0) {
|
|
6381
|
+
try {
|
|
6382
|
+
const count = await page.locator(rule.selector).count();
|
|
6383
|
+
const passed = count > 0;
|
|
6384
|
+
checks.push({
|
|
6385
|
+
rule: "selector",
|
|
6386
|
+
passed,
|
|
6387
|
+
detail: passed ? `"${rule.selector}" found (${String(count)})` : `"${rule.selector}" not found`
|
|
6388
|
+
});
|
|
6389
|
+
} catch (err) {
|
|
6390
|
+
console.warn(
|
|
6391
|
+
`[browserclaw] isAuthenticated selector check failed: ${err instanceof Error ? err.message : String(err)}`
|
|
6392
|
+
);
|
|
6393
|
+
checks.push({ rule: "selector", passed: false, detail: `"${rule.selector}" error during evaluation` });
|
|
6394
|
+
}
|
|
6395
|
+
}
|
|
6396
|
+
if (rule.text !== void 0 || rule.textGone !== void 0) {
|
|
6397
|
+
let bodyText = null;
|
|
6398
|
+
try {
|
|
6399
|
+
const raw = await evaluateViaPlaywright({
|
|
6400
|
+
cdpUrl: this.cdpUrl,
|
|
6401
|
+
targetId: this.targetId,
|
|
6402
|
+
fn: '() => { const b = document.body; return b ? b.innerText : ""; }'
|
|
6403
|
+
});
|
|
6404
|
+
bodyText = typeof raw === "string" ? raw : null;
|
|
6405
|
+
} catch (err) {
|
|
6406
|
+
console.warn(
|
|
6407
|
+
`[browserclaw] isAuthenticated body text fetch failed: ${err instanceof Error ? err.message : String(err)}`
|
|
6408
|
+
);
|
|
6409
|
+
}
|
|
6410
|
+
if (rule.text !== void 0) {
|
|
6411
|
+
if (bodyText === null) {
|
|
6412
|
+
checks.push({ rule: "text", passed: false, detail: `"${rule.text}" error during evaluation` });
|
|
6413
|
+
} else {
|
|
6414
|
+
const passed = bodyText.includes(rule.text);
|
|
6415
|
+
checks.push({
|
|
6416
|
+
rule: "text",
|
|
6417
|
+
passed,
|
|
6418
|
+
detail: passed ? `"${rule.text}" found` : `"${rule.text}" not found in page text`
|
|
6419
|
+
});
|
|
6420
|
+
}
|
|
6421
|
+
}
|
|
6422
|
+
if (rule.textGone !== void 0) {
|
|
6423
|
+
if (bodyText === null) {
|
|
6424
|
+
checks.push({ rule: "textGone", passed: false, detail: `"${rule.textGone}" error during evaluation` });
|
|
6425
|
+
} else {
|
|
6426
|
+
const passed = !bodyText.includes(rule.textGone);
|
|
6427
|
+
checks.push({
|
|
6428
|
+
rule: "textGone",
|
|
6429
|
+
passed,
|
|
6430
|
+
detail: passed ? `"${rule.textGone}" absent (good)` : `"${rule.textGone}" still present`
|
|
6431
|
+
});
|
|
6432
|
+
}
|
|
6433
|
+
}
|
|
6434
|
+
}
|
|
6435
|
+
if (rule.fn !== void 0) {
|
|
6436
|
+
try {
|
|
6437
|
+
const result = await evaluateViaPlaywright({
|
|
6438
|
+
cdpUrl: this.cdpUrl,
|
|
6439
|
+
targetId: this.targetId,
|
|
6440
|
+
fn: rule.fn
|
|
6441
|
+
});
|
|
6442
|
+
const passed = result !== null && result !== void 0 && result !== false && result !== 0 && result !== "";
|
|
6443
|
+
checks.push({
|
|
6444
|
+
rule: "fn",
|
|
6445
|
+
passed,
|
|
6446
|
+
detail: passed ? "function returned truthy" : `function returned ${JSON.stringify(result)}`
|
|
6447
|
+
});
|
|
6448
|
+
} catch (err) {
|
|
6449
|
+
checks.push({
|
|
6450
|
+
rule: "fn",
|
|
6451
|
+
passed: false,
|
|
6452
|
+
detail: `function threw: ${err instanceof Error ? err.message : String(err)}`
|
|
6453
|
+
});
|
|
6454
|
+
}
|
|
6455
|
+
}
|
|
6456
|
+
}
|
|
6457
|
+
return {
|
|
6458
|
+
authenticated: checks.length > 0 && checks.every((c) => c.passed),
|
|
6459
|
+
checks
|
|
6460
|
+
};
|
|
6461
|
+
}
|
|
6330
6462
|
// ── Playwright Escape Hatches ─────────────────────────────────
|
|
6331
6463
|
/**
|
|
6332
6464
|
* Get the underlying Playwright `Page` object for this tab.
|
|
@@ -6385,9 +6517,11 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6385
6517
|
ssrfPolicy;
|
|
6386
6518
|
recordVideo;
|
|
6387
6519
|
chrome;
|
|
6388
|
-
|
|
6520
|
+
_telemetry;
|
|
6521
|
+
constructor(cdpUrl, chrome, telemetry, ssrfPolicy, recordVideo) {
|
|
6389
6522
|
this.cdpUrl = cdpUrl;
|
|
6390
6523
|
this.chrome = chrome;
|
|
6524
|
+
this._telemetry = telemetry;
|
|
6391
6525
|
this.ssrfPolicy = ssrfPolicy;
|
|
6392
6526
|
this.recordVideo = recordVideo;
|
|
6393
6527
|
}
|
|
@@ -6416,13 +6550,21 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6416
6550
|
* ```
|
|
6417
6551
|
*/
|
|
6418
6552
|
static async launch(opts = {}) {
|
|
6553
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6419
6554
|
const chrome = await launchChrome(opts);
|
|
6420
6555
|
const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
|
|
6421
6556
|
const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
6422
|
-
const
|
|
6557
|
+
const telemetry = {
|
|
6558
|
+
launchMs: chrome.launchMs,
|
|
6559
|
+
timestamps: { startedAt, launchedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
6560
|
+
};
|
|
6561
|
+
const browser = new _BrowserClaw(cdpUrl, chrome, telemetry, ssrfPolicy, opts.recordVideo);
|
|
6423
6562
|
if (opts.url !== void 0 && opts.url !== "") {
|
|
6424
6563
|
const page = await browser.currentPage();
|
|
6564
|
+
const navT0 = Date.now();
|
|
6425
6565
|
await page.goto(opts.url);
|
|
6566
|
+
telemetry.navMs = Date.now() - navT0;
|
|
6567
|
+
telemetry.timestamps.navigatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6426
6568
|
}
|
|
6427
6569
|
return browser;
|
|
6428
6570
|
}
|
|
@@ -6441,6 +6583,8 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6441
6583
|
* ```
|
|
6442
6584
|
*/
|
|
6443
6585
|
static async connect(cdpUrl, opts) {
|
|
6586
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6587
|
+
const connectT0 = Date.now();
|
|
6444
6588
|
let resolvedUrl = cdpUrl;
|
|
6445
6589
|
if (resolvedUrl === void 0 || resolvedUrl === "") {
|
|
6446
6590
|
const discovered = await discoverChromeCdpUrl();
|
|
@@ -6456,7 +6600,11 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6456
6600
|
}
|
|
6457
6601
|
await connectBrowser(resolvedUrl, opts?.authToken);
|
|
6458
6602
|
const ssrfPolicy = opts?.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
6459
|
-
|
|
6603
|
+
const telemetry = {
|
|
6604
|
+
connectMs: Date.now() - connectT0,
|
|
6605
|
+
timestamps: { startedAt, connectedAt: (/* @__PURE__ */ new Date()).toISOString() }
|
|
6606
|
+
};
|
|
6607
|
+
return new _BrowserClaw(resolvedUrl, null, telemetry, ssrfPolicy, opts?.recordVideo);
|
|
6460
6608
|
}
|
|
6461
6609
|
/**
|
|
6462
6610
|
* Open a URL in a new tab and return the page handle.
|
|
@@ -6485,7 +6633,12 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6485
6633
|
* @returns CrawlPage for the first/active page
|
|
6486
6634
|
*/
|
|
6487
6635
|
async currentPage() {
|
|
6636
|
+
const connectT0 = Date.now();
|
|
6488
6637
|
const { browser } = await connectBrowser(this.cdpUrl);
|
|
6638
|
+
if (this._telemetry.connectMs === void 0) {
|
|
6639
|
+
this._telemetry.connectMs = Date.now() - connectT0;
|
|
6640
|
+
this._telemetry.timestamps.connectedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6641
|
+
}
|
|
6489
6642
|
const pages = getAllPages(browser);
|
|
6490
6643
|
if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
|
|
6491
6644
|
const tid = await pageTargetId(pages[0]).catch(() => null);
|
|
@@ -6562,15 +6715,61 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6562
6715
|
* If the browser was launched by `BrowserClaw.launch()`, the Chrome process
|
|
6563
6716
|
* will be terminated. If connected via `BrowserClaw.connect()`, only the
|
|
6564
6717
|
* Playwright connection is closed.
|
|
6718
|
+
*
|
|
6719
|
+
* @param exitReason - Optional structured reason for stopping (e.g. `'success'`, `'auth_failed'`, `'timeout'`)
|
|
6565
6720
|
*/
|
|
6566
|
-
async stop() {
|
|
6567
|
-
|
|
6568
|
-
|
|
6569
|
-
|
|
6570
|
-
|
|
6571
|
-
|
|
6721
|
+
async stop(exitReason) {
|
|
6722
|
+
this._telemetry.timestamps.stoppedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6723
|
+
if (exitReason !== void 0) this._telemetry.exitReason = exitReason;
|
|
6724
|
+
try {
|
|
6725
|
+
clearRecordingContext(this.cdpUrl);
|
|
6726
|
+
await disconnectBrowser();
|
|
6727
|
+
if (this.chrome) {
|
|
6728
|
+
await stopChrome(this.chrome);
|
|
6729
|
+
this.chrome = null;
|
|
6730
|
+
}
|
|
6731
|
+
this._telemetry.cleanupOk = true;
|
|
6732
|
+
} catch (err) {
|
|
6733
|
+
this._telemetry.cleanupOk = false;
|
|
6734
|
+
throw err;
|
|
6572
6735
|
}
|
|
6573
6736
|
}
|
|
6737
|
+
/**
|
|
6738
|
+
* Get structured telemetry for this browser session.
|
|
6739
|
+
*
|
|
6740
|
+
* Returns timing data, timestamps, and exit information collected
|
|
6741
|
+
* throughout the session lifecycle. Useful for diagnosing startup
|
|
6742
|
+
* latency, auth failures, and cleanup issues in cron/unattended runs.
|
|
6743
|
+
*
|
|
6744
|
+
* @returns Telemetry envelope with launch/connect/nav timings and exit info
|
|
6745
|
+
*
|
|
6746
|
+
* @example
|
|
6747
|
+
* ```ts
|
|
6748
|
+
* const browser = await BrowserClaw.launch({ url: 'https://example.com' });
|
|
6749
|
+
* const page = await browser.currentPage();
|
|
6750
|
+
*
|
|
6751
|
+
* // ... do work ...
|
|
6752
|
+
*
|
|
6753
|
+
* const auth = await page.isAuthenticated([{ cookie: 'session' }]);
|
|
6754
|
+
* browser.recordAuthResult(auth.authenticated);
|
|
6755
|
+
*
|
|
6756
|
+
* await browser.stop(auth.authenticated ? 'success' : 'auth_failed');
|
|
6757
|
+
* console.log(browser.telemetry());
|
|
6758
|
+
* // { launchMs: 1823, connectMs: 45, navMs: 620, authOk: true,
|
|
6759
|
+
* // exitReason: 'success', cleanupOk: true, timestamps: { ... } }
|
|
6760
|
+
* ```
|
|
6761
|
+
*/
|
|
6762
|
+
telemetry() {
|
|
6763
|
+
return this._telemetry;
|
|
6764
|
+
}
|
|
6765
|
+
/**
|
|
6766
|
+
* Record the result of an authentication check in the telemetry envelope.
|
|
6767
|
+
*
|
|
6768
|
+
* @param ok - Whether authentication was successful
|
|
6769
|
+
*/
|
|
6770
|
+
recordAuthResult(ok) {
|
|
6771
|
+
this._telemetry.authOk = ok;
|
|
6772
|
+
}
|
|
6574
6773
|
};
|
|
6575
6774
|
|
|
6576
6775
|
exports.BlockedBrowserTargetError = BlockedBrowserTargetError;
|