browserclaw 0.11.5 → 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/README.md +28 -28
- package/dist/index.cjs +247 -43
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +151 -22
- package/dist/index.d.ts +151 -22
- package/dist/index.js +249 -45
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -15,16 +15,16 @@ The AI-native browser automation library — born from [OpenClaw](https://github
|
|
|
15
15
|
```typescript
|
|
16
16
|
import { BrowserClaw } from 'browserclaw';
|
|
17
17
|
|
|
18
|
-
const browser = await BrowserClaw.launch({ url: 'https://
|
|
18
|
+
const browser = await BrowserClaw.launch({ url: 'https://demo.playwright.dev/todomvc' });
|
|
19
19
|
const page = await browser.currentPage();
|
|
20
20
|
|
|
21
21
|
// Snapshot — the core feature
|
|
22
22
|
const { snapshot, refs } = await page.snapshot();
|
|
23
23
|
// snapshot: AI-readable text tree
|
|
24
|
-
// refs: { "e1": { role: "
|
|
24
|
+
// refs: { "e1": { role: "textbox", name: "What needs to be done?" }, "e2": { role: "link", name: "Playwright" } }
|
|
25
25
|
|
|
26
|
-
await page.
|
|
27
|
-
await page.
|
|
26
|
+
await page.type('e1', 'Buy groceries', { submit: true }); // Type by ref
|
|
27
|
+
await page.click('e2'); // Click by ref
|
|
28
28
|
await browser.stop();
|
|
29
29
|
```
|
|
30
30
|
|
|
@@ -102,20 +102,20 @@ Requires a Chromium-based browser installed on the system (Chrome, Brave, Edge,
|
|
|
102
102
|
## How It Works
|
|
103
103
|
|
|
104
104
|
```
|
|
105
|
-
┌─────────────┐ snapshot()
|
|
106
|
-
│ Web Page │ ──────────────► │ AI-readable text tree
|
|
107
|
-
│ │ │
|
|
108
|
-
│ [buttons] │ │ - heading "
|
|
109
|
-
│ [links] │ │ -
|
|
110
|
-
│ [inputs] │ │ - link "
|
|
111
|
-
└─────────────┘
|
|
105
|
+
┌─────────────┐ snapshot() ┌──────────────────────────────────────────┐
|
|
106
|
+
│ Web Page │ ──────────────► │ AI-readable text tree │
|
|
107
|
+
│ │ │ │
|
|
108
|
+
│ [buttons] │ │ - heading "todos" │
|
|
109
|
+
│ [links] │ │ - textbox "What needs to be done?" [e1] │
|
|
110
|
+
│ [inputs] │ │ - link "Playwright" [e2] │
|
|
111
|
+
└─────────────┘ └──────────────┬───────────────────────────┘
|
|
112
112
|
│
|
|
113
113
|
AI reads snapshot,
|
|
114
|
-
decides:
|
|
114
|
+
decides: type in e1
|
|
115
115
|
│
|
|
116
|
-
┌─────────────┐
|
|
116
|
+
┌─────────────┐ type('e1',...) ┌──────────────▼──────────────────┐
|
|
117
117
|
│ Web Page │ ◄────────────── │ Ref "e1" resolves to a │
|
|
118
|
-
│ (
|
|
118
|
+
│ (updated) │ │ Playwright locator — one ref, │
|
|
119
119
|
│ │ │ one exact element │
|
|
120
120
|
└─────────────┘ └─────────────────────────────────┘
|
|
121
121
|
```
|
|
@@ -133,7 +133,7 @@ Requires a Chromium-based browser installed on the system (Chrome, Brave, Edge,
|
|
|
133
133
|
```typescript
|
|
134
134
|
// Launch a new Chrome instance (auto-detects Chrome/Brave/Edge/Chromium)
|
|
135
135
|
const browser = await BrowserClaw.launch({
|
|
136
|
-
url: 'https://
|
|
136
|
+
url: 'https://demo.playwright.dev/todomvc', // navigate initial tab (no extra tabs)
|
|
137
137
|
headless: false, // default: false (visible window)
|
|
138
138
|
executablePath: '...', // optional: specific browser path
|
|
139
139
|
cdpPort: 9222, // default: 9222
|
|
@@ -159,7 +159,7 @@ const browser = await BrowserClaw.connect();
|
|
|
159
159
|
### Pages & Tabs
|
|
160
160
|
|
|
161
161
|
```typescript
|
|
162
|
-
const page = await browser.open('https://
|
|
162
|
+
const page = await browser.open('https://demo.playwright.dev/todomvc');
|
|
163
163
|
const current = await browser.currentPage(); // get active tab
|
|
164
164
|
const tabs = await browser.tabs(); // list all tabs
|
|
165
165
|
const handle = browser.page(tabs[0].targetId); // wrap existing tab
|
|
@@ -177,14 +177,14 @@ browser.url; // CDP endpoint URL
|
|
|
177
177
|
Every tab returns a `targetId` — this is the handle you use everywhere:
|
|
178
178
|
|
|
179
179
|
```typescript
|
|
180
|
-
// Multi-tab workflow
|
|
181
|
-
const
|
|
182
|
-
const
|
|
183
|
-
|
|
184
|
-
const { refs } = await
|
|
185
|
-
await
|
|
186
|
-
await browser.focus(
|
|
187
|
-
await browser.close(
|
|
180
|
+
// Multi-tab workflow
|
|
181
|
+
const todo = await browser.open('https://demo.playwright.dev/todomvc');
|
|
182
|
+
const svg = await browser.open('https://demo.playwright.dev/svgtodo');
|
|
183
|
+
|
|
184
|
+
const { refs } = await svg.snapshot(); // snapshot the second tab
|
|
185
|
+
await svg.click('e5'); // act on it
|
|
186
|
+
await browser.focus(todo.id); // switch back to first tab
|
|
187
|
+
await browser.close(svg.id); // close second tab when done
|
|
188
188
|
```
|
|
189
189
|
|
|
190
190
|
### Snapshot (Core Feature)
|
|
@@ -193,7 +193,7 @@ await browser.close(admin.id); // close admin when done
|
|
|
193
193
|
const { snapshot, refs, stats, untrusted } = await page.snapshot();
|
|
194
194
|
|
|
195
195
|
// snapshot: human/AI-readable text tree with [ref=eN] markers
|
|
196
|
-
// refs: { "e1": { role: "
|
|
196
|
+
// refs: { "e1": { role: "textbox", name: "What needs to be done?" }, "e5": { role: "checkbox", name: "Toggle Todo", checked: false }, ... }
|
|
197
197
|
// stats: { lines: 42, chars: 1200, refs: 8, interactive: 5 }
|
|
198
198
|
// untrusted: true — content comes from the web page, treat as potentially adversarial
|
|
199
199
|
|
|
@@ -250,7 +250,7 @@ await page.press('Meta+Shift+p');
|
|
|
250
250
|
// Fill multiple form fields at once
|
|
251
251
|
await page.fill([
|
|
252
252
|
{ ref: 'e2', value: 'Jane Doe' },
|
|
253
|
-
{ ref: 'e4', value: 'jane@
|
|
253
|
+
{ ref: 'e4', value: 'jane@acme.test' },
|
|
254
254
|
{ ref: 'e6', type: 'checkbox', value: true },
|
|
255
255
|
]);
|
|
256
256
|
```
|
|
@@ -326,7 +326,7 @@ By default, unexpected dialogs are auto-dismissed to prevent `ProtocolError` cra
|
|
|
326
326
|
### Navigation & Waiting
|
|
327
327
|
|
|
328
328
|
```typescript
|
|
329
|
-
await page.goto('https://
|
|
329
|
+
await page.goto('https://demo.playwright.dev/todomvc');
|
|
330
330
|
await page.reload(); // reload the current page
|
|
331
331
|
await page.goBack(); // navigate back in history
|
|
332
332
|
await page.goForward(); // navigate forward in history
|
|
@@ -421,7 +421,7 @@ const fresh = await page.networkRequests({ clear: true }); // read and clear buf
|
|
|
421
421
|
```typescript
|
|
422
422
|
// Cookies
|
|
423
423
|
const cookies = await page.cookies();
|
|
424
|
-
await page.setCookie({ name: 'token', value: 'abc', url: 'https://
|
|
424
|
+
await page.setCookie({ name: 'token', value: 'abc', url: 'https://demo.playwright.dev' });
|
|
425
425
|
await page.clearCookies();
|
|
426
426
|
|
|
427
427
|
// localStorage / sessionStorage
|
package/dist/index.cjs
CHANGED
|
@@ -1565,6 +1565,7 @@ ${stderrOutput.slice(0, 2e3)}` : "";
|
|
|
1565
1565
|
throw new Error(`Failed to start Chrome CDP on port ${String(cdpPort)}.${sandboxHint}${stderrHint}`);
|
|
1566
1566
|
}
|
|
1567
1567
|
proc.stderr.off("data", onStderr);
|
|
1568
|
+
proc.stderr.resume();
|
|
1568
1569
|
stderrChunks.length = 0;
|
|
1569
1570
|
return {
|
|
1570
1571
|
pid: proc.pid ?? -1,
|
|
@@ -1572,6 +1573,7 @@ ${stderrOutput.slice(0, 2e3)}` : "";
|
|
|
1572
1573
|
userDataDir,
|
|
1573
1574
|
cdpPort,
|
|
1574
1575
|
startedAt,
|
|
1576
|
+
launchMs: Date.now() - startedAt,
|
|
1575
1577
|
proc
|
|
1576
1578
|
};
|
|
1577
1579
|
}
|
|
@@ -2335,6 +2337,7 @@ async function disconnectBrowser() {
|
|
|
2335
2337
|
}
|
|
2336
2338
|
}
|
|
2337
2339
|
for (const cur of cachedByCdpUrl.values()) {
|
|
2340
|
+
clearRoleRefsForCdpUrl(cur.cdpUrl);
|
|
2338
2341
|
if (cur.onDisconnected && typeof cur.browser.off === "function")
|
|
2339
2342
|
cur.browser.off("disconnected", cur.onDisconnected);
|
|
2340
2343
|
await cur.browser.close().catch(() => {
|
|
@@ -2564,7 +2567,6 @@ async function getPageForTargetId(opts) {
|
|
|
2564
2567
|
);
|
|
2565
2568
|
}
|
|
2566
2569
|
if (isBlockedPageRef(opts.cdpUrl, found)) throw new BlockedBrowserTargetError();
|
|
2567
|
-
if (isBlockedTarget(opts.cdpUrl, opts.targetId)) throw new BlockedBrowserTargetError();
|
|
2568
2570
|
return found;
|
|
2569
2571
|
}
|
|
2570
2572
|
async function resolvePageByTargetIdOrThrow(opts) {
|
|
@@ -3217,7 +3219,7 @@ async function writeViaSiblingTempPath(params) {
|
|
|
3217
3219
|
const requestedTargetPath = path.resolve(params.targetPath);
|
|
3218
3220
|
const targetPath = await promises$1.realpath(path.dirname(requestedTargetPath)).then((realDir) => path.join(realDir, path.basename(requestedTargetPath))).catch(() => requestedTargetPath);
|
|
3219
3221
|
const relativeTargetPath = path.relative(rootDir, targetPath);
|
|
3220
|
-
if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${path.sep}`) || isAbsolute(relativeTargetPath)) {
|
|
3222
|
+
if (!relativeTargetPath || relativeTargetPath === ".." || relativeTargetPath.startsWith(`..${path.sep}`) || path.isAbsolute(relativeTargetPath)) {
|
|
3221
3223
|
throw new Error("Target path is outside the allowed root");
|
|
3222
3224
|
}
|
|
3223
3225
|
const tempPath = buildSiblingTempPath(targetPath);
|
|
@@ -3232,9 +3234,6 @@ async function writeViaSiblingTempPath(params) {
|
|
|
3232
3234
|
});
|
|
3233
3235
|
}
|
|
3234
3236
|
}
|
|
3235
|
-
function isAbsolute(p) {
|
|
3236
|
-
return p.startsWith("/") || /^[a-zA-Z]:/.test(p);
|
|
3237
|
-
}
|
|
3238
3237
|
async function assertBrowserNavigationResultAllowed(opts) {
|
|
3239
3238
|
const rawUrl = opts.url.trim();
|
|
3240
3239
|
if (rawUrl === "") return;
|
|
@@ -3275,7 +3274,6 @@ async function setCheckedViaEvaluate(locator, checked) {
|
|
|
3275
3274
|
else input.checked = desired;
|
|
3276
3275
|
input.dispatchEvent(new Event("input", { bubbles: true }));
|
|
3277
3276
|
input.dispatchEvent(new Event("change", { bubbles: true }));
|
|
3278
|
-
input.click();
|
|
3279
3277
|
}, checked);
|
|
3280
3278
|
}
|
|
3281
3279
|
function resolveLocator(page, resolved) {
|
|
@@ -3452,7 +3450,10 @@ async function fillFormViaPlaywright(opts) {
|
|
|
3452
3450
|
const checked = rawValue === true || rawValue === 1 || rawValue === "1" || rawValue === "true";
|
|
3453
3451
|
try {
|
|
3454
3452
|
await locator.setChecked(checked, { timeout, force: true });
|
|
3455
|
-
} catch {
|
|
3453
|
+
} catch (setCheckedErr) {
|
|
3454
|
+
console.warn(
|
|
3455
|
+
`[browserclaw] setChecked fallback for ref "${ref}": ${setCheckedErr instanceof Error ? setCheckedErr.message : String(setCheckedErr)}`
|
|
3456
|
+
);
|
|
3456
3457
|
try {
|
|
3457
3458
|
await setCheckedViaEvaluate(locator, checked);
|
|
3458
3459
|
} catch (err) {
|
|
@@ -3564,6 +3565,7 @@ async function armFileUploadViaPlaywright(opts) {
|
|
|
3564
3565
|
scopeLabel: `uploads directory (${DEFAULT_UPLOAD_DIR})`
|
|
3565
3566
|
});
|
|
3566
3567
|
if (!uploadPathsResult.ok) {
|
|
3568
|
+
console.warn(`[browserclaw] armFileUpload: path validation failed: ${uploadPathsResult.error}`);
|
|
3567
3569
|
try {
|
|
3568
3570
|
await page.keyboard.press("Escape");
|
|
3569
3571
|
} catch {
|
|
@@ -4613,14 +4615,17 @@ async function traceStopViaPlaywright(opts) {
|
|
|
4613
4615
|
if (!ctxState.traceActive) {
|
|
4614
4616
|
throw new Error("No active trace. Start a trace before stopping it.");
|
|
4615
4617
|
}
|
|
4616
|
-
|
|
4617
|
-
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4618
|
+
try {
|
|
4619
|
+
await writeViaSiblingTempPath({
|
|
4620
|
+
rootDir: path.dirname(opts.path),
|
|
4621
|
+
targetPath: opts.path,
|
|
4622
|
+
writeTemp: async (tempPath) => {
|
|
4623
|
+
await context.tracing.stop({ path: tempPath });
|
|
4624
|
+
}
|
|
4625
|
+
});
|
|
4626
|
+
} finally {
|
|
4627
|
+
ctxState.traceActive = false;
|
|
4628
|
+
}
|
|
4624
4629
|
}
|
|
4625
4630
|
|
|
4626
4631
|
// src/snapshot/ref-map.ts
|
|
@@ -4819,7 +4824,7 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
|
4819
4824
|
const isInteractive = INTERACTIVE_ROLES.has(role);
|
|
4820
4825
|
const isContent = CONTENT_ROLES.has(role);
|
|
4821
4826
|
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
4822
|
-
if (options.compact === true && isStructural && name
|
|
4827
|
+
if (options.compact === true && isStructural && !name) continue;
|
|
4823
4828
|
if (!(isInteractive || isContent && name !== "")) {
|
|
4824
4829
|
result.push(line);
|
|
4825
4830
|
continue;
|
|
@@ -4830,7 +4835,7 @@ function buildRoleSnapshotFromAriaSnapshot(ariaSnapshot, options = {}) {
|
|
|
4830
4835
|
const state = parseStateFromSuffix(suffix);
|
|
4831
4836
|
refs[ref] = { role, name, nth, ...state };
|
|
4832
4837
|
let enhanced = `${prefix}${roleRaw}`;
|
|
4833
|
-
if (name
|
|
4838
|
+
if (name) enhanced += ` "${name}"`;
|
|
4834
4839
|
enhanced += ` [ref=${ref}]`;
|
|
4835
4840
|
if (nth > 0) enhanced += ` [nth=${String(nth)}]`;
|
|
4836
4841
|
if (suffix !== "") enhanced += suffix;
|
|
@@ -4908,7 +4913,7 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
|
4908
4913
|
}
|
|
4909
4914
|
const role = roleRaw.toLowerCase();
|
|
4910
4915
|
const isStructural = STRUCTURAL_ROLES.has(role);
|
|
4911
|
-
if (options.compact === true && isStructural && name
|
|
4916
|
+
if (options.compact === true && isStructural && !name) continue;
|
|
4912
4917
|
const ref = parseAiSnapshotRef(suffix);
|
|
4913
4918
|
const state = parseStateFromSuffix(suffix);
|
|
4914
4919
|
if (ref !== null) {
|
|
@@ -4916,9 +4921,9 @@ function buildRoleSnapshotFromAiSnapshot(aiSnapshot, options = {}) {
|
|
|
4916
4921
|
out.push(line);
|
|
4917
4922
|
} else if (INTERACTIVE_ROLES.has(role)) {
|
|
4918
4923
|
const generatedRef = nextGeneratedRef();
|
|
4919
|
-
refs[generatedRef] = { role, ...name
|
|
4924
|
+
refs[generatedRef] = { role, ...name ? { name } : {}, ...state };
|
|
4920
4925
|
let enhanced = `${prefix}${roleRaw}`;
|
|
4921
|
-
if (name
|
|
4926
|
+
if (name) enhanced += ` "${name}"`;
|
|
4922
4927
|
enhanced += ` [ref=${generatedRef}]`;
|
|
4923
4928
|
if (suffix.trim() !== "") enhanced += suffix;
|
|
4924
4929
|
out.push(enhanced);
|
|
@@ -4999,7 +5004,7 @@ async function snapshotRole(opts) {
|
|
|
4999
5004
|
if (!maybe._snapshotForAI) {
|
|
5000
5005
|
throw new Error("refs=aria requires Playwright _snapshotForAI support.");
|
|
5001
5006
|
}
|
|
5002
|
-
const result = await maybe._snapshotForAI({ timeout: 5e3, track: "response" });
|
|
5007
|
+
const result = await maybe._snapshotForAI({ timeout: normalizeTimeoutMs(opts.timeoutMs, 5e3), track: "response" });
|
|
5003
5008
|
const built2 = buildRoleSnapshotFromAiSnapshot(String(result.full), opts.options);
|
|
5004
5009
|
storeRoleRefsForTarget({
|
|
5005
5010
|
page,
|
|
@@ -5509,7 +5514,7 @@ var CrawlPage = class {
|
|
|
5509
5514
|
* ```ts
|
|
5510
5515
|
* await page.fill([
|
|
5511
5516
|
* { ref: 'e2', type: 'text', value: 'Jane Doe' },
|
|
5512
|
-
* { ref: 'e4', type: 'text', value: 'jane@
|
|
5517
|
+
* { ref: 'e4', type: 'text', value: 'jane@acme.test' },
|
|
5513
5518
|
* { ref: 'e6', type: 'checkbox', value: true },
|
|
5514
5519
|
* ]);
|
|
5515
5520
|
* ```
|
|
@@ -5562,17 +5567,18 @@ var CrawlPage = class {
|
|
|
5562
5567
|
});
|
|
5563
5568
|
}
|
|
5564
5569
|
/**
|
|
5565
|
-
* Arm a one-shot dialog handler (alert, confirm, prompt).
|
|
5570
|
+
* Arm a one-shot dialog handler (alert, confirm, prompt). Fire-and-forget:
|
|
5571
|
+
* returns immediately once the arm is registered. The dialog is handled in
|
|
5572
|
+
* the background when it fires.
|
|
5566
5573
|
*
|
|
5567
|
-
*
|
|
5574
|
+
* Call this BEFORE triggering the action that opens the dialog.
|
|
5568
5575
|
*
|
|
5569
5576
|
* @param opts - Dialog options (accept/dismiss, prompt text, timeout)
|
|
5570
5577
|
*
|
|
5571
5578
|
* @example
|
|
5572
5579
|
* ```ts
|
|
5573
|
-
*
|
|
5574
|
-
* await page.click('e5');
|
|
5575
|
-
* await dialogDone; // wait for dialog to be handled
|
|
5580
|
+
* await page.armDialog({ accept: true }); // registers the handler, returns immediately
|
|
5581
|
+
* await page.click('e5'); // triggers confirm() — handled in background
|
|
5576
5582
|
* ```
|
|
5577
5583
|
*/
|
|
5578
5584
|
async armDialog(opts) {
|
|
@@ -6054,7 +6060,7 @@ var CrawlPage = class {
|
|
|
6054
6060
|
* await page.setCookie({
|
|
6055
6061
|
* name: 'token',
|
|
6056
6062
|
* value: 'abc123',
|
|
6057
|
-
* url: 'https://
|
|
6063
|
+
* url: 'https://demo.playwright.dev/todomvc',
|
|
6058
6064
|
* });
|
|
6059
6065
|
* ```
|
|
6060
6066
|
*/
|
|
@@ -6306,7 +6312,7 @@ var CrawlPage = class {
|
|
|
6306
6312
|
*
|
|
6307
6313
|
* @example
|
|
6308
6314
|
* ```ts
|
|
6309
|
-
* await page.goto('https://
|
|
6315
|
+
* await page.goto('https://demo.playwright.dev/todomvc');
|
|
6310
6316
|
* const challenge = await page.detectChallenge();
|
|
6311
6317
|
* if (challenge?.kind === 'cloudflare-js') {
|
|
6312
6318
|
* const { resolved } = await page.waitForChallenge({ timeoutMs: 20000 });
|
|
@@ -6322,6 +6328,137 @@ var CrawlPage = class {
|
|
|
6322
6328
|
pollMs: opts?.pollMs
|
|
6323
6329
|
});
|
|
6324
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
|
+
}
|
|
6325
6462
|
// ── Playwright Escape Hatches ─────────────────────────────────
|
|
6326
6463
|
/**
|
|
6327
6464
|
* Get the underlying Playwright `Page` object for this tab.
|
|
@@ -6367,7 +6504,7 @@ var CrawlPage = class {
|
|
|
6367
6504
|
*
|
|
6368
6505
|
* // Use Playwright selectors
|
|
6369
6506
|
* const input = await page.locator('input[name="email"]');
|
|
6370
|
-
* await input.fill('test@
|
|
6507
|
+
* await input.fill('test@acme.test');
|
|
6371
6508
|
* ```
|
|
6372
6509
|
*/
|
|
6373
6510
|
async locator(selector) {
|
|
@@ -6380,9 +6517,11 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6380
6517
|
ssrfPolicy;
|
|
6381
6518
|
recordVideo;
|
|
6382
6519
|
chrome;
|
|
6383
|
-
|
|
6520
|
+
_telemetry;
|
|
6521
|
+
constructor(cdpUrl, chrome, telemetry, ssrfPolicy, recordVideo) {
|
|
6384
6522
|
this.cdpUrl = cdpUrl;
|
|
6385
6523
|
this.chrome = chrome;
|
|
6524
|
+
this._telemetry = telemetry;
|
|
6386
6525
|
this.ssrfPolicy = ssrfPolicy;
|
|
6387
6526
|
this.recordVideo = recordVideo;
|
|
6388
6527
|
}
|
|
@@ -6398,26 +6537,34 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6398
6537
|
* @example
|
|
6399
6538
|
* ```ts
|
|
6400
6539
|
* // Launch and navigate to a URL
|
|
6401
|
-
* const browser = await BrowserClaw.launch({ url: 'https://
|
|
6540
|
+
* const browser = await BrowserClaw.launch({ url: 'https://demo.playwright.dev/todomvc' });
|
|
6402
6541
|
*
|
|
6403
6542
|
* // Headless mode
|
|
6404
|
-
* const browser = await BrowserClaw.launch({ url: 'https://
|
|
6543
|
+
* const browser = await BrowserClaw.launch({ url: 'https://demo.playwright.dev/todomvc', headless: true });
|
|
6405
6544
|
*
|
|
6406
6545
|
* // Specific browser
|
|
6407
6546
|
* const browser = await BrowserClaw.launch({
|
|
6408
|
-
* url: 'https://
|
|
6547
|
+
* url: 'https://demo.playwright.dev/todomvc',
|
|
6409
6548
|
* executablePath: '/usr/bin/google-chrome',
|
|
6410
6549
|
* });
|
|
6411
6550
|
* ```
|
|
6412
6551
|
*/
|
|
6413
6552
|
static async launch(opts = {}) {
|
|
6553
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6414
6554
|
const chrome = await launchChrome(opts);
|
|
6415
6555
|
const cdpUrl = `http://127.0.0.1:${String(chrome.cdpPort)}`;
|
|
6416
6556
|
const ssrfPolicy = opts.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts.ssrfPolicy;
|
|
6417
|
-
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);
|
|
6418
6562
|
if (opts.url !== void 0 && opts.url !== "") {
|
|
6419
6563
|
const page = await browser.currentPage();
|
|
6564
|
+
const navT0 = Date.now();
|
|
6420
6565
|
await page.goto(opts.url);
|
|
6566
|
+
telemetry.navMs = Date.now() - navT0;
|
|
6567
|
+
telemetry.timestamps.navigatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6421
6568
|
}
|
|
6422
6569
|
return browser;
|
|
6423
6570
|
}
|
|
@@ -6436,6 +6583,8 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6436
6583
|
* ```
|
|
6437
6584
|
*/
|
|
6438
6585
|
static async connect(cdpUrl, opts) {
|
|
6586
|
+
const startedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
6587
|
+
const connectT0 = Date.now();
|
|
6439
6588
|
let resolvedUrl = cdpUrl;
|
|
6440
6589
|
if (resolvedUrl === void 0 || resolvedUrl === "") {
|
|
6441
6590
|
const discovered = await discoverChromeCdpUrl();
|
|
@@ -6451,7 +6600,11 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6451
6600
|
}
|
|
6452
6601
|
await connectBrowser(resolvedUrl, opts?.authToken);
|
|
6453
6602
|
const ssrfPolicy = opts?.allowInternal === true ? { ...opts.ssrfPolicy, dangerouslyAllowPrivateNetwork: true } : opts?.ssrfPolicy;
|
|
6454
|
-
|
|
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);
|
|
6455
6608
|
}
|
|
6456
6609
|
/**
|
|
6457
6610
|
* Open a URL in a new tab and return the page handle.
|
|
@@ -6461,7 +6614,7 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6461
6614
|
*
|
|
6462
6615
|
* @example
|
|
6463
6616
|
* ```ts
|
|
6464
|
-
* const page = await browser.open('https://
|
|
6617
|
+
* const page = await browser.open('https://demo.playwright.dev/todomvc');
|
|
6465
6618
|
* const { snapshot, refs } = await page.snapshot();
|
|
6466
6619
|
* ```
|
|
6467
6620
|
*/
|
|
@@ -6480,7 +6633,12 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6480
6633
|
* @returns CrawlPage for the first/active page
|
|
6481
6634
|
*/
|
|
6482
6635
|
async currentPage() {
|
|
6636
|
+
const connectT0 = Date.now();
|
|
6483
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
|
+
}
|
|
6484
6642
|
const pages = getAllPages(browser);
|
|
6485
6643
|
if (!pages.length) throw new Error("No pages available. Use browser.open(url) to create a tab.");
|
|
6486
6644
|
const tid = await pageTargetId(pages[0]).catch(() => null);
|
|
@@ -6557,15 +6715,61 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
6557
6715
|
* If the browser was launched by `BrowserClaw.launch()`, the Chrome process
|
|
6558
6716
|
* will be terminated. If connected via `BrowserClaw.connect()`, only the
|
|
6559
6717
|
* Playwright connection is closed.
|
|
6718
|
+
*
|
|
6719
|
+
* @param exitReason - Optional structured reason for stopping (e.g. `'success'`, `'auth_failed'`, `'timeout'`)
|
|
6560
6720
|
*/
|
|
6561
|
-
async stop() {
|
|
6562
|
-
|
|
6563
|
-
|
|
6564
|
-
|
|
6565
|
-
|
|
6566
|
-
|
|
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;
|
|
6567
6735
|
}
|
|
6568
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
|
+
}
|
|
6569
6773
|
};
|
|
6570
6774
|
|
|
6571
6775
|
exports.BlockedBrowserTargetError = BlockedBrowserTargetError;
|