browserclaw 0.6.0 → 0.7.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 +349 -2
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +95 -1
- package/dist/index.d.ts +95 -1
- package/dist/index.js +347 -3
- package/dist/index.js.map +1 -1
- package/package.json +2 -1
package/dist/index.d.cts
CHANGED
|
@@ -545,6 +545,22 @@ interface HttpCredentials {
|
|
|
545
545
|
interface ContextState {
|
|
546
546
|
traceActive: boolean;
|
|
547
547
|
}
|
|
548
|
+
/** The kind of anti-bot challenge detected on a page. */
|
|
549
|
+
type ChallengeKind = 'cloudflare-js' | 'cloudflare-block' | 'cloudflare-turnstile' | 'hcaptcha' | 'recaptcha' | 'blocked' | 'rate-limited';
|
|
550
|
+
/** Information about a detected anti-bot challenge. */
|
|
551
|
+
interface ChallengeInfo {
|
|
552
|
+
/** What type of challenge is present */
|
|
553
|
+
kind: ChallengeKind;
|
|
554
|
+
/** Human-readable description */
|
|
555
|
+
message: string;
|
|
556
|
+
}
|
|
557
|
+
/** Result of waiting for an anti-bot challenge to resolve. */
|
|
558
|
+
interface ChallengeWaitResult {
|
|
559
|
+
/** Whether the challenge cleared within the timeout */
|
|
560
|
+
resolved: boolean;
|
|
561
|
+
/** The challenge still present (null if resolved) */
|
|
562
|
+
challenge: ChallengeInfo | null;
|
|
563
|
+
}
|
|
548
564
|
/** Result of DNS pinning resolution — hostname locked to resolved addresses. */
|
|
549
565
|
interface PinnedHostname {
|
|
550
566
|
hostname: string;
|
|
@@ -1162,6 +1178,46 @@ declare class CrawlPage {
|
|
|
1162
1178
|
* ```
|
|
1163
1179
|
*/
|
|
1164
1180
|
setDevice(name: string): Promise<void>;
|
|
1181
|
+
/**
|
|
1182
|
+
* Detect whether the page is showing an anti-bot challenge
|
|
1183
|
+
* (Cloudflare, hCaptcha, reCAPTCHA, access-denied, rate-limit, etc.).
|
|
1184
|
+
*
|
|
1185
|
+
* Returns `null` if no challenge is detected.
|
|
1186
|
+
*
|
|
1187
|
+
* @example
|
|
1188
|
+
* ```ts
|
|
1189
|
+
* const challenge = await page.detectChallenge();
|
|
1190
|
+
* if (challenge) {
|
|
1191
|
+
* console.log(challenge.kind); // 'cloudflare-js'
|
|
1192
|
+
* console.log(challenge.message); // 'Cloudflare JS challenge'
|
|
1193
|
+
* }
|
|
1194
|
+
* ```
|
|
1195
|
+
*/
|
|
1196
|
+
detectChallenge(): Promise<ChallengeInfo | null>;
|
|
1197
|
+
/**
|
|
1198
|
+
* Wait for an anti-bot challenge to resolve on its own.
|
|
1199
|
+
*
|
|
1200
|
+
* Cloudflare JS challenges typically auto-resolve in ~5 seconds.
|
|
1201
|
+
* CAPTCHA challenges will only resolve if solved in a visible browser window.
|
|
1202
|
+
*
|
|
1203
|
+
* @param opts.timeoutMs - Maximum wait time (default: `15000`)
|
|
1204
|
+
* @param opts.pollMs - Poll interval (default: `500`)
|
|
1205
|
+
* @returns Whether the challenge resolved, and the remaining challenge info if not
|
|
1206
|
+
*
|
|
1207
|
+
* @example
|
|
1208
|
+
* ```ts
|
|
1209
|
+
* await page.goto('https://example.com');
|
|
1210
|
+
* const challenge = await page.detectChallenge();
|
|
1211
|
+
* if (challenge?.kind === 'cloudflare-js') {
|
|
1212
|
+
* const { resolved } = await page.waitForChallenge({ timeoutMs: 20000 });
|
|
1213
|
+
* if (!resolved) throw new Error('Challenge did not resolve');
|
|
1214
|
+
* }
|
|
1215
|
+
* ```
|
|
1216
|
+
*/
|
|
1217
|
+
waitForChallenge(opts?: {
|
|
1218
|
+
timeoutMs?: number;
|
|
1219
|
+
pollMs?: number;
|
|
1220
|
+
}): Promise<ChallengeWaitResult>;
|
|
1165
1221
|
}
|
|
1166
1222
|
/**
|
|
1167
1223
|
* Main entry point for browserclaw.
|
|
@@ -1454,4 +1510,42 @@ declare function getRestoredPageForTarget(opts: {
|
|
|
1454
1510
|
targetId?: string;
|
|
1455
1511
|
}): Promise<Page>;
|
|
1456
1512
|
|
|
1457
|
-
|
|
1513
|
+
/**
|
|
1514
|
+
* Comprehensive browser stealth evasions.
|
|
1515
|
+
*
|
|
1516
|
+
* Injected via `addInitScript()` (runs before any page JS) and via
|
|
1517
|
+
* `page.evaluate()` for already-loaded pages. Each patch is wrapped
|
|
1518
|
+
* in try/catch so a single failure never breaks the rest.
|
|
1519
|
+
*
|
|
1520
|
+
* Covers: navigator.webdriver, plugins, languages, window.chrome,
|
|
1521
|
+
* Permissions API, WebGL fingerprint, Notification.permission,
|
|
1522
|
+
* navigator.connection, console toString, headless-mode quirks,
|
|
1523
|
+
* hardwareConcurrency, and deviceMemory.
|
|
1524
|
+
*/
|
|
1525
|
+
declare const STEALTH_SCRIPT = "(function() {\n 'use strict';\n function p(fn) { try { fn(); } catch(_) {} }\n\n // \u2500\u2500 1. navigator.webdriver \u2192 undefined \u2500\u2500\n p(function() {\n Object.defineProperty(navigator, 'webdriver', { get: function() { return undefined; }, configurable: true });\n });\n\n // \u2500\u2500 2. navigator.plugins + mimeTypes (only if empty \u2014 Chrome 92+ populates them natively) \u2500\u2500\n p(function() {\n if (navigator.plugins && navigator.plugins.length > 0) return;\n\n function FakePlugin(name, fn, desc, mimes) {\n this.name = name; this.filename = fn; this.description = desc; this.length = mimes.length;\n for (var i = 0; i < mimes.length; i++) { this[i] = mimes[i]; mimes[i].enabledPlugin = this; }\n }\n FakePlugin.prototype.item = function(i) { return this[i] || null; };\n FakePlugin.prototype.namedItem = function(n) {\n for (var i = 0; i < this.length; i++) if (this[i].type === n) return this[i];\n return null;\n };\n\n function M(type, suf, desc) { this.type = type; this.suffixes = suf; this.description = desc; }\n\n var m1 = new M('application/pdf', 'pdf', 'Portable Document Format');\n var m2 = new M('application/x-google-chrome-pdf', 'pdf', 'Portable Document Format');\n var m3 = new M('application/x-nacl', '', 'Native Client Executable');\n var m4 = new M('application/x-pnacl', '', 'Portable Native Client Executable');\n\n var plugins = [\n new FakePlugin('Chrome PDF Plugin', 'internal-pdf-viewer', 'Portable Document Format', [m1]),\n new FakePlugin('Chrome PDF Viewer', 'mhjfbmdgcfjbbpaeojofohoefgiehjai', '', [m2]),\n new FakePlugin('Native Client', 'internal-nacl-plugin', '', [m3, m4]),\n ];\n\n function makeIterable(arr, items) {\n arr.length = items.length;\n for (var i = 0; i < items.length; i++) arr[i] = items[i];\n arr[Symbol.iterator] = function() {\n var idx = 0;\n return { next: function() {\n return idx < items.length ? { value: items[idx++], done: false } : { done: true };\n }};\n };\n }\n\n var pa = { item: function(i) { return plugins[i] || null; },\n namedItem: function(n) { for (var i = 0; i < plugins.length; i++) if (plugins[i].name === n) return plugins[i]; return null; },\n refresh: function() {} };\n makeIterable(pa, plugins);\n Object.defineProperty(navigator, 'plugins', { get: function() { return pa; } });\n\n var allMimes = [m1, m2, m3, m4];\n var ma = { item: function(i) { return allMimes[i] || null; },\n namedItem: function(n) { for (var i = 0; i < allMimes.length; i++) if (allMimes[i].type === n) return allMimes[i]; return null; } };\n makeIterable(ma, allMimes);\n Object.defineProperty(navigator, 'mimeTypes', { get: function() { return ma; } });\n });\n\n // \u2500\u2500 3. navigator.languages (cached + frozen so identity check passes) \u2500\u2500\n p(function() {\n if (!navigator.languages || navigator.languages.length === 0) {\n var langs = Object.freeze(['en-US', 'en']);\n Object.defineProperty(navigator, 'languages', { get: function() { return langs; } });\n }\n });\n\n // \u2500\u2500 4. window.chrome \u2500\u2500\n p(function() {\n if (window.chrome && window.chrome.runtime && window.chrome.runtime.connect) return;\n\n var chrome = window.chrome || {};\n var noop = function() {};\n var evtStub = { addListener: noop, removeListener: noop, hasListeners: function() { return false; } };\n chrome.runtime = chrome.runtime || {};\n chrome.runtime.onMessage = chrome.runtime.onMessage || evtStub;\n chrome.runtime.onConnect = chrome.runtime.onConnect || evtStub;\n chrome.runtime.sendMessage = chrome.runtime.sendMessage || noop;\n chrome.runtime.connect = chrome.runtime.connect || function() {\n return { onMessage: { addListener: noop }, postMessage: noop, disconnect: noop };\n };\n if (chrome.runtime.id === undefined) chrome.runtime.id = undefined;\n if (!chrome.loadTimes) chrome.loadTimes = function() { return {}; };\n if (!chrome.csi) chrome.csi = function() { return {}; };\n if (!chrome.app) {\n chrome.app = {\n isInstalled: false,\n InstallState: { INSTALLED: 'installed', NOT_INSTALLED: 'not_installed', DISABLED: 'disabled' },\n RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },\n getDetails: function() { return null; },\n getIsInstalled: function() { return false; },\n runningState: function() { return 'cannot_run'; },\n };\n }\n\n if (!window.chrome) {\n Object.defineProperty(window, 'chrome', { value: chrome, writable: false, enumerable: true, configurable: false });\n }\n });\n\n // \u2500\u2500 5. Permissions API consistency \u2500\u2500\n p(function() {\n var orig = navigator.permissions.query.bind(navigator.permissions);\n function q(params) {\n if (params.name === 'notifications') {\n return Promise.resolve({\n state: typeof Notification !== 'undefined' ? Notification.permission : 'prompt',\n name: 'notifications', onchange: null,\n addEventListener: function(){}, removeEventListener: function(){}, dispatchEvent: function(){ return true; },\n });\n }\n return orig(params);\n }\n q.toString = function() { return 'function query() { [native code] }'; };\n navigator.permissions.query = q;\n });\n\n // \u2500\u2500 6. WebGL vendor / renderer \u2500\u2500\n p(function() {\n var h = {\n apply: function(target, self, args) {\n var param = args[0];\n if (param === 0x9245) return 'Intel Inc.';\n if (param === 0x9246) return 'Intel Iris OpenGL Engine';\n return Reflect.apply(target, self, args);\n }\n };\n if (typeof WebGLRenderingContext !== 'undefined')\n WebGLRenderingContext.prototype.getParameter = new Proxy(WebGLRenderingContext.prototype.getParameter, h);\n if (typeof WebGL2RenderingContext !== 'undefined')\n WebGL2RenderingContext.prototype.getParameter = new Proxy(WebGL2RenderingContext.prototype.getParameter, h);\n });\n\n // \u2500\u2500 7. Notification.permission \u2500\u2500\n p(function() {\n if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {\n Object.defineProperty(Notification, 'permission', { get: function() { return 'default'; }, configurable: true });\n }\n });\n\n // \u2500\u2500 8. navigator.connection (cached so identity check passes) \u2500\u2500\n p(function() {\n if (navigator.connection) return;\n var conn = {\n effectiveType: '4g', rtt: 50, downlink: 10, saveData: false, onchange: null,\n addEventListener: function(){}, removeEventListener: function(){}, dispatchEvent: function(){ return true; },\n };\n Object.defineProperty(navigator, 'connection', { get: function() { return conn; } });\n });\n\n // \u2500\u2500 9. Iframe contentWindow.chrome \u2500\u2500\n // Handled by patch 4 \u2014 chrome object is now on window, propagates to iframes on same origin.\n\n // \u2500\u2500 10. console method toString \u2500\u2500\n p(function() {\n ['log','info','warn','error','debug','table','trace'].forEach(function(n) {\n if (console[n]) {\n console[n].toString = function() { return 'function ' + n + '() { [native code] }'; };\n }\n });\n });\n\n // \u2500\u2500 11. Headless-mode window / screen fixes \u2500\u2500\n p(function() {\n if (window.outerWidth === 0)\n Object.defineProperty(window, 'outerWidth', { get: function() { return window.innerWidth || 1920; } });\n if (window.outerHeight === 0)\n Object.defineProperty(window, 'outerHeight', { get: function() { return (window.innerHeight || 1080) + 85; } });\n });\n\n p(function() {\n if (screen.colorDepth === 0) {\n Object.defineProperty(screen, 'colorDepth', { get: function() { return 24; } });\n Object.defineProperty(screen, 'pixelDepth', { get: function() { return 24; } });\n }\n });\n\n // \u2500\u2500 12. navigator.hardwareConcurrency \u2500\u2500\n p(function() {\n if (!navigator.hardwareConcurrency)\n Object.defineProperty(navigator, 'hardwareConcurrency', { get: function() { return 4; } });\n });\n\n // \u2500\u2500 13. navigator.deviceMemory \u2500\u2500\n p(function() {\n if (!navigator.deviceMemory)\n Object.defineProperty(navigator, 'deviceMemory', { get: function() { return 8; } });\n });\n})()";
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* Detect whether the current page is showing an anti-bot challenge.
|
|
1529
|
+
* Returns `null` if no challenge is detected.
|
|
1530
|
+
*/
|
|
1531
|
+
declare function detectChallengeViaPlaywright(opts: {
|
|
1532
|
+
cdpUrl: string;
|
|
1533
|
+
targetId?: string;
|
|
1534
|
+
}): Promise<ChallengeInfo | null>;
|
|
1535
|
+
/**
|
|
1536
|
+
* Wait for an anti-bot challenge to resolve on its own (e.g. Cloudflare JS challenge).
|
|
1537
|
+
*
|
|
1538
|
+
* Returns `{ resolved: true }` if the challenge cleared within the timeout,
|
|
1539
|
+
* or `{ resolved: false, challenge }` with the still-present challenge info.
|
|
1540
|
+
*
|
|
1541
|
+
* For challenges that require human interaction (CAPTCHA), this will time out
|
|
1542
|
+
* unless the user solves the challenge in the visible browser window.
|
|
1543
|
+
*/
|
|
1544
|
+
declare function waitForChallengeViaPlaywright(opts: {
|
|
1545
|
+
cdpUrl: string;
|
|
1546
|
+
targetId?: string;
|
|
1547
|
+
timeoutMs?: number;
|
|
1548
|
+
pollMs?: number;
|
|
1549
|
+
}): Promise<ChallengeWaitResult>;
|
|
1550
|
+
|
|
1551
|
+
export { type AriaNode, type AriaSnapshotResult, type BatchAction, type BatchActionResult, BrowserClaw, type BrowserNavigationPolicyOptions, type BrowserNavigationRequestLike, type BrowserTab, BrowserTabNotFoundError, type ChallengeInfo, type ChallengeKind, type ChallengeWaitResult, type ChromeExecutable, type ChromeKind, type ClickOptions, type ColorScheme, type ConnectOptions, type ConsoleMessage, type ContextState, type CookieData, CrawlPage, type DialogOptions, type DownloadResult, type FormField, type FrameEvalResult, type GeolocationOptions, type HttpCredentials, InvalidBrowserNavigationUrlError, type LaunchOptions, type LookupFn, type NetworkRequest, type PageError, type PinnedHostname, type ResponseBodyResult, type RoleRefInfo, type RoleRefs, STEALTH_SCRIPT, type ScreenshotOptions, type SnapshotOptions, type SnapshotResult, type SnapshotStats, type SsrfPolicy, type StorageKind, type TraceStartOptions, type TypeOptions, type UntrustedContentMeta, type WaitOptions, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, detectChallengeViaPlaywright, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, waitForChallengeViaPlaywright, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
|
package/dist/index.d.ts
CHANGED
|
@@ -545,6 +545,22 @@ interface HttpCredentials {
|
|
|
545
545
|
interface ContextState {
|
|
546
546
|
traceActive: boolean;
|
|
547
547
|
}
|
|
548
|
+
/** The kind of anti-bot challenge detected on a page. */
|
|
549
|
+
type ChallengeKind = 'cloudflare-js' | 'cloudflare-block' | 'cloudflare-turnstile' | 'hcaptcha' | 'recaptcha' | 'blocked' | 'rate-limited';
|
|
550
|
+
/** Information about a detected anti-bot challenge. */
|
|
551
|
+
interface ChallengeInfo {
|
|
552
|
+
/** What type of challenge is present */
|
|
553
|
+
kind: ChallengeKind;
|
|
554
|
+
/** Human-readable description */
|
|
555
|
+
message: string;
|
|
556
|
+
}
|
|
557
|
+
/** Result of waiting for an anti-bot challenge to resolve. */
|
|
558
|
+
interface ChallengeWaitResult {
|
|
559
|
+
/** Whether the challenge cleared within the timeout */
|
|
560
|
+
resolved: boolean;
|
|
561
|
+
/** The challenge still present (null if resolved) */
|
|
562
|
+
challenge: ChallengeInfo | null;
|
|
563
|
+
}
|
|
548
564
|
/** Result of DNS pinning resolution — hostname locked to resolved addresses. */
|
|
549
565
|
interface PinnedHostname {
|
|
550
566
|
hostname: string;
|
|
@@ -1162,6 +1178,46 @@ declare class CrawlPage {
|
|
|
1162
1178
|
* ```
|
|
1163
1179
|
*/
|
|
1164
1180
|
setDevice(name: string): Promise<void>;
|
|
1181
|
+
/**
|
|
1182
|
+
* Detect whether the page is showing an anti-bot challenge
|
|
1183
|
+
* (Cloudflare, hCaptcha, reCAPTCHA, access-denied, rate-limit, etc.).
|
|
1184
|
+
*
|
|
1185
|
+
* Returns `null` if no challenge is detected.
|
|
1186
|
+
*
|
|
1187
|
+
* @example
|
|
1188
|
+
* ```ts
|
|
1189
|
+
* const challenge = await page.detectChallenge();
|
|
1190
|
+
* if (challenge) {
|
|
1191
|
+
* console.log(challenge.kind); // 'cloudflare-js'
|
|
1192
|
+
* console.log(challenge.message); // 'Cloudflare JS challenge'
|
|
1193
|
+
* }
|
|
1194
|
+
* ```
|
|
1195
|
+
*/
|
|
1196
|
+
detectChallenge(): Promise<ChallengeInfo | null>;
|
|
1197
|
+
/**
|
|
1198
|
+
* Wait for an anti-bot challenge to resolve on its own.
|
|
1199
|
+
*
|
|
1200
|
+
* Cloudflare JS challenges typically auto-resolve in ~5 seconds.
|
|
1201
|
+
* CAPTCHA challenges will only resolve if solved in a visible browser window.
|
|
1202
|
+
*
|
|
1203
|
+
* @param opts.timeoutMs - Maximum wait time (default: `15000`)
|
|
1204
|
+
* @param opts.pollMs - Poll interval (default: `500`)
|
|
1205
|
+
* @returns Whether the challenge resolved, and the remaining challenge info if not
|
|
1206
|
+
*
|
|
1207
|
+
* @example
|
|
1208
|
+
* ```ts
|
|
1209
|
+
* await page.goto('https://example.com');
|
|
1210
|
+
* const challenge = await page.detectChallenge();
|
|
1211
|
+
* if (challenge?.kind === 'cloudflare-js') {
|
|
1212
|
+
* const { resolved } = await page.waitForChallenge({ timeoutMs: 20000 });
|
|
1213
|
+
* if (!resolved) throw new Error('Challenge did not resolve');
|
|
1214
|
+
* }
|
|
1215
|
+
* ```
|
|
1216
|
+
*/
|
|
1217
|
+
waitForChallenge(opts?: {
|
|
1218
|
+
timeoutMs?: number;
|
|
1219
|
+
pollMs?: number;
|
|
1220
|
+
}): Promise<ChallengeWaitResult>;
|
|
1165
1221
|
}
|
|
1166
1222
|
/**
|
|
1167
1223
|
* Main entry point for browserclaw.
|
|
@@ -1454,4 +1510,42 @@ declare function getRestoredPageForTarget(opts: {
|
|
|
1454
1510
|
targetId?: string;
|
|
1455
1511
|
}): Promise<Page>;
|
|
1456
1512
|
|
|
1457
|
-
|
|
1513
|
+
/**
|
|
1514
|
+
* Comprehensive browser stealth evasions.
|
|
1515
|
+
*
|
|
1516
|
+
* Injected via `addInitScript()` (runs before any page JS) and via
|
|
1517
|
+
* `page.evaluate()` for already-loaded pages. Each patch is wrapped
|
|
1518
|
+
* in try/catch so a single failure never breaks the rest.
|
|
1519
|
+
*
|
|
1520
|
+
* Covers: navigator.webdriver, plugins, languages, window.chrome,
|
|
1521
|
+
* Permissions API, WebGL fingerprint, Notification.permission,
|
|
1522
|
+
* navigator.connection, console toString, headless-mode quirks,
|
|
1523
|
+
* hardwareConcurrency, and deviceMemory.
|
|
1524
|
+
*/
|
|
1525
|
+
declare const STEALTH_SCRIPT = "(function() {\n 'use strict';\n function p(fn) { try { fn(); } catch(_) {} }\n\n // \u2500\u2500 1. navigator.webdriver \u2192 undefined \u2500\u2500\n p(function() {\n Object.defineProperty(navigator, 'webdriver', { get: function() { return undefined; }, configurable: true });\n });\n\n // \u2500\u2500 2. navigator.plugins + mimeTypes (only if empty \u2014 Chrome 92+ populates them natively) \u2500\u2500\n p(function() {\n if (navigator.plugins && navigator.plugins.length > 0) return;\n\n function FakePlugin(name, fn, desc, mimes) {\n this.name = name; this.filename = fn; this.description = desc; this.length = mimes.length;\n for (var i = 0; i < mimes.length; i++) { this[i] = mimes[i]; mimes[i].enabledPlugin = this; }\n }\n FakePlugin.prototype.item = function(i) { return this[i] || null; };\n FakePlugin.prototype.namedItem = function(n) {\n for (var i = 0; i < this.length; i++) if (this[i].type === n) return this[i];\n return null;\n };\n\n function M(type, suf, desc) { this.type = type; this.suffixes = suf; this.description = desc; }\n\n var m1 = new M('application/pdf', 'pdf', 'Portable Document Format');\n var m2 = new M('application/x-google-chrome-pdf', 'pdf', 'Portable Document Format');\n var m3 = new M('application/x-nacl', '', 'Native Client Executable');\n var m4 = new M('application/x-pnacl', '', 'Portable Native Client Executable');\n\n var plugins = [\n new FakePlugin('Chrome PDF Plugin', 'internal-pdf-viewer', 'Portable Document Format', [m1]),\n new FakePlugin('Chrome PDF Viewer', 'mhjfbmdgcfjbbpaeojofohoefgiehjai', '', [m2]),\n new FakePlugin('Native Client', 'internal-nacl-plugin', '', [m3, m4]),\n ];\n\n function makeIterable(arr, items) {\n arr.length = items.length;\n for (var i = 0; i < items.length; i++) arr[i] = items[i];\n arr[Symbol.iterator] = function() {\n var idx = 0;\n return { next: function() {\n return idx < items.length ? { value: items[idx++], done: false } : { done: true };\n }};\n };\n }\n\n var pa = { item: function(i) { return plugins[i] || null; },\n namedItem: function(n) { for (var i = 0; i < plugins.length; i++) if (plugins[i].name === n) return plugins[i]; return null; },\n refresh: function() {} };\n makeIterable(pa, plugins);\n Object.defineProperty(navigator, 'plugins', { get: function() { return pa; } });\n\n var allMimes = [m1, m2, m3, m4];\n var ma = { item: function(i) { return allMimes[i] || null; },\n namedItem: function(n) { for (var i = 0; i < allMimes.length; i++) if (allMimes[i].type === n) return allMimes[i]; return null; } };\n makeIterable(ma, allMimes);\n Object.defineProperty(navigator, 'mimeTypes', { get: function() { return ma; } });\n });\n\n // \u2500\u2500 3. navigator.languages (cached + frozen so identity check passes) \u2500\u2500\n p(function() {\n if (!navigator.languages || navigator.languages.length === 0) {\n var langs = Object.freeze(['en-US', 'en']);\n Object.defineProperty(navigator, 'languages', { get: function() { return langs; } });\n }\n });\n\n // \u2500\u2500 4. window.chrome \u2500\u2500\n p(function() {\n if (window.chrome && window.chrome.runtime && window.chrome.runtime.connect) return;\n\n var chrome = window.chrome || {};\n var noop = function() {};\n var evtStub = { addListener: noop, removeListener: noop, hasListeners: function() { return false; } };\n chrome.runtime = chrome.runtime || {};\n chrome.runtime.onMessage = chrome.runtime.onMessage || evtStub;\n chrome.runtime.onConnect = chrome.runtime.onConnect || evtStub;\n chrome.runtime.sendMessage = chrome.runtime.sendMessage || noop;\n chrome.runtime.connect = chrome.runtime.connect || function() {\n return { onMessage: { addListener: noop }, postMessage: noop, disconnect: noop };\n };\n if (chrome.runtime.id === undefined) chrome.runtime.id = undefined;\n if (!chrome.loadTimes) chrome.loadTimes = function() { return {}; };\n if (!chrome.csi) chrome.csi = function() { return {}; };\n if (!chrome.app) {\n chrome.app = {\n isInstalled: false,\n InstallState: { INSTALLED: 'installed', NOT_INSTALLED: 'not_installed', DISABLED: 'disabled' },\n RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },\n getDetails: function() { return null; },\n getIsInstalled: function() { return false; },\n runningState: function() { return 'cannot_run'; },\n };\n }\n\n if (!window.chrome) {\n Object.defineProperty(window, 'chrome', { value: chrome, writable: false, enumerable: true, configurable: false });\n }\n });\n\n // \u2500\u2500 5. Permissions API consistency \u2500\u2500\n p(function() {\n var orig = navigator.permissions.query.bind(navigator.permissions);\n function q(params) {\n if (params.name === 'notifications') {\n return Promise.resolve({\n state: typeof Notification !== 'undefined' ? Notification.permission : 'prompt',\n name: 'notifications', onchange: null,\n addEventListener: function(){}, removeEventListener: function(){}, dispatchEvent: function(){ return true; },\n });\n }\n return orig(params);\n }\n q.toString = function() { return 'function query() { [native code] }'; };\n navigator.permissions.query = q;\n });\n\n // \u2500\u2500 6. WebGL vendor / renderer \u2500\u2500\n p(function() {\n var h = {\n apply: function(target, self, args) {\n var param = args[0];\n if (param === 0x9245) return 'Intel Inc.';\n if (param === 0x9246) return 'Intel Iris OpenGL Engine';\n return Reflect.apply(target, self, args);\n }\n };\n if (typeof WebGLRenderingContext !== 'undefined')\n WebGLRenderingContext.prototype.getParameter = new Proxy(WebGLRenderingContext.prototype.getParameter, h);\n if (typeof WebGL2RenderingContext !== 'undefined')\n WebGL2RenderingContext.prototype.getParameter = new Proxy(WebGL2RenderingContext.prototype.getParameter, h);\n });\n\n // \u2500\u2500 7. Notification.permission \u2500\u2500\n p(function() {\n if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {\n Object.defineProperty(Notification, 'permission', { get: function() { return 'default'; }, configurable: true });\n }\n });\n\n // \u2500\u2500 8. navigator.connection (cached so identity check passes) \u2500\u2500\n p(function() {\n if (navigator.connection) return;\n var conn = {\n effectiveType: '4g', rtt: 50, downlink: 10, saveData: false, onchange: null,\n addEventListener: function(){}, removeEventListener: function(){}, dispatchEvent: function(){ return true; },\n };\n Object.defineProperty(navigator, 'connection', { get: function() { return conn; } });\n });\n\n // \u2500\u2500 9. Iframe contentWindow.chrome \u2500\u2500\n // Handled by patch 4 \u2014 chrome object is now on window, propagates to iframes on same origin.\n\n // \u2500\u2500 10. console method toString \u2500\u2500\n p(function() {\n ['log','info','warn','error','debug','table','trace'].forEach(function(n) {\n if (console[n]) {\n console[n].toString = function() { return 'function ' + n + '() { [native code] }'; };\n }\n });\n });\n\n // \u2500\u2500 11. Headless-mode window / screen fixes \u2500\u2500\n p(function() {\n if (window.outerWidth === 0)\n Object.defineProperty(window, 'outerWidth', { get: function() { return window.innerWidth || 1920; } });\n if (window.outerHeight === 0)\n Object.defineProperty(window, 'outerHeight', { get: function() { return (window.innerHeight || 1080) + 85; } });\n });\n\n p(function() {\n if (screen.colorDepth === 0) {\n Object.defineProperty(screen, 'colorDepth', { get: function() { return 24; } });\n Object.defineProperty(screen, 'pixelDepth', { get: function() { return 24; } });\n }\n });\n\n // \u2500\u2500 12. navigator.hardwareConcurrency \u2500\u2500\n p(function() {\n if (!navigator.hardwareConcurrency)\n Object.defineProperty(navigator, 'hardwareConcurrency', { get: function() { return 4; } });\n });\n\n // \u2500\u2500 13. navigator.deviceMemory \u2500\u2500\n p(function() {\n if (!navigator.deviceMemory)\n Object.defineProperty(navigator, 'deviceMemory', { get: function() { return 8; } });\n });\n})()";
|
|
1526
|
+
|
|
1527
|
+
/**
|
|
1528
|
+
* Detect whether the current page is showing an anti-bot challenge.
|
|
1529
|
+
* Returns `null` if no challenge is detected.
|
|
1530
|
+
*/
|
|
1531
|
+
declare function detectChallengeViaPlaywright(opts: {
|
|
1532
|
+
cdpUrl: string;
|
|
1533
|
+
targetId?: string;
|
|
1534
|
+
}): Promise<ChallengeInfo | null>;
|
|
1535
|
+
/**
|
|
1536
|
+
* Wait for an anti-bot challenge to resolve on its own (e.g. Cloudflare JS challenge).
|
|
1537
|
+
*
|
|
1538
|
+
* Returns `{ resolved: true }` if the challenge cleared within the timeout,
|
|
1539
|
+
* or `{ resolved: false, challenge }` with the still-present challenge info.
|
|
1540
|
+
*
|
|
1541
|
+
* For challenges that require human interaction (CAPTCHA), this will time out
|
|
1542
|
+
* unless the user solves the challenge in the visible browser window.
|
|
1543
|
+
*/
|
|
1544
|
+
declare function waitForChallengeViaPlaywright(opts: {
|
|
1545
|
+
cdpUrl: string;
|
|
1546
|
+
targetId?: string;
|
|
1547
|
+
timeoutMs?: number;
|
|
1548
|
+
pollMs?: number;
|
|
1549
|
+
}): Promise<ChallengeWaitResult>;
|
|
1550
|
+
|
|
1551
|
+
export { type AriaNode, type AriaSnapshotResult, type BatchAction, type BatchActionResult, BrowserClaw, type BrowserNavigationPolicyOptions, type BrowserNavigationRequestLike, type BrowserTab, BrowserTabNotFoundError, type ChallengeInfo, type ChallengeKind, type ChallengeWaitResult, type ChromeExecutable, type ChromeKind, type ClickOptions, type ColorScheme, type ConnectOptions, type ConsoleMessage, type ContextState, type CookieData, CrawlPage, type DialogOptions, type DownloadResult, type FormField, type FrameEvalResult, type GeolocationOptions, type HttpCredentials, InvalidBrowserNavigationUrlError, type LaunchOptions, type LookupFn, type NetworkRequest, type PageError, type PinnedHostname, type ResponseBodyResult, type RoleRefInfo, type RoleRefs, STEALTH_SCRIPT, type ScreenshotOptions, type SnapshotOptions, type SnapshotResult, type SnapshotStats, type SsrfPolicy, type StorageKind, type TraceStartOptions, type TypeOptions, type UntrustedContentMeta, type WaitOptions, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, detectChallengeViaPlaywright, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, waitForChallengeViaPlaywright, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
|
package/dist/index.js
CHANGED
|
@@ -1284,7 +1284,20 @@ async function fetchChromeVersion(cdpUrl, timeoutMs = 500, authToken) {
|
|
|
1284
1284
|
async function isChromeReachable(cdpUrl, timeoutMs = 500, authToken) {
|
|
1285
1285
|
if (isWebSocketUrl(cdpUrl)) return await canOpenWebSocket(cdpUrl, timeoutMs);
|
|
1286
1286
|
const version = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
1287
|
-
|
|
1287
|
+
if (version !== null) return true;
|
|
1288
|
+
let isLoopback = false;
|
|
1289
|
+
try {
|
|
1290
|
+
const u = new URL(cdpUrl.startsWith("http") ? cdpUrl : `http://${cdpUrl}`);
|
|
1291
|
+
isLoopback = isLoopbackHost(u.hostname);
|
|
1292
|
+
} catch {
|
|
1293
|
+
}
|
|
1294
|
+
if (!isLoopback) return false;
|
|
1295
|
+
for (let i = 0; i < 2; i++) {
|
|
1296
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
1297
|
+
const retry = await fetchChromeVersion(cdpUrl, timeoutMs, authToken);
|
|
1298
|
+
if (retry !== null) return true;
|
|
1299
|
+
}
|
|
1300
|
+
return false;
|
|
1288
1301
|
}
|
|
1289
1302
|
async function getChromeWebSocketUrl(cdpUrl, timeoutMs = 500, authToken) {
|
|
1290
1303
|
if (isWebSocketUrl(cdpUrl)) return cdpUrl;
|
|
@@ -1476,6 +1489,198 @@ async function stopChrome(running, timeoutMs = 2500) {
|
|
|
1476
1489
|
}
|
|
1477
1490
|
}
|
|
1478
1491
|
|
|
1492
|
+
// src/stealth.ts
|
|
1493
|
+
var STEALTH_SCRIPT = `(function() {
|
|
1494
|
+
'use strict';
|
|
1495
|
+
function p(fn) { try { fn(); } catch(_) {} }
|
|
1496
|
+
|
|
1497
|
+
// \u2500\u2500 1. navigator.webdriver \u2192 undefined \u2500\u2500
|
|
1498
|
+
p(function() {
|
|
1499
|
+
Object.defineProperty(navigator, 'webdriver', { get: function() { return undefined; }, configurable: true });
|
|
1500
|
+
});
|
|
1501
|
+
|
|
1502
|
+
// \u2500\u2500 2. navigator.plugins + mimeTypes (only if empty \u2014 Chrome 92+ populates them natively) \u2500\u2500
|
|
1503
|
+
p(function() {
|
|
1504
|
+
if (navigator.plugins && navigator.plugins.length > 0) return;
|
|
1505
|
+
|
|
1506
|
+
function FakePlugin(name, fn, desc, mimes) {
|
|
1507
|
+
this.name = name; this.filename = fn; this.description = desc; this.length = mimes.length;
|
|
1508
|
+
for (var i = 0; i < mimes.length; i++) { this[i] = mimes[i]; mimes[i].enabledPlugin = this; }
|
|
1509
|
+
}
|
|
1510
|
+
FakePlugin.prototype.item = function(i) { return this[i] || null; };
|
|
1511
|
+
FakePlugin.prototype.namedItem = function(n) {
|
|
1512
|
+
for (var i = 0; i < this.length; i++) if (this[i].type === n) return this[i];
|
|
1513
|
+
return null;
|
|
1514
|
+
};
|
|
1515
|
+
|
|
1516
|
+
function M(type, suf, desc) { this.type = type; this.suffixes = suf; this.description = desc; }
|
|
1517
|
+
|
|
1518
|
+
var m1 = new M('application/pdf', 'pdf', 'Portable Document Format');
|
|
1519
|
+
var m2 = new M('application/x-google-chrome-pdf', 'pdf', 'Portable Document Format');
|
|
1520
|
+
var m3 = new M('application/x-nacl', '', 'Native Client Executable');
|
|
1521
|
+
var m4 = new M('application/x-pnacl', '', 'Portable Native Client Executable');
|
|
1522
|
+
|
|
1523
|
+
var plugins = [
|
|
1524
|
+
new FakePlugin('Chrome PDF Plugin', 'internal-pdf-viewer', 'Portable Document Format', [m1]),
|
|
1525
|
+
new FakePlugin('Chrome PDF Viewer', 'mhjfbmdgcfjbbpaeojofohoefgiehjai', '', [m2]),
|
|
1526
|
+
new FakePlugin('Native Client', 'internal-nacl-plugin', '', [m3, m4]),
|
|
1527
|
+
];
|
|
1528
|
+
|
|
1529
|
+
function makeIterable(arr, items) {
|
|
1530
|
+
arr.length = items.length;
|
|
1531
|
+
for (var i = 0; i < items.length; i++) arr[i] = items[i];
|
|
1532
|
+
arr[Symbol.iterator] = function() {
|
|
1533
|
+
var idx = 0;
|
|
1534
|
+
return { next: function() {
|
|
1535
|
+
return idx < items.length ? { value: items[idx++], done: false } : { done: true };
|
|
1536
|
+
}};
|
|
1537
|
+
};
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
var pa = { item: function(i) { return plugins[i] || null; },
|
|
1541
|
+
namedItem: function(n) { for (var i = 0; i < plugins.length; i++) if (plugins[i].name === n) return plugins[i]; return null; },
|
|
1542
|
+
refresh: function() {} };
|
|
1543
|
+
makeIterable(pa, plugins);
|
|
1544
|
+
Object.defineProperty(navigator, 'plugins', { get: function() { return pa; } });
|
|
1545
|
+
|
|
1546
|
+
var allMimes = [m1, m2, m3, m4];
|
|
1547
|
+
var ma = { item: function(i) { return allMimes[i] || null; },
|
|
1548
|
+
namedItem: function(n) { for (var i = 0; i < allMimes.length; i++) if (allMimes[i].type === n) return allMimes[i]; return null; } };
|
|
1549
|
+
makeIterable(ma, allMimes);
|
|
1550
|
+
Object.defineProperty(navigator, 'mimeTypes', { get: function() { return ma; } });
|
|
1551
|
+
});
|
|
1552
|
+
|
|
1553
|
+
// \u2500\u2500 3. navigator.languages (cached + frozen so identity check passes) \u2500\u2500
|
|
1554
|
+
p(function() {
|
|
1555
|
+
if (!navigator.languages || navigator.languages.length === 0) {
|
|
1556
|
+
var langs = Object.freeze(['en-US', 'en']);
|
|
1557
|
+
Object.defineProperty(navigator, 'languages', { get: function() { return langs; } });
|
|
1558
|
+
}
|
|
1559
|
+
});
|
|
1560
|
+
|
|
1561
|
+
// \u2500\u2500 4. window.chrome \u2500\u2500
|
|
1562
|
+
p(function() {
|
|
1563
|
+
if (window.chrome && window.chrome.runtime && window.chrome.runtime.connect) return;
|
|
1564
|
+
|
|
1565
|
+
var chrome = window.chrome || {};
|
|
1566
|
+
var noop = function() {};
|
|
1567
|
+
var evtStub = { addListener: noop, removeListener: noop, hasListeners: function() { return false; } };
|
|
1568
|
+
chrome.runtime = chrome.runtime || {};
|
|
1569
|
+
chrome.runtime.onMessage = chrome.runtime.onMessage || evtStub;
|
|
1570
|
+
chrome.runtime.onConnect = chrome.runtime.onConnect || evtStub;
|
|
1571
|
+
chrome.runtime.sendMessage = chrome.runtime.sendMessage || noop;
|
|
1572
|
+
chrome.runtime.connect = chrome.runtime.connect || function() {
|
|
1573
|
+
return { onMessage: { addListener: noop }, postMessage: noop, disconnect: noop };
|
|
1574
|
+
};
|
|
1575
|
+
if (chrome.runtime.id === undefined) chrome.runtime.id = undefined;
|
|
1576
|
+
if (!chrome.loadTimes) chrome.loadTimes = function() { return {}; };
|
|
1577
|
+
if (!chrome.csi) chrome.csi = function() { return {}; };
|
|
1578
|
+
if (!chrome.app) {
|
|
1579
|
+
chrome.app = {
|
|
1580
|
+
isInstalled: false,
|
|
1581
|
+
InstallState: { INSTALLED: 'installed', NOT_INSTALLED: 'not_installed', DISABLED: 'disabled' },
|
|
1582
|
+
RunningState: { CANNOT_RUN: 'cannot_run', READY_TO_RUN: 'ready_to_run', RUNNING: 'running' },
|
|
1583
|
+
getDetails: function() { return null; },
|
|
1584
|
+
getIsInstalled: function() { return false; },
|
|
1585
|
+
runningState: function() { return 'cannot_run'; },
|
|
1586
|
+
};
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
if (!window.chrome) {
|
|
1590
|
+
Object.defineProperty(window, 'chrome', { value: chrome, writable: false, enumerable: true, configurable: false });
|
|
1591
|
+
}
|
|
1592
|
+
});
|
|
1593
|
+
|
|
1594
|
+
// \u2500\u2500 5. Permissions API consistency \u2500\u2500
|
|
1595
|
+
p(function() {
|
|
1596
|
+
var orig = navigator.permissions.query.bind(navigator.permissions);
|
|
1597
|
+
function q(params) {
|
|
1598
|
+
if (params.name === 'notifications') {
|
|
1599
|
+
return Promise.resolve({
|
|
1600
|
+
state: typeof Notification !== 'undefined' ? Notification.permission : 'prompt',
|
|
1601
|
+
name: 'notifications', onchange: null,
|
|
1602
|
+
addEventListener: function(){}, removeEventListener: function(){}, dispatchEvent: function(){ return true; },
|
|
1603
|
+
});
|
|
1604
|
+
}
|
|
1605
|
+
return orig(params);
|
|
1606
|
+
}
|
|
1607
|
+
q.toString = function() { return 'function query() { [native code] }'; };
|
|
1608
|
+
navigator.permissions.query = q;
|
|
1609
|
+
});
|
|
1610
|
+
|
|
1611
|
+
// \u2500\u2500 6. WebGL vendor / renderer \u2500\u2500
|
|
1612
|
+
p(function() {
|
|
1613
|
+
var h = {
|
|
1614
|
+
apply: function(target, self, args) {
|
|
1615
|
+
var param = args[0];
|
|
1616
|
+
if (param === 0x9245) return 'Intel Inc.';
|
|
1617
|
+
if (param === 0x9246) return 'Intel Iris OpenGL Engine';
|
|
1618
|
+
return Reflect.apply(target, self, args);
|
|
1619
|
+
}
|
|
1620
|
+
};
|
|
1621
|
+
if (typeof WebGLRenderingContext !== 'undefined')
|
|
1622
|
+
WebGLRenderingContext.prototype.getParameter = new Proxy(WebGLRenderingContext.prototype.getParameter, h);
|
|
1623
|
+
if (typeof WebGL2RenderingContext !== 'undefined')
|
|
1624
|
+
WebGL2RenderingContext.prototype.getParameter = new Proxy(WebGL2RenderingContext.prototype.getParameter, h);
|
|
1625
|
+
});
|
|
1626
|
+
|
|
1627
|
+
// \u2500\u2500 7. Notification.permission \u2500\u2500
|
|
1628
|
+
p(function() {
|
|
1629
|
+
if (typeof Notification !== 'undefined' && Notification.permission === 'denied') {
|
|
1630
|
+
Object.defineProperty(Notification, 'permission', { get: function() { return 'default'; }, configurable: true });
|
|
1631
|
+
}
|
|
1632
|
+
});
|
|
1633
|
+
|
|
1634
|
+
// \u2500\u2500 8. navigator.connection (cached so identity check passes) \u2500\u2500
|
|
1635
|
+
p(function() {
|
|
1636
|
+
if (navigator.connection) return;
|
|
1637
|
+
var conn = {
|
|
1638
|
+
effectiveType: '4g', rtt: 50, downlink: 10, saveData: false, onchange: null,
|
|
1639
|
+
addEventListener: function(){}, removeEventListener: function(){}, dispatchEvent: function(){ return true; },
|
|
1640
|
+
};
|
|
1641
|
+
Object.defineProperty(navigator, 'connection', { get: function() { return conn; } });
|
|
1642
|
+
});
|
|
1643
|
+
|
|
1644
|
+
// \u2500\u2500 9. Iframe contentWindow.chrome \u2500\u2500
|
|
1645
|
+
// Handled by patch 4 \u2014 chrome object is now on window, propagates to iframes on same origin.
|
|
1646
|
+
|
|
1647
|
+
// \u2500\u2500 10. console method toString \u2500\u2500
|
|
1648
|
+
p(function() {
|
|
1649
|
+
['log','info','warn','error','debug','table','trace'].forEach(function(n) {
|
|
1650
|
+
if (console[n]) {
|
|
1651
|
+
console[n].toString = function() { return 'function ' + n + '() { [native code] }'; };
|
|
1652
|
+
}
|
|
1653
|
+
});
|
|
1654
|
+
});
|
|
1655
|
+
|
|
1656
|
+
// \u2500\u2500 11. Headless-mode window / screen fixes \u2500\u2500
|
|
1657
|
+
p(function() {
|
|
1658
|
+
if (window.outerWidth === 0)
|
|
1659
|
+
Object.defineProperty(window, 'outerWidth', { get: function() { return window.innerWidth || 1920; } });
|
|
1660
|
+
if (window.outerHeight === 0)
|
|
1661
|
+
Object.defineProperty(window, 'outerHeight', { get: function() { return (window.innerHeight || 1080) + 85; } });
|
|
1662
|
+
});
|
|
1663
|
+
|
|
1664
|
+
p(function() {
|
|
1665
|
+
if (screen.colorDepth === 0) {
|
|
1666
|
+
Object.defineProperty(screen, 'colorDepth', { get: function() { return 24; } });
|
|
1667
|
+
Object.defineProperty(screen, 'pixelDepth', { get: function() { return 24; } });
|
|
1668
|
+
}
|
|
1669
|
+
});
|
|
1670
|
+
|
|
1671
|
+
// \u2500\u2500 12. navigator.hardwareConcurrency \u2500\u2500
|
|
1672
|
+
p(function() {
|
|
1673
|
+
if (!navigator.hardwareConcurrency)
|
|
1674
|
+
Object.defineProperty(navigator, 'hardwareConcurrency', { get: function() { return 4; } });
|
|
1675
|
+
});
|
|
1676
|
+
|
|
1677
|
+
// \u2500\u2500 13. navigator.deviceMemory \u2500\u2500
|
|
1678
|
+
p(function() {
|
|
1679
|
+
if (!navigator.deviceMemory)
|
|
1680
|
+
Object.defineProperty(navigator, 'deviceMemory', { get: function() { return 8; } });
|
|
1681
|
+
});
|
|
1682
|
+
})()`;
|
|
1683
|
+
|
|
1479
1684
|
// src/connection.ts
|
|
1480
1685
|
var BrowserTabNotFoundError = class extends Error {
|
|
1481
1686
|
constructor(message = "Tab not found") {
|
|
@@ -1716,7 +1921,6 @@ function ensurePageState(page) {
|
|
|
1716
1921
|
}
|
|
1717
1922
|
return state;
|
|
1718
1923
|
}
|
|
1719
|
-
var STEALTH_SCRIPT = `Object.defineProperty(navigator, 'webdriver', { get: () => undefined })`;
|
|
1720
1924
|
function applyStealthToPage(page) {
|
|
1721
1925
|
page.evaluate(STEALTH_SCRIPT).catch((e) => {
|
|
1722
1926
|
if (process.env.DEBUG !== void 0 && process.env.DEBUG !== "")
|
|
@@ -3536,6 +3740,99 @@ async function setTimezoneViaPlaywright(opts) {
|
|
|
3536
3740
|
});
|
|
3537
3741
|
}
|
|
3538
3742
|
|
|
3743
|
+
// src/anti-bot.ts
|
|
3744
|
+
var DETECT_CHALLENGE_SCRIPT = `(function() {
|
|
3745
|
+
var title = (document.title || '').toLowerCase();
|
|
3746
|
+
|
|
3747
|
+
// Cloudflare JS challenge
|
|
3748
|
+
if (title === 'just a moment...'
|
|
3749
|
+
|| document.querySelector('#challenge-running, #cf-please-wait, #challenge-form')
|
|
3750
|
+
|| title.indexOf('checking your browser') !== -1) {
|
|
3751
|
+
return { kind: 'cloudflare-js', message: 'Cloudflare JS challenge' };
|
|
3752
|
+
}
|
|
3753
|
+
|
|
3754
|
+
// Cloudflare block page (needs body text \u2014 read lazily)
|
|
3755
|
+
var body = null;
|
|
3756
|
+
function getBody() { if (body === null) body = (document.body && document.body.textContent) || ''; return body; }
|
|
3757
|
+
|
|
3758
|
+
if (title.indexOf('attention required') !== -1
|
|
3759
|
+
|| (document.querySelector('.cf-error-details') && getBody().indexOf('blocked') !== -1)) {
|
|
3760
|
+
return { kind: 'cloudflare-block', message: 'Cloudflare block page' };
|
|
3761
|
+
}
|
|
3762
|
+
|
|
3763
|
+
// Cloudflare Turnstile
|
|
3764
|
+
if (document.querySelector('.cf-turnstile, iframe[src*="challenges.cloudflare.com"]')) {
|
|
3765
|
+
return { kind: 'cloudflare-turnstile', message: 'Cloudflare Turnstile challenge' };
|
|
3766
|
+
}
|
|
3767
|
+
|
|
3768
|
+
// hCaptcha
|
|
3769
|
+
if (document.querySelector('.h-captcha, iframe[src*="hcaptcha.com"]')) {
|
|
3770
|
+
return { kind: 'hcaptcha', message: 'hCaptcha challenge' };
|
|
3771
|
+
}
|
|
3772
|
+
|
|
3773
|
+
// reCAPTCHA
|
|
3774
|
+
if (document.querySelector('.g-recaptcha, iframe[src*="google.com/recaptcha"]')) {
|
|
3775
|
+
return { kind: 'recaptcha', message: 'reCAPTCHA challenge' };
|
|
3776
|
+
}
|
|
3777
|
+
|
|
3778
|
+
// Generic access-denied / rate-limit pages (only read body for short pages)
|
|
3779
|
+
var b = getBody();
|
|
3780
|
+
if (b.length < 5000) {
|
|
3781
|
+
if (/access denied|403 forbidden/i.test(title) || /access denied/i.test(b)) {
|
|
3782
|
+
return { kind: 'blocked', message: 'Access denied' };
|
|
3783
|
+
}
|
|
3784
|
+
if (/\\b429\\b/i.test(title) || /too many requests|rate limit/i.test(b)) {
|
|
3785
|
+
return { kind: 'rate-limited', message: 'Rate limited' };
|
|
3786
|
+
}
|
|
3787
|
+
}
|
|
3788
|
+
|
|
3789
|
+
return null;
|
|
3790
|
+
})()`;
|
|
3791
|
+
function parseChallengeResult(raw) {
|
|
3792
|
+
if (raw !== null && typeof raw === "object" && "kind" in raw) {
|
|
3793
|
+
return raw;
|
|
3794
|
+
}
|
|
3795
|
+
return null;
|
|
3796
|
+
}
|
|
3797
|
+
async function detectChallengeViaPlaywright(opts) {
|
|
3798
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3799
|
+
ensurePageState(page);
|
|
3800
|
+
return parseChallengeResult(await page.evaluate(DETECT_CHALLENGE_SCRIPT));
|
|
3801
|
+
}
|
|
3802
|
+
async function waitForChallengeViaPlaywright(opts) {
|
|
3803
|
+
const page = await getPageForTargetId({ cdpUrl: opts.cdpUrl, targetId: opts.targetId });
|
|
3804
|
+
ensurePageState(page);
|
|
3805
|
+
const timeout = normalizeTimeoutMs(opts.timeoutMs, 15e3);
|
|
3806
|
+
const poll = Math.max(250, Math.min(5e3, opts.pollMs ?? 500));
|
|
3807
|
+
const detect = async () => parseChallengeResult(await page.evaluate(DETECT_CHALLENGE_SCRIPT));
|
|
3808
|
+
const initial = await detect();
|
|
3809
|
+
if (initial === null) return { resolved: true, challenge: null };
|
|
3810
|
+
if (initial.kind === "cloudflare-js") {
|
|
3811
|
+
try {
|
|
3812
|
+
await page.waitForFunction(
|
|
3813
|
+
"document.title.toLowerCase() !== 'just a moment...' && !document.querySelector('#challenge-running')",
|
|
3814
|
+
void 0,
|
|
3815
|
+
{ timeout }
|
|
3816
|
+
);
|
|
3817
|
+
await page.waitForLoadState("domcontentloaded", { timeout: 5e3 }).catch(() => {
|
|
3818
|
+
});
|
|
3819
|
+
const after = await detect();
|
|
3820
|
+
return { resolved: after === null, challenge: after };
|
|
3821
|
+
} catch {
|
|
3822
|
+
const after = await detect();
|
|
3823
|
+
return { resolved: after === null, challenge: after };
|
|
3824
|
+
}
|
|
3825
|
+
}
|
|
3826
|
+
const deadline = Date.now() + timeout;
|
|
3827
|
+
while (Date.now() < deadline) {
|
|
3828
|
+
await page.waitForTimeout(poll);
|
|
3829
|
+
const current = await detect();
|
|
3830
|
+
if (current === null) return { resolved: true, challenge: null };
|
|
3831
|
+
}
|
|
3832
|
+
const final = await detect();
|
|
3833
|
+
return { resolved: final === null, challenge: final };
|
|
3834
|
+
}
|
|
3835
|
+
|
|
3539
3836
|
// src/capture/activity.ts
|
|
3540
3837
|
function consolePriority(level) {
|
|
3541
3838
|
switch (level) {
|
|
@@ -5140,6 +5437,53 @@ var CrawlPage = class {
|
|
|
5140
5437
|
name
|
|
5141
5438
|
});
|
|
5142
5439
|
}
|
|
5440
|
+
// ── Anti-Bot ──────────────────────────────────────────────────
|
|
5441
|
+
/**
|
|
5442
|
+
* Detect whether the page is showing an anti-bot challenge
|
|
5443
|
+
* (Cloudflare, hCaptcha, reCAPTCHA, access-denied, rate-limit, etc.).
|
|
5444
|
+
*
|
|
5445
|
+
* Returns `null` if no challenge is detected.
|
|
5446
|
+
*
|
|
5447
|
+
* @example
|
|
5448
|
+
* ```ts
|
|
5449
|
+
* const challenge = await page.detectChallenge();
|
|
5450
|
+
* if (challenge) {
|
|
5451
|
+
* console.log(challenge.kind); // 'cloudflare-js'
|
|
5452
|
+
* console.log(challenge.message); // 'Cloudflare JS challenge'
|
|
5453
|
+
* }
|
|
5454
|
+
* ```
|
|
5455
|
+
*/
|
|
5456
|
+
async detectChallenge() {
|
|
5457
|
+
return detectChallengeViaPlaywright({ cdpUrl: this.cdpUrl, targetId: this.targetId });
|
|
5458
|
+
}
|
|
5459
|
+
/**
|
|
5460
|
+
* Wait for an anti-bot challenge to resolve on its own.
|
|
5461
|
+
*
|
|
5462
|
+
* Cloudflare JS challenges typically auto-resolve in ~5 seconds.
|
|
5463
|
+
* CAPTCHA challenges will only resolve if solved in a visible browser window.
|
|
5464
|
+
*
|
|
5465
|
+
* @param opts.timeoutMs - Maximum wait time (default: `15000`)
|
|
5466
|
+
* @param opts.pollMs - Poll interval (default: `500`)
|
|
5467
|
+
* @returns Whether the challenge resolved, and the remaining challenge info if not
|
|
5468
|
+
*
|
|
5469
|
+
* @example
|
|
5470
|
+
* ```ts
|
|
5471
|
+
* await page.goto('https://example.com');
|
|
5472
|
+
* const challenge = await page.detectChallenge();
|
|
5473
|
+
* if (challenge?.kind === 'cloudflare-js') {
|
|
5474
|
+
* const { resolved } = await page.waitForChallenge({ timeoutMs: 20000 });
|
|
5475
|
+
* if (!resolved) throw new Error('Challenge did not resolve');
|
|
5476
|
+
* }
|
|
5477
|
+
* ```
|
|
5478
|
+
*/
|
|
5479
|
+
async waitForChallenge(opts) {
|
|
5480
|
+
return waitForChallengeViaPlaywright({
|
|
5481
|
+
cdpUrl: this.cdpUrl,
|
|
5482
|
+
targetId: this.targetId,
|
|
5483
|
+
timeoutMs: opts?.timeoutMs,
|
|
5484
|
+
pollMs: opts?.pollMs
|
|
5485
|
+
});
|
|
5486
|
+
}
|
|
5143
5487
|
};
|
|
5144
5488
|
var BrowserClaw = class _BrowserClaw {
|
|
5145
5489
|
cdpUrl;
|
|
@@ -5285,6 +5629,6 @@ var BrowserClaw = class _BrowserClaw {
|
|
|
5285
5629
|
}
|
|
5286
5630
|
};
|
|
5287
5631
|
|
|
5288
|
-
export { BrowserClaw, BrowserTabNotFoundError, CrawlPage, InvalidBrowserNavigationUrlError, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
|
|
5632
|
+
export { BrowserClaw, BrowserTabNotFoundError, CrawlPage, InvalidBrowserNavigationUrlError, STEALTH_SCRIPT, assertBrowserNavigationAllowed, assertBrowserNavigationRedirectChainAllowed, assertBrowserNavigationResultAllowed, assertSafeUploadPaths, batchViaPlaywright, createPinnedLookup, detectChallengeViaPlaywright, ensureContextState, executeSingleAction, forceDisconnectPlaywrightForTarget, getChromeWebSocketUrl, getRestoredPageForTarget, isChromeCdpReady, isChromeReachable, normalizeCdpHttpBaseForJsonEndpoints, parseRoleRef, requireRef, requireRefOrSelector, requiresInspectableBrowserNavigationRedirects, resolveBoundedDelayMs, resolveInteractionTimeoutMs, resolvePageByTargetIdOrThrow, resolvePinnedHostnameWithPolicy, resolveStrictExistingUploadPaths, sanitizeUntrustedFileName, waitForChallengeViaPlaywright, withBrowserNavigationPolicy, withPageScopedCdpClient, withPlaywrightPageCdpSession, writeViaSiblingTempPath };
|
|
5289
5633
|
//# sourceMappingURL=index.js.map
|
|
5290
5634
|
//# sourceMappingURL=index.js.map
|