teamshare-bridge 0.27.0 → 0.27.1
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/cli/commands/research.js +30 -3
- package/dist/cli/commands/research.js.map +1 -1
- package/dist/lib/api.d.ts +4 -0
- package/dist/lib/api.js.map +1 -1
- package/dist/lib/browser/batch.d.ts +5 -0
- package/dist/lib/browser/batch.js +29 -0
- package/dist/lib/browser/batch.js.map +1 -1
- package/dist/lib/browser/challenge.d.ts +85 -0
- package/dist/lib/browser/challenge.js +164 -0
- package/dist/lib/browser/challenge.js.map +1 -0
- package/dist/lib/browser/launch.d.ts +9 -0
- package/dist/lib/browser/launch.js +11 -9
- package/dist/lib/browser/launch.js.map +1 -1
- package/dist/lib/browser/session.d.ts +50 -1
- package/dist/lib/browser/session.js +162 -2
- package/dist/lib/browser/session.js.map +1 -1
- package/dist/lib/browser/stealth.d.ts +20 -0
- package/dist/lib/browser/stealth.js +86 -0
- package/dist/lib/browser/stealth.js.map +1 -0
- package/dist/lib/orchestrator/handlers/browser.js +92 -3
- package/dist/lib/orchestrator/handlers/browser.js.map +1 -1
- package/dist/lib/tools/docx-source.d.ts +6 -4
- package/dist/lib/tools/docx-source.js +27 -4
- package/dist/lib/tools/docx-source.js.map +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cloudflare / CAPTCHA challenge handling (2026-10).
|
|
3
|
+
*
|
|
4
|
+
* The debug browser used to click blindly at a challenge page for as long as
|
|
5
|
+
* the agent kept trying. A managed challenge is not a button: it either passes
|
|
6
|
+
* on its own after Cloudflare's JS runs, or it needs a human. This module:
|
|
7
|
+
*
|
|
8
|
+
* - DETECTS a challenge from the live DOM (title, challenge form, Turnstile /
|
|
9
|
+
* reCAPTCHA / hCaptcha iframe, block page);
|
|
10
|
+
* - makes at most ONE bounded attempt (a single click on a Turnstile widget,
|
|
11
|
+
* or a passive wait for a managed challenge) within a hard timeout;
|
|
12
|
+
* - returns a structured verdict so the agent stops and asks a human instead
|
|
13
|
+
* of sitting on the challenge.
|
|
14
|
+
*
|
|
15
|
+
* It is deliberately NOT a CAPTCHA solver: an image grid is never attempted,
|
|
16
|
+
* and an interactive Turnstile that does not clear after one click is reported
|
|
17
|
+
* as `CHALLENGE_REQUIRED`. The prompt then pauses the run with `ask_human`;
|
|
18
|
+
* the human solves the challenge in the visible window and the answer resumes
|
|
19
|
+
* the same session.
|
|
20
|
+
*/
|
|
21
|
+
import type { CdpPage } from './cdp';
|
|
22
|
+
export type ChallengeVendor = 'cloudflare' | 'turnstile' | 'recaptcha' | 'hcaptcha' | 'unknown';
|
|
23
|
+
export type ChallengeKind =
|
|
24
|
+
/** "Just a moment..." / "Checking your browser" — usually clears itself. */
|
|
25
|
+
'managed'
|
|
26
|
+
/** A visible Turnstile checkbox/widget — one bounded click is worth trying. */
|
|
27
|
+
| 'turnstile'
|
|
28
|
+
/** reCAPTCHA / hCaptcha image challenge — never attempted automatically. */
|
|
29
|
+
| 'captcha'
|
|
30
|
+
/** Cloudflare block page (WAF / rate limit) — not a challenge to solve. */
|
|
31
|
+
| 'block';
|
|
32
|
+
export interface ChallengeState {
|
|
33
|
+
detected: true;
|
|
34
|
+
vendor: ChallengeVendor;
|
|
35
|
+
kind: ChallengeKind;
|
|
36
|
+
title: string;
|
|
37
|
+
url: string;
|
|
38
|
+
/** Widget centre (CSS pixels) when a visible widget was found. */
|
|
39
|
+
widget?: {
|
|
40
|
+
x: number;
|
|
41
|
+
y: number;
|
|
42
|
+
width: number;
|
|
43
|
+
height: number;
|
|
44
|
+
};
|
|
45
|
+
/** Short page excerpt for the agent/human (capped). */
|
|
46
|
+
text?: string;
|
|
47
|
+
}
|
|
48
|
+
export interface ChallengeResolution {
|
|
49
|
+
detected: boolean;
|
|
50
|
+
attempted: boolean;
|
|
51
|
+
passed: boolean;
|
|
52
|
+
elapsedMs: number;
|
|
53
|
+
/** The challenge as detected (first sighting), when one existed. */
|
|
54
|
+
challenge?: ChallengeState;
|
|
55
|
+
/** True when a previous failure parked this host; no attempt was made. */
|
|
56
|
+
cooldown?: boolean;
|
|
57
|
+
/** What the caller should do next when `passed` is false. */
|
|
58
|
+
nextStep?: 'ask_human' | 'report_blocked';
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* The in-page probe. Exported so the unit test can syntax-check it; it must
|
|
62
|
+
* stay a self-contained expression returning a JSON string.
|
|
63
|
+
*/
|
|
64
|
+
export declare function buildChallengeProbeScript(): string;
|
|
65
|
+
/**
|
|
66
|
+
* Read the live page for challenge markers. Never throws — a navigation or a
|
|
67
|
+
* destroyed execution context simply means "no challenge observed".
|
|
68
|
+
*/
|
|
69
|
+
export declare function detectChallenge(page: CdpPage): Promise<ChallengeState | null>;
|
|
70
|
+
/**
|
|
71
|
+
* Bounded resolution. Callers inject `detect`/`click` so this stays pure with
|
|
72
|
+
* respect to the CDP client and can be unit-tested.
|
|
73
|
+
*
|
|
74
|
+
* - managed / block / captcha: never clicked. Managed may pass on its own.
|
|
75
|
+
* - turnstile: ONE click on the widget, then wait for clearance.
|
|
76
|
+
* - Anything still challenged at the deadline returns `passed:false` with
|
|
77
|
+
* `nextStep` set, so the caller can stop and ask a human.
|
|
78
|
+
*/
|
|
79
|
+
export declare function resolveChallenge(opts: {
|
|
80
|
+
detect: () => Promise<ChallengeState | null>;
|
|
81
|
+
click?: (x: number, y: number) => Promise<void>;
|
|
82
|
+
attempt?: boolean;
|
|
83
|
+
timeoutMs?: number;
|
|
84
|
+
pollMs?: number;
|
|
85
|
+
}): Promise<ChallengeResolution>;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildChallengeProbeScript = buildChallengeProbeScript;
|
|
4
|
+
exports.detectChallenge = detectChallenge;
|
|
5
|
+
exports.resolveChallenge = resolveChallenge;
|
|
6
|
+
const cdp_1 = require("./cdp");
|
|
7
|
+
const browse_types_1 = require("./browse-types");
|
|
8
|
+
const DEFAULT_TIMEOUT_MS = (() => {
|
|
9
|
+
const raw = Number(process.env.TEAMSHARE_CHALLENGE_TIMEOUT_MS);
|
|
10
|
+
return Number.isFinite(raw) && raw > 0 ? raw : 25_000;
|
|
11
|
+
})();
|
|
12
|
+
const POLL_MS = 500;
|
|
13
|
+
/**
|
|
14
|
+
* The in-page probe. Exported so the unit test can syntax-check it; it must
|
|
15
|
+
* stay a self-contained expression returning a JSON string.
|
|
16
|
+
*/
|
|
17
|
+
function buildChallengeProbeScript() {
|
|
18
|
+
return `(() => {
|
|
19
|
+
const title = document.title || '';
|
|
20
|
+
const url = location.href;
|
|
21
|
+
const body = document.body ? (document.body.innerText || '') : '';
|
|
22
|
+
const text = body.slice(0, 1200);
|
|
23
|
+
const lower = text.toLowerCase();
|
|
24
|
+
const hay = (title + ' ' + lower).toLowerCase();
|
|
25
|
+
|
|
26
|
+
const iframe = Array.from(document.querySelectorAll('iframe')).find((f) => {
|
|
27
|
+
const src = String(f.getAttribute('src') || f.src || '').toLowerCase();
|
|
28
|
+
return src.includes('challenges.cloudflare.com') ||
|
|
29
|
+
src.includes('turnstile') ||
|
|
30
|
+
src.includes('recaptcha') ||
|
|
31
|
+
src.includes('hcaptcha');
|
|
32
|
+
});
|
|
33
|
+
const src = iframe ? String(iframe.getAttribute('src') || iframe.src || '').toLowerCase() : '';
|
|
34
|
+
const turnstileEl = document.querySelector(
|
|
35
|
+
'.cf-turnstile, [data-sitekey], input[name="cf-turnstile-response"], #cf-chl-widget',
|
|
36
|
+
);
|
|
37
|
+
const managedEl = document.querySelector(
|
|
38
|
+
'#challenge-form, #challenge-stage, #cf-please-wait, #cf-challenge-running, #cf-verify, .cf-browser-verification',
|
|
39
|
+
);
|
|
40
|
+
const blockEl = document.querySelector('#cf-error-details, .cf-error-details');
|
|
41
|
+
|
|
42
|
+
let vendor = 'unknown';
|
|
43
|
+
let kind = null;
|
|
44
|
+
if (blockEl || /attention required|access denied|you have been blocked|error 10\\d\\d/i.test(hay)) {
|
|
45
|
+
vendor = 'cloudflare';
|
|
46
|
+
kind = 'block';
|
|
47
|
+
} else if (src.includes('recaptcha')) {
|
|
48
|
+
vendor = 'recaptcha';
|
|
49
|
+
kind = 'captcha';
|
|
50
|
+
} else if (src.includes('hcaptcha')) {
|
|
51
|
+
vendor = 'hcaptcha';
|
|
52
|
+
kind = 'captcha';
|
|
53
|
+
} else if (src.includes('turnstile') || src.includes('challenges.cloudflare.com') || turnstileEl) {
|
|
54
|
+
vendor = 'turnstile';
|
|
55
|
+
kind = 'turnstile';
|
|
56
|
+
} else if (managedEl || /just a moment|checking your browser|verify you are human|enable javascript and cookies|cf-chl/i.test(hay)) {
|
|
57
|
+
vendor = 'cloudflare';
|
|
58
|
+
kind = 'managed';
|
|
59
|
+
}
|
|
60
|
+
if (!kind) return JSON.stringify({ detected: false });
|
|
61
|
+
|
|
62
|
+
let widget = null;
|
|
63
|
+
const el = iframe || turnstileEl;
|
|
64
|
+
if (el) {
|
|
65
|
+
const r = el.getBoundingClientRect();
|
|
66
|
+
if (r.width > 4 && r.height > 4) {
|
|
67
|
+
// Turnstile's checkbox sits ~30px from the widget's left edge;
|
|
68
|
+
// clicking the exact centre hits the label text instead.
|
|
69
|
+
const x = Math.round(r.x + Math.min(30, r.width / 2));
|
|
70
|
+
const y = Math.round(r.y + r.height / 2);
|
|
71
|
+
widget = { x, y, width: Math.round(r.width), height: Math.round(r.height) };
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return JSON.stringify({
|
|
75
|
+
detected: true,
|
|
76
|
+
vendor,
|
|
77
|
+
kind,
|
|
78
|
+
title: title.slice(0, 200),
|
|
79
|
+
url,
|
|
80
|
+
widget,
|
|
81
|
+
text: text.slice(0, 600),
|
|
82
|
+
});
|
|
83
|
+
})()`;
|
|
84
|
+
}
|
|
85
|
+
/**
|
|
86
|
+
* Read the live page for challenge markers. Never throws — a navigation or a
|
|
87
|
+
* destroyed execution context simply means "no challenge observed".
|
|
88
|
+
*/
|
|
89
|
+
async function detectChallenge(page) {
|
|
90
|
+
try {
|
|
91
|
+
const raw = await (0, cdp_1.evaluateJs)(page, buildChallengeProbeScript());
|
|
92
|
+
const parsed = JSON.parse(String(raw ?? '{"detected":false}'));
|
|
93
|
+
return parsed && parsed.detected ? parsed : null;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* Bounded resolution. Callers inject `detect`/`click` so this stays pure with
|
|
101
|
+
* respect to the CDP client and can be unit-tested.
|
|
102
|
+
*
|
|
103
|
+
* - managed / block / captcha: never clicked. Managed may pass on its own.
|
|
104
|
+
* - turnstile: ONE click on the widget, then wait for clearance.
|
|
105
|
+
* - Anything still challenged at the deadline returns `passed:false` with
|
|
106
|
+
* `nextStep` set, so the caller can stop and ask a human.
|
|
107
|
+
*/
|
|
108
|
+
async function resolveChallenge(opts) {
|
|
109
|
+
const started = Date.now();
|
|
110
|
+
const timeout = Math.min(Math.max(opts.timeoutMs ?? DEFAULT_TIMEOUT_MS, 1_000), 120_000);
|
|
111
|
+
const pollMs = Math.max(opts.pollMs ?? POLL_MS, 200);
|
|
112
|
+
const first = await opts.detect().catch(() => null);
|
|
113
|
+
if (!first) {
|
|
114
|
+
return {
|
|
115
|
+
detected: false,
|
|
116
|
+
attempted: false,
|
|
117
|
+
passed: true,
|
|
118
|
+
elapsedMs: Date.now() - started,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
const blockedKind = first.kind === 'block' || first.kind === 'captcha';
|
|
122
|
+
const canClick = opts.attempt !== false &&
|
|
123
|
+
first.kind === 'turnstile' &&
|
|
124
|
+
!!first.widget &&
|
|
125
|
+
typeof opts.click === 'function';
|
|
126
|
+
let attempted = false;
|
|
127
|
+
if (canClick) {
|
|
128
|
+
attempted = true;
|
|
129
|
+
try {
|
|
130
|
+
await opts.click(first.widget.x, first.widget.y);
|
|
131
|
+
// Give the widget a beat to react before polling; a second click is
|
|
132
|
+
// never sent - repeated clicks lower Cloudflare's score.
|
|
133
|
+
await (0, browse_types_1.sleep)(700);
|
|
134
|
+
}
|
|
135
|
+
catch {
|
|
136
|
+
/* a failed click is a failed attempt; polling still runs */
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
const deadline = started + (blockedKind ? 1_500 : timeout);
|
|
140
|
+
let last = first;
|
|
141
|
+
while (Date.now() < deadline) {
|
|
142
|
+
const now = await opts.detect().catch(() => null);
|
|
143
|
+
if (!now) {
|
|
144
|
+
return {
|
|
145
|
+
detected: true,
|
|
146
|
+
attempted,
|
|
147
|
+
passed: true,
|
|
148
|
+
elapsedMs: Date.now() - started,
|
|
149
|
+
challenge: first,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
last = now;
|
|
153
|
+
await (0, browse_types_1.sleep)(Math.min(pollMs, Math.max(0, deadline - Date.now())));
|
|
154
|
+
}
|
|
155
|
+
return {
|
|
156
|
+
detected: true,
|
|
157
|
+
attempted,
|
|
158
|
+
passed: false,
|
|
159
|
+
elapsedMs: Date.now() - started,
|
|
160
|
+
challenge: last ?? first,
|
|
161
|
+
nextStep: (last ?? first).kind === 'block' ? 'report_blocked' : 'ask_human',
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=challenge.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"challenge.js","sourceRoot":"","sources":["../../../src/lib/browser/challenge.ts"],"names":[],"mappings":"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"}
|
|
@@ -60,6 +60,15 @@ export interface LaunchOptions {
|
|
|
60
60
|
startUrl?: string;
|
|
61
61
|
/** Extra Chrome CLI args. */
|
|
62
62
|
args?: string[];
|
|
63
|
+
/**
|
|
64
|
+
* OPT-IN (2026-10): reduce the automation fingerprint. Drops the
|
|
65
|
+
* Puppeteer-style flags (`--disable-extensions`, `--disable-sync`, …) that
|
|
66
|
+
* sites use as automation tells and disables `AutomationControlled`. The
|
|
67
|
+
* session also injects the pre-document shims (webdriver/chrome/plugins).
|
|
68
|
+
* Off by default: a deterministic QA browser is intentionally boring, and
|
|
69
|
+
* aggressive spoofing can break a site that expects a normal Chrome.
|
|
70
|
+
*/
|
|
71
|
+
stealth?: boolean;
|
|
63
72
|
/** Timeout to wait for Chrome to become reachable (ms, default 15000). */
|
|
64
73
|
timeoutMs?: number;
|
|
65
74
|
}
|
|
@@ -74,16 +74,18 @@ async function launchChrome(options) {
|
|
|
74
74
|
const chromeArgs = [
|
|
75
75
|
`--remote-debugging-port=${port}`,
|
|
76
76
|
`--user-data-dir=${userDataDir}`,
|
|
77
|
-
'--no-first-run',
|
|
78
|
-
'--no-default-browser-check',
|
|
79
|
-
'--disable-background-networking',
|
|
80
|
-
'--disable-default-apps',
|
|
81
|
-
'--disable-extensions',
|
|
82
|
-
'--disable-sync',
|
|
83
|
-
'--disable-translate',
|
|
84
|
-
'--metrics-recording-only',
|
|
85
|
-
'--safebrowsing-disable-auto-update',
|
|
86
77
|
];
|
|
78
|
+
if (options?.stealth) {
|
|
79
|
+
// Minimal, ordinary-looking launch: the automation FORENSICS flags below
|
|
80
|
+
// are the ones a challenge script checks for. `AutomationControlled` off
|
|
81
|
+
// removes the `navigator.webdriver` tell at the source. First-run flags are
|
|
82
|
+
// kept because a fresh isolated profile otherwise opens a first-run tab
|
|
83
|
+
// that has nothing to do with automation.
|
|
84
|
+
chromeArgs.push('--no-first-run', '--no-default-browser-check', '--disable-blink-features=AutomationControlled');
|
|
85
|
+
}
|
|
86
|
+
else {
|
|
87
|
+
chromeArgs.push('--no-first-run', '--no-default-browser-check', '--disable-background-networking', '--disable-default-apps', '--disable-extensions', '--disable-sync', '--disable-translate', '--metrics-recording-only', '--safebrowsing-disable-auto-update');
|
|
88
|
+
}
|
|
87
89
|
if (headless) {
|
|
88
90
|
chromeArgs.push('--headless=new');
|
|
89
91
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"launch.js","sourceRoot":"","sources":["../../../src/lib/browser/launch.ts"],"names":[],"mappings":";;
|
|
1
|
+
{"version":3,"file":"launch.js","sourceRoot":"","sources":["../../../src/lib/browser/launch.ts"],"names":[],"mappings":"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"}
|
|
@@ -6,6 +6,7 @@ import { type FillOtpOptions, type FillOtpResult } from './otp';
|
|
|
6
6
|
import { type NativeDialogState, type PagePaginationState, type PageSnapshot, type WaitForKind } from './page-context';
|
|
7
7
|
import { type PageEvent } from './page-events';
|
|
8
8
|
import { type RefElement } from './refs';
|
|
9
|
+
import { type ChallengeResolution, type ChallengeState } from './challenge';
|
|
9
10
|
/**
|
|
10
11
|
* Persistent Chrome profile for the AUTO-ANSWER browser (research reads,
|
|
11
12
|
* `browse --auto-answer`, the web-auto-answer tool).
|
|
@@ -51,6 +52,12 @@ export interface BrowserSessionOptions {
|
|
|
51
52
|
isolated?: boolean;
|
|
52
53
|
/** Extra Chrome args for auto-launch. */
|
|
53
54
|
chromeArgs?: string[];
|
|
55
|
+
/**
|
|
56
|
+
* OPT-IN (2026-10): reduce the automation fingerprint. Drops the
|
|
57
|
+
* Puppeteer-style launch flags, disables `AutomationControlled` and injects
|
|
58
|
+
* the pre-document shims before any page script runs. Off by default.
|
|
59
|
+
*/
|
|
60
|
+
stealth?: boolean;
|
|
54
61
|
/**
|
|
55
62
|
* OPT-IN (default false): permit typing into password/credential fields.
|
|
56
63
|
*
|
|
@@ -81,6 +88,8 @@ export declare class BrowserSession {
|
|
|
81
88
|
private _headless;
|
|
82
89
|
private _profileDir;
|
|
83
90
|
private _chromeArgs;
|
|
91
|
+
/** Opt-in fingerprint reduction (see BrowserSessionOptions.stealth). */
|
|
92
|
+
private _stealth;
|
|
84
93
|
private _connected;
|
|
85
94
|
/** Opt-in credential entry (see BrowserSessionOptions.allowCredentials). */
|
|
86
95
|
private _allowCredentials;
|
|
@@ -113,11 +122,26 @@ export declare class BrowserSession {
|
|
|
113
122
|
* it. A native dialog blocks the renderer, so a page snapshot would hang.
|
|
114
123
|
*/
|
|
115
124
|
private _nativeDialog;
|
|
125
|
+
/**
|
|
126
|
+
* Per-host challenge cooldown (2026-10): after a failed bounded attempt the
|
|
127
|
+
* same host is NOT re-attempted for 10 minutes. Without this an agent loop
|
|
128
|
+
* could sit on a challenge page forever, which is exactly what the human
|
|
129
|
+
* sees as "stuck trying Cloudflare".
|
|
130
|
+
*/
|
|
131
|
+
private _challengeCooldown;
|
|
116
132
|
constructor(id: string, opts?: BrowserSessionOptions);
|
|
117
133
|
/** The stable session name the browser manager keys this session by. */
|
|
118
134
|
get sessionKey(): string;
|
|
119
135
|
/** The user-data-dir this session launches Chrome with. */
|
|
120
136
|
get profileDir(): string;
|
|
137
|
+
/** Whether this session launched with the opt-in stealth shims. */
|
|
138
|
+
get stealth(): boolean;
|
|
139
|
+
/**
|
|
140
|
+
* Turn on the opt-in stealth shims for a LIVE session (2026-10). The launch
|
|
141
|
+
* flags cannot change after Chrome started, but the pre-document shims can be
|
|
142
|
+
* registered now (future navigations) and applied to the current document.
|
|
143
|
+
*/
|
|
144
|
+
enableStealth(): Promise<void>;
|
|
121
145
|
/** The test-account label this window is tagged for (2026-11). */
|
|
122
146
|
get actor(): string | null;
|
|
123
147
|
/** Tag an untagged window. A tagged window never changes hands. */
|
|
@@ -268,7 +292,32 @@ export declare class BrowserSession {
|
|
|
268
292
|
truncated: boolean;
|
|
269
293
|
stoppedBecause: string;
|
|
270
294
|
}>;
|
|
271
|
-
navigate(url: string
|
|
295
|
+
navigate(url: string, opts?: {
|
|
296
|
+
autoChallenge?: boolean;
|
|
297
|
+
challengeTimeoutMs?: number;
|
|
298
|
+
}): Promise<PageInfo & {
|
|
299
|
+
challenge?: ChallengeResolution;
|
|
300
|
+
}>;
|
|
301
|
+
/** Detect a Cloudflare / CAPTCHA challenge on the live page (cheap, safe). */
|
|
302
|
+
detectChallenge(): Promise<ChallengeState | null>;
|
|
303
|
+
/**
|
|
304
|
+
* One bounded challenge attempt (2026-10): a single click for a Turnstile
|
|
305
|
+
* widget, passive wait for a managed challenge, nothing for a block/image
|
|
306
|
+
* CAPTCHA. Callers stop and ask a human when `passed` is false.
|
|
307
|
+
*/
|
|
308
|
+
resolveChallenge(opts?: {
|
|
309
|
+
attempt?: boolean;
|
|
310
|
+
timeoutMs?: number;
|
|
311
|
+
}): Promise<ChallengeResolution>;
|
|
312
|
+
/**
|
|
313
|
+
* Poll only (no click). Used after a human answered a `CHALLENGE_REQUIRED`
|
|
314
|
+
* pause: the human solved it in the visible window; this waits for the page
|
|
315
|
+
* to clear and returns the new screen.
|
|
316
|
+
*/
|
|
317
|
+
waitForChallenge(opts?: {
|
|
318
|
+
timeoutMs?: number;
|
|
319
|
+
attempt?: boolean;
|
|
320
|
+
}): Promise<ChallengeResolution>;
|
|
272
321
|
/**
|
|
273
322
|
* Set the browser viewport before a design-verification screenshot
|
|
274
323
|
* (2026-11): the design's viewport IS the contract — a pixel diff at any
|
|
@@ -60,6 +60,9 @@ const page_context_1 = require("./page-context");
|
|
|
60
60
|
const page_events_1 = require("./page-events");
|
|
61
61
|
const refs_1 = require("./refs");
|
|
62
62
|
const screen_change_1 = require("./screen-change");
|
|
63
|
+
const stealth_1 = require("./stealth");
|
|
64
|
+
const browse_types_1 = require("./browse-types");
|
|
65
|
+
const challenge_1 = require("./challenge");
|
|
63
66
|
/**
|
|
64
67
|
* Persistent Chrome profile for the AUTO-ANSWER browser (research reads,
|
|
65
68
|
* `browse --auto-answer`, the web-auto-answer tool).
|
|
@@ -89,6 +92,15 @@ exports.DEBUG_PROFILE_DIR = (0, node_path_1.join)((0, node_os_1.homedir)(), '.te
|
|
|
89
92
|
* CdpPage and must instrument it again.
|
|
90
93
|
*/
|
|
91
94
|
const instrumentedPages = new WeakSet();
|
|
95
|
+
/** Host of a URL for the per-host challenge cooldown ('' when unparseable). */
|
|
96
|
+
function hostOf(url) {
|
|
97
|
+
try {
|
|
98
|
+
return new URL(url).host.toLowerCase();
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
return '';
|
|
102
|
+
}
|
|
103
|
+
}
|
|
92
104
|
class BrowserSession {
|
|
93
105
|
id;
|
|
94
106
|
page = null;
|
|
@@ -98,6 +110,8 @@ class BrowserSession {
|
|
|
98
110
|
_headless;
|
|
99
111
|
_profileDir;
|
|
100
112
|
_chromeArgs;
|
|
113
|
+
/** Opt-in fingerprint reduction (see BrowserSessionOptions.stealth). */
|
|
114
|
+
_stealth;
|
|
101
115
|
_connected = false;
|
|
102
116
|
/** Opt-in credential entry (see BrowserSessionOptions.allowCredentials). */
|
|
103
117
|
_allowCredentials;
|
|
@@ -130,6 +144,13 @@ class BrowserSession {
|
|
|
130
144
|
* it. A native dialog blocks the renderer, so a page snapshot would hang.
|
|
131
145
|
*/
|
|
132
146
|
_nativeDialog = null;
|
|
147
|
+
/**
|
|
148
|
+
* Per-host challenge cooldown (2026-10): after a failed bounded attempt the
|
|
149
|
+
* same host is NOT re-attempted for 10 minutes. Without this an agent loop
|
|
150
|
+
* could sit on a challenge page forever, which is exactly what the human
|
|
151
|
+
* sees as "stuck trying Cloudflare".
|
|
152
|
+
*/
|
|
153
|
+
_challengeCooldown = null;
|
|
133
154
|
constructor(id, opts) {
|
|
134
155
|
this.id = id;
|
|
135
156
|
this._port = opts?.port ?? exports.DEBUG_CDP_PORT;
|
|
@@ -141,6 +162,7 @@ class BrowserSession {
|
|
|
141
162
|
? opts.actor.trim()
|
|
142
163
|
: null;
|
|
143
164
|
this._chromeArgs = opts?.chromeArgs ?? [];
|
|
165
|
+
this._stealth = opts?.stealth === true;
|
|
144
166
|
this._allowCredentials = opts?.allowCredentials ?? false;
|
|
145
167
|
this._qaVision = opts?.qaVision ?? false;
|
|
146
168
|
}
|
|
@@ -152,6 +174,36 @@ class BrowserSession {
|
|
|
152
174
|
get profileDir() {
|
|
153
175
|
return this._profileDir;
|
|
154
176
|
}
|
|
177
|
+
/** Whether this session launched with the opt-in stealth shims. */
|
|
178
|
+
get stealth() {
|
|
179
|
+
return this._stealth;
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Turn on the opt-in stealth shims for a LIVE session (2026-10). The launch
|
|
183
|
+
* flags cannot change after Chrome started, but the pre-document shims can be
|
|
184
|
+
* registered now (future navigations) and applied to the current document.
|
|
185
|
+
*/
|
|
186
|
+
async enableStealth() {
|
|
187
|
+
if (this._stealth)
|
|
188
|
+
return;
|
|
189
|
+
this._stealth = true;
|
|
190
|
+
if (!this.page)
|
|
191
|
+
return;
|
|
192
|
+
try {
|
|
193
|
+
await (0, cdp_1.cdpSend)(this.page, 'Page.addScriptToEvaluateOnNewDocument', {
|
|
194
|
+
source: (0, stealth_1.buildStealthScript)(),
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
/* best-effort */
|
|
199
|
+
}
|
|
200
|
+
try {
|
|
201
|
+
await (0, cdp_1.evaluateJs)(this.page, (0, stealth_1.buildStealthScript)());
|
|
202
|
+
}
|
|
203
|
+
catch {
|
|
204
|
+
/* document may be mid-navigation */
|
|
205
|
+
}
|
|
206
|
+
}
|
|
155
207
|
/** The test-account label this window is tagged for (2026-11). */
|
|
156
208
|
get actor() {
|
|
157
209
|
return this._actor;
|
|
@@ -187,6 +239,8 @@ class BrowserSession {
|
|
|
187
239
|
this.setActor(opts.actor);
|
|
188
240
|
if (opts.chromeArgs)
|
|
189
241
|
this._chromeArgs = opts.chromeArgs;
|
|
242
|
+
if (opts.stealth !== undefined)
|
|
243
|
+
this._stealth = opts.stealth === true;
|
|
190
244
|
if (opts.allowCredentials !== undefined) {
|
|
191
245
|
this._allowCredentials = opts.allowCredentials;
|
|
192
246
|
}
|
|
@@ -276,6 +330,7 @@ class BrowserSession {
|
|
|
276
330
|
headless: this._headless,
|
|
277
331
|
userDataDir: this._profileDir,
|
|
278
332
|
args: this._chromeArgs,
|
|
333
|
+
stealth: this._stealth,
|
|
279
334
|
// WE launched it, so WE may close it (browser_close / the idle
|
|
280
335
|
// sweep). Without this the instance would be unkillable by design.
|
|
281
336
|
surviveStop: false,
|
|
@@ -366,6 +421,24 @@ class BrowserSession {
|
|
|
366
421
|
catch {
|
|
367
422
|
/* older Chrome may refuse; the immediate injection still runs */
|
|
368
423
|
}
|
|
424
|
+
if (this._stealth) {
|
|
425
|
+
// Must be registered for FUTURE documents (before any page script) and
|
|
426
|
+
// injected now for the document that was already loading.
|
|
427
|
+
try {
|
|
428
|
+
await (0, cdp_1.cdpSend)(page, 'Page.addScriptToEvaluateOnNewDocument', {
|
|
429
|
+
source: (0, stealth_1.buildStealthScript)(),
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
catch {
|
|
433
|
+
/* best-effort */
|
|
434
|
+
}
|
|
435
|
+
try {
|
|
436
|
+
await (0, cdp_1.evaluateJs)(page, (0, stealth_1.buildStealthScript)());
|
|
437
|
+
}
|
|
438
|
+
catch {
|
|
439
|
+
/* document not ready yet; the new-document script covers it */
|
|
440
|
+
}
|
|
441
|
+
}
|
|
369
442
|
const install = async () => {
|
|
370
443
|
try {
|
|
371
444
|
await (0, cdp_1.evaluateJs)(page, (0, page_events_1.buildPageEventScript)());
|
|
@@ -719,9 +792,96 @@ class BrowserSession {
|
|
|
719
792
|
stoppedBecause,
|
|
720
793
|
};
|
|
721
794
|
}
|
|
722
|
-
async navigate(url) {
|
|
795
|
+
async navigate(url, opts) {
|
|
796
|
+
this.ensureConnected();
|
|
797
|
+
const info = await (0, cdp_1.navigateTo)(this.page, url);
|
|
798
|
+
const host = hostOf(url);
|
|
799
|
+
// A host that already failed its one bounded attempt is not re-attempted:
|
|
800
|
+
// report the previous verdict (cooldown) instead of burning the run.
|
|
801
|
+
if (this._challengeCooldown &&
|
|
802
|
+
Date.now() < this._challengeCooldown.until &&
|
|
803
|
+
this._challengeCooldown.host === host) {
|
|
804
|
+
const now = await this.detectChallenge();
|
|
805
|
+
if (!now) {
|
|
806
|
+
this._challengeCooldown = null;
|
|
807
|
+
return info;
|
|
808
|
+
}
|
|
809
|
+
return {
|
|
810
|
+
...info,
|
|
811
|
+
challenge: {
|
|
812
|
+
...this._challengeCooldown.state,
|
|
813
|
+
challenge: now,
|
|
814
|
+
attempted: false,
|
|
815
|
+
passed: false,
|
|
816
|
+
cooldown: true,
|
|
817
|
+
nextStep: now.kind === 'block' ? 'report_blocked' : 'ask_human',
|
|
818
|
+
},
|
|
819
|
+
};
|
|
820
|
+
}
|
|
821
|
+
// Give a client-rendered challenge a moment to mount before probing; a
|
|
822
|
+
// challenge script usually replaces the document right after load.
|
|
823
|
+
await (0, browse_types_1.sleep)(350);
|
|
824
|
+
const challenge = await this.resolveChallenge({
|
|
825
|
+
attempt: opts?.autoChallenge !== false,
|
|
826
|
+
timeoutMs: opts?.challengeTimeoutMs,
|
|
827
|
+
});
|
|
828
|
+
if (challenge.detected) {
|
|
829
|
+
if (challenge.passed) {
|
|
830
|
+
if (this._challengeCooldown?.host === host) {
|
|
831
|
+
this._challengeCooldown = null;
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
else {
|
|
835
|
+
this._challengeCooldown = {
|
|
836
|
+
host,
|
|
837
|
+
until: Date.now() + 10 * 60_000,
|
|
838
|
+
state: challenge,
|
|
839
|
+
};
|
|
840
|
+
}
|
|
841
|
+
return { ...info, challenge };
|
|
842
|
+
}
|
|
843
|
+
return info;
|
|
844
|
+
}
|
|
845
|
+
/** Detect a Cloudflare / CAPTCHA challenge on the live page (cheap, safe). */
|
|
846
|
+
async detectChallenge() {
|
|
847
|
+
if (!this.page)
|
|
848
|
+
return null;
|
|
849
|
+
return (0, challenge_1.detectChallenge)(this.page);
|
|
850
|
+
}
|
|
851
|
+
/**
|
|
852
|
+
* One bounded challenge attempt (2026-10): a single click for a Turnstile
|
|
853
|
+
* widget, passive wait for a managed challenge, nothing for a block/image
|
|
854
|
+
* CAPTCHA. Callers stop and ask a human when `passed` is false.
|
|
855
|
+
*/
|
|
856
|
+
async resolveChallenge(opts) {
|
|
857
|
+
this.ensureConnected();
|
|
858
|
+
const page = this.page;
|
|
859
|
+
return (0, challenge_1.resolveChallenge)({
|
|
860
|
+
detect: () => (0, challenge_1.detectChallenge)(page),
|
|
861
|
+
click: (x, y) => (0, cdp_1.clickAt)(page, x, y),
|
|
862
|
+
attempt: opts?.attempt !== false,
|
|
863
|
+
timeoutMs: opts?.timeoutMs,
|
|
864
|
+
});
|
|
865
|
+
}
|
|
866
|
+
/**
|
|
867
|
+
* Poll only (no click). Used after a human answered a `CHALLENGE_REQUIRED`
|
|
868
|
+
* pause: the human solved it in the visible window; this waits for the page
|
|
869
|
+
* to clear and returns the new screen.
|
|
870
|
+
*/
|
|
871
|
+
async waitForChallenge(opts) {
|
|
723
872
|
this.ensureConnected();
|
|
724
|
-
|
|
873
|
+
const page = this.page;
|
|
874
|
+
const result = await (0, challenge_1.resolveChallenge)({
|
|
875
|
+
detect: () => (0, challenge_1.detectChallenge)(page),
|
|
876
|
+
click: (x, y) => (0, cdp_1.clickAt)(page, x, y),
|
|
877
|
+
// Explicit first-try after a mid-flow challenge; false by default so a
|
|
878
|
+
// resume AFTER a human answer only polls.
|
|
879
|
+
attempt: opts?.attempt === true,
|
|
880
|
+
timeoutMs: opts?.timeoutMs,
|
|
881
|
+
});
|
|
882
|
+
if (result.passed)
|
|
883
|
+
this._challengeCooldown = null;
|
|
884
|
+
return result;
|
|
725
885
|
}
|
|
726
886
|
/**
|
|
727
887
|
* Set the browser viewport before a design-verification screenshot
|