leapfrog-mcp 0.6.2 → 0.6.3
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/adaptive-wait.d.ts +72 -0
- package/dist/adaptive-wait.js +695 -0
- package/dist/api-intelligence.d.ts +82 -0
- package/dist/api-intelligence.js +575 -0
- package/dist/captcha-solver.d.ts +26 -0
- package/dist/captcha-solver.js +547 -0
- package/dist/cdp-connector.d.ts +33 -0
- package/dist/cdp-connector.js +176 -0
- package/dist/consent-dismiss.d.ts +33 -0
- package/dist/consent-dismiss.js +358 -0
- package/dist/crash-recovery.d.ts +74 -0
- package/dist/crash-recovery.js +242 -0
- package/dist/domain-knowledge.d.ts +149 -0
- package/dist/domain-knowledge.js +449 -0
- package/dist/harness-intelligence.d.ts +65 -0
- package/dist/harness-intelligence.js +432 -0
- package/dist/humanize-fingerprint.d.ts +42 -0
- package/dist/humanize-fingerprint.js +161 -0
- package/dist/humanize-mouse.d.ts +95 -0
- package/dist/humanize-mouse.js +275 -0
- package/dist/humanize-pause.d.ts +48 -0
- package/dist/humanize-pause.js +111 -0
- package/dist/humanize-scroll.d.ts +67 -0
- package/dist/humanize-scroll.js +185 -0
- package/dist/humanize-typing.d.ts +60 -0
- package/dist/humanize-typing.js +258 -0
- package/dist/humanize-utils.d.ts +62 -0
- package/dist/humanize-utils.js +100 -0
- package/dist/intervention.d.ts +65 -0
- package/dist/intervention.js +591 -0
- package/dist/logger.d.ts +13 -0
- package/dist/logger.js +47 -0
- package/dist/network-intelligence.d.ts +70 -0
- package/dist/network-intelligence.js +424 -0
- package/dist/page-classifier.d.ts +33 -0
- package/dist/page-classifier.js +1000 -0
- package/dist/paginate.d.ts +42 -0
- package/dist/paginate.js +693 -0
- package/dist/recording.d.ts +72 -0
- package/dist/recording.js +934 -0
- package/dist/script-executor.d.ts +31 -0
- package/dist/script-executor.js +249 -0
- package/dist/session-hud.d.ts +20 -0
- package/dist/session-hud.js +134 -0
- package/dist/snapshot-differ.d.ts +26 -0
- package/dist/snapshot-differ.js +225 -0
- package/dist/ssrf.d.ts +28 -0
- package/dist/ssrf.js +290 -0
- package/dist/stealth-audit.d.ts +27 -0
- package/dist/stealth-audit.js +719 -0
- package/dist/stealth.d.ts +195 -0
- package/dist/stealth.js +1157 -0
- package/dist/tab-manager.d.ts +14 -0
- package/dist/tab-manager.js +306 -0
- package/dist/tiles-coordinator.d.ts +106 -0
- package/dist/tiles-coordinator.js +358 -0
- package/package.json +1 -1
|
@@ -0,0 +1,547 @@
|
|
|
1
|
+
// ─── CAPTCHA Solver Integration ──────────────────────────────────────────
|
|
2
|
+
//
|
|
3
|
+
// Integrates with external CAPTCHA solving services (CapSolver, 2Captcha,
|
|
4
|
+
// NopeCHA) to automatically solve CAPTCHAs detected by the intervention
|
|
5
|
+
// system. This module is standalone — no cross-deps except logger.ts types.
|
|
6
|
+
//
|
|
7
|
+
// Flow:
|
|
8
|
+
// 1. intervention.ts detects a CAPTCHA (reCAPTCHA v2/v3, hCaptcha, Turnstile)
|
|
9
|
+
// 2. If LEAP_CAPTCHA_PROVIDER env var is set, this module handles solving
|
|
10
|
+
// 3. Extract sitekey + pageURL from the page
|
|
11
|
+
// 4. POST to the solver API
|
|
12
|
+
// 5. Poll for the token (or receive it directly from NopeCHA)
|
|
13
|
+
// 6. Inject the token into the page
|
|
14
|
+
// 7. Submit the form / trigger the callback
|
|
15
|
+
//
|
|
16
|
+
// Env vars:
|
|
17
|
+
// LEAP_CAPTCHA_PROVIDER — 'capsolver' | '2captcha' | 'nopecha'
|
|
18
|
+
// LEAP_CAPTCHA_API_KEY — API key for the selected provider
|
|
19
|
+
//
|
|
20
|
+
// If LEAP_CAPTCHA_PROVIDER is unset, all exports are safe no-ops.
|
|
21
|
+
import { logger } from "./logger.js";
|
|
22
|
+
// ─── Constants ───────────────────────────────────────────────────────────
|
|
23
|
+
const POLL_INTERVAL_MS = 3_000;
|
|
24
|
+
const POLL_TIMEOUT_MS = 120_000;
|
|
25
|
+
const CAPSOLVER_CREATE_URL = "https://api.capsolver.com/createTask";
|
|
26
|
+
const CAPSOLVER_RESULT_URL = "https://api.capsolver.com/getTaskResult";
|
|
27
|
+
const TWOCAPTCHA_CREATE_URL = "https://api.2captcha.com/createTask";
|
|
28
|
+
const TWOCAPTCHA_RESULT_URL = "https://api.2captcha.com/getTaskResult";
|
|
29
|
+
const NOPECHA_SOLVE_URL = "https://api.nopecha.com/token";
|
|
30
|
+
/** Maps our internal captcha type to each provider's task type string. */
|
|
31
|
+
const CAPSOLVER_TASK_TYPES = {
|
|
32
|
+
"recaptcha-v2": "ReCaptchaV2TaskProxyLess",
|
|
33
|
+
"recaptcha-v3": "ReCaptchaV3TaskProxyLess",
|
|
34
|
+
hcaptcha: "HCaptchaTaskProxyLess",
|
|
35
|
+
turnstile: "AntiTurnstileTaskProxyLess",
|
|
36
|
+
};
|
|
37
|
+
const TWOCAPTCHA_TASK_TYPES = {
|
|
38
|
+
"recaptcha-v2": "RecaptchaV2TaskProxyless",
|
|
39
|
+
"recaptcha-v3": "RecaptchaV3TaskProxyless",
|
|
40
|
+
hcaptcha: "HCaptchaTaskProxyless",
|
|
41
|
+
turnstile: "TurnstileTaskProxyless",
|
|
42
|
+
};
|
|
43
|
+
const NOPECHA_TYPE_MAP = {
|
|
44
|
+
"recaptcha-v2": "recaptcha2",
|
|
45
|
+
"recaptcha-v3": "recaptcha3",
|
|
46
|
+
hcaptcha: "hcaptcha",
|
|
47
|
+
turnstile: "turnstile",
|
|
48
|
+
};
|
|
49
|
+
// ─── Config helpers ──────────────────────────────────────────────────────
|
|
50
|
+
function getProvider() {
|
|
51
|
+
const raw = process.env.LEAP_CAPTCHA_PROVIDER;
|
|
52
|
+
if (!raw)
|
|
53
|
+
return null;
|
|
54
|
+
const normalized = raw.trim().toLowerCase();
|
|
55
|
+
if (normalized === "capsolver" || normalized === "2captcha" || normalized === "nopecha") {
|
|
56
|
+
return normalized;
|
|
57
|
+
}
|
|
58
|
+
logger.warn("captcha:invalid-provider", { value: raw, valid: "capsolver, 2captcha, nopecha" });
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
function getApiKey() {
|
|
62
|
+
const key = process.env.LEAP_CAPTCHA_API_KEY;
|
|
63
|
+
if (!key || key.trim().length === 0)
|
|
64
|
+
return null;
|
|
65
|
+
return key.trim();
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Returns true if a CAPTCHA solving provider is configured and ready.
|
|
69
|
+
* Safe to call at any time — reads env vars on each invocation so config
|
|
70
|
+
* changes at runtime are picked up immediately.
|
|
71
|
+
*/
|
|
72
|
+
export function isCaptchaSolverEnabled() {
|
|
73
|
+
return getProvider() !== null && getApiKey() !== null;
|
|
74
|
+
}
|
|
75
|
+
// ─── Captcha type normalization ──────────────────────────────────────────
|
|
76
|
+
/**
|
|
77
|
+
* Normalizes the free-form captchaType string (from intervention.ts or the
|
|
78
|
+
* caller) into one of our four supported CaptchaType values.
|
|
79
|
+
*
|
|
80
|
+
* Accepts a variety of inputs:
|
|
81
|
+
* "recaptcha", "recaptcha-v2", "recaptcha_v2", "reCAPTCHA v2" → "recaptcha-v2"
|
|
82
|
+
* "recaptcha-v3", "recaptcha_v3", "reCAPTCHA v3" → "recaptcha-v3"
|
|
83
|
+
* "hcaptcha", "h-captcha" → "hcaptcha"
|
|
84
|
+
* "turnstile", "cloudflare", "cf-turnstile" → "turnstile"
|
|
85
|
+
*/
|
|
86
|
+
function normalizeCaptchaType(raw) {
|
|
87
|
+
const lower = raw.toLowerCase().replace(/[\s_]/g, "-");
|
|
88
|
+
if (lower.includes("recaptcha") && lower.includes("v3"))
|
|
89
|
+
return "recaptcha-v3";
|
|
90
|
+
if (lower.includes("recaptcha"))
|
|
91
|
+
return "recaptcha-v2";
|
|
92
|
+
if (lower.includes("hcaptcha") || lower.includes("h-captcha"))
|
|
93
|
+
return "hcaptcha";
|
|
94
|
+
if (lower.includes("turnstile") || lower.includes("cloudflare") || lower.includes("cf-turnstile"))
|
|
95
|
+
return "turnstile";
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
// ─── Sitekey extraction ──────────────────────────────────────────────────
|
|
99
|
+
/**
|
|
100
|
+
* Extracts the sitekey from the page for the given captcha type.
|
|
101
|
+
* Runs inside page.evaluate() — all DOM access happens in-browser.
|
|
102
|
+
*/
|
|
103
|
+
async function extractSitekey(page, captchaType) {
|
|
104
|
+
const result = await page.evaluate((ct) => {
|
|
105
|
+
function getAttr(selector, attr) {
|
|
106
|
+
const el = document.querySelector(selector);
|
|
107
|
+
return el ? el.getAttribute(attr) : null;
|
|
108
|
+
}
|
|
109
|
+
function extractFromIframeSrc(pattern, paramName) {
|
|
110
|
+
const iframes = document.querySelectorAll("iframe[src]");
|
|
111
|
+
for (const iframe of iframes) {
|
|
112
|
+
const src = iframe.getAttribute("src") || "";
|
|
113
|
+
if (!src.includes(pattern))
|
|
114
|
+
continue;
|
|
115
|
+
try {
|
|
116
|
+
const url = new URL(src, window.location.href);
|
|
117
|
+
const val = url.searchParams.get(paramName);
|
|
118
|
+
if (val)
|
|
119
|
+
return val;
|
|
120
|
+
}
|
|
121
|
+
catch {
|
|
122
|
+
// Malformed URL — skip
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return null;
|
|
126
|
+
}
|
|
127
|
+
let sitekey = null;
|
|
128
|
+
switch (ct) {
|
|
129
|
+
case "recaptcha-v2":
|
|
130
|
+
case "recaptcha-v3":
|
|
131
|
+
// Method 1: data-sitekey on the widget div
|
|
132
|
+
sitekey = getAttr("div.g-recaptcha[data-sitekey]", "data-sitekey");
|
|
133
|
+
// Method 2: sitekey in the reCAPTCHA script src
|
|
134
|
+
if (!sitekey) {
|
|
135
|
+
const scripts = document.querySelectorAll('script[src*="recaptcha"]');
|
|
136
|
+
for (const script of scripts) {
|
|
137
|
+
const src = script.getAttribute("src") || "";
|
|
138
|
+
try {
|
|
139
|
+
const url = new URL(src, window.location.href);
|
|
140
|
+
const render = url.searchParams.get("render");
|
|
141
|
+
if (render && render !== "explicit") {
|
|
142
|
+
sitekey = render;
|
|
143
|
+
break;
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
// skip
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
// Method 3: sitekey param in iframe src
|
|
152
|
+
if (!sitekey) {
|
|
153
|
+
sitekey = extractFromIframeSrc("recaptcha", "k");
|
|
154
|
+
}
|
|
155
|
+
break;
|
|
156
|
+
case "hcaptcha":
|
|
157
|
+
sitekey = getAttr("div.h-captcha[data-sitekey]", "data-sitekey");
|
|
158
|
+
if (!sitekey) {
|
|
159
|
+
sitekey = extractFromIframeSrc("hcaptcha", "sitekey");
|
|
160
|
+
}
|
|
161
|
+
break;
|
|
162
|
+
case "turnstile":
|
|
163
|
+
sitekey = getAttr("div.cf-turnstile[data-sitekey]", "data-sitekey");
|
|
164
|
+
if (!sitekey) {
|
|
165
|
+
sitekey = extractFromIframeSrc("challenges.cloudflare.com", "k");
|
|
166
|
+
}
|
|
167
|
+
break;
|
|
168
|
+
}
|
|
169
|
+
if (!sitekey)
|
|
170
|
+
return null;
|
|
171
|
+
return { sitekey, pageURL: window.location.href };
|
|
172
|
+
}, captchaType);
|
|
173
|
+
return result;
|
|
174
|
+
}
|
|
175
|
+
// ─── Provider: CapSolver ─────────────────────────────────────────────────
|
|
176
|
+
async function solveWithCapSolver(apiKey, captchaType, sitekey, pageURL) {
|
|
177
|
+
const taskType = CAPSOLVER_TASK_TYPES[captchaType];
|
|
178
|
+
const createBody = {
|
|
179
|
+
clientKey: apiKey,
|
|
180
|
+
task: {
|
|
181
|
+
type: taskType,
|
|
182
|
+
websiteURL: pageURL,
|
|
183
|
+
websiteKey: sitekey,
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
logger.debug("captcha:capsolver:create", { taskType, pageURL });
|
|
187
|
+
const createRes = await fetch(CAPSOLVER_CREATE_URL, {
|
|
188
|
+
method: "POST",
|
|
189
|
+
headers: { "Content-Type": "application/json" },
|
|
190
|
+
body: JSON.stringify(createBody),
|
|
191
|
+
});
|
|
192
|
+
if (!createRes.ok) {
|
|
193
|
+
throw new Error(`CapSolver createTask HTTP ${createRes.status}: ${await createRes.text()}`);
|
|
194
|
+
}
|
|
195
|
+
const createData = (await createRes.json());
|
|
196
|
+
if (createData.errorId !== 0) {
|
|
197
|
+
throw new Error(`CapSolver createTask error: ${createData.errorCode} — ${createData.errorDescription}`);
|
|
198
|
+
}
|
|
199
|
+
if (!createData.taskId) {
|
|
200
|
+
throw new Error("CapSolver createTask returned no taskId");
|
|
201
|
+
}
|
|
202
|
+
return pollForToken(CAPSOLVER_RESULT_URL, apiKey, createData.taskId, "capsolver");
|
|
203
|
+
}
|
|
204
|
+
// ─── Provider: 2Captcha ─────────────────────────────────────────────────
|
|
205
|
+
async function solveWith2Captcha(apiKey, captchaType, sitekey, pageURL) {
|
|
206
|
+
const taskType = TWOCAPTCHA_TASK_TYPES[captchaType];
|
|
207
|
+
const createBody = {
|
|
208
|
+
clientKey: apiKey,
|
|
209
|
+
task: {
|
|
210
|
+
type: taskType,
|
|
211
|
+
websiteURL: pageURL,
|
|
212
|
+
websiteKey: sitekey,
|
|
213
|
+
},
|
|
214
|
+
};
|
|
215
|
+
logger.debug("captcha:2captcha:create", { taskType, pageURL });
|
|
216
|
+
const createRes = await fetch(TWOCAPTCHA_CREATE_URL, {
|
|
217
|
+
method: "POST",
|
|
218
|
+
headers: { "Content-Type": "application/json" },
|
|
219
|
+
body: JSON.stringify(createBody),
|
|
220
|
+
});
|
|
221
|
+
if (!createRes.ok) {
|
|
222
|
+
throw new Error(`2Captcha createTask HTTP ${createRes.status}: ${await createRes.text()}`);
|
|
223
|
+
}
|
|
224
|
+
const createData = (await createRes.json());
|
|
225
|
+
if (createData.errorId !== 0) {
|
|
226
|
+
throw new Error(`2Captcha createTask error: ${createData.errorCode} — ${createData.errorDescription}`);
|
|
227
|
+
}
|
|
228
|
+
if (!createData.taskId) {
|
|
229
|
+
throw new Error("2Captcha createTask returned no taskId");
|
|
230
|
+
}
|
|
231
|
+
return pollForToken(TWOCAPTCHA_RESULT_URL, apiKey, createData.taskId, "2captcha");
|
|
232
|
+
}
|
|
233
|
+
// ─── Provider: NopeCHA ──────────────────────────────────────────────────
|
|
234
|
+
async function solveWithNopeCHA(apiKey, captchaType, sitekey, pageURL) {
|
|
235
|
+
const nopechaType = NOPECHA_TYPE_MAP[captchaType];
|
|
236
|
+
logger.debug("captcha:nopecha:solve", { type: nopechaType, pageURL });
|
|
237
|
+
const body = {
|
|
238
|
+
type: nopechaType,
|
|
239
|
+
sitekey,
|
|
240
|
+
url: pageURL,
|
|
241
|
+
key: apiKey,
|
|
242
|
+
};
|
|
243
|
+
const res = await fetch(NOPECHA_SOLVE_URL, {
|
|
244
|
+
method: "POST",
|
|
245
|
+
headers: { "Content-Type": "application/json" },
|
|
246
|
+
body: JSON.stringify(body),
|
|
247
|
+
});
|
|
248
|
+
if (!res.ok) {
|
|
249
|
+
throw new Error(`NopeCHA HTTP ${res.status}: ${await res.text()}`);
|
|
250
|
+
}
|
|
251
|
+
const data = (await res.json());
|
|
252
|
+
if (data.error) {
|
|
253
|
+
throw new Error(`NopeCHA error ${data.error}: ${data.message ?? "unknown"}`);
|
|
254
|
+
}
|
|
255
|
+
if (!data.data || typeof data.data !== "string") {
|
|
256
|
+
throw new Error("NopeCHA returned no token");
|
|
257
|
+
}
|
|
258
|
+
return data.data;
|
|
259
|
+
}
|
|
260
|
+
// ─── Polling (CapSolver / 2Captcha shared) ──────────────────────────────
|
|
261
|
+
async function pollForToken(resultURL, apiKey, taskId, providerName) {
|
|
262
|
+
const deadline = Date.now() + POLL_TIMEOUT_MS;
|
|
263
|
+
let attempts = 0;
|
|
264
|
+
while (Date.now() < deadline) {
|
|
265
|
+
// Wait before first poll — the task is never ready instantly
|
|
266
|
+
await sleep(POLL_INTERVAL_MS);
|
|
267
|
+
attempts++;
|
|
268
|
+
logger.debug(`captcha:${providerName}:poll`, { taskId, attempt: attempts });
|
|
269
|
+
const res = await fetch(resultURL, {
|
|
270
|
+
method: "POST",
|
|
271
|
+
headers: { "Content-Type": "application/json" },
|
|
272
|
+
body: JSON.stringify({ clientKey: apiKey, taskId }),
|
|
273
|
+
});
|
|
274
|
+
if (!res.ok) {
|
|
275
|
+
throw new Error(`${providerName} getTaskResult HTTP ${res.status}: ${await res.text()}`);
|
|
276
|
+
}
|
|
277
|
+
const data = (await res.json());
|
|
278
|
+
if (data.errorId !== 0) {
|
|
279
|
+
throw new Error(`${providerName} getTaskResult error: ${data.errorCode} — ${data.errorDescription}`);
|
|
280
|
+
}
|
|
281
|
+
if (data.status === "ready") {
|
|
282
|
+
const token = data.solution?.gRecaptchaResponse ?? data.solution?.token;
|
|
283
|
+
if (!token) {
|
|
284
|
+
throw new Error(`${providerName} returned ready status but no token in solution`);
|
|
285
|
+
}
|
|
286
|
+
logger.info(`captcha:${providerName}:solved`, { taskId, attempts });
|
|
287
|
+
return token;
|
|
288
|
+
}
|
|
289
|
+
// status === "processing" — continue polling
|
|
290
|
+
}
|
|
291
|
+
throw new Error(`${providerName} polling timed out after ${POLL_TIMEOUT_MS / 1000}s (${attempts} attempts)`);
|
|
292
|
+
}
|
|
293
|
+
// ─── Token injection ────────────────────────────────────────────────────
|
|
294
|
+
/**
|
|
295
|
+
* Injects the solved CAPTCHA token into the page and triggers the
|
|
296
|
+
* appropriate callback so the form recognizes the solution.
|
|
297
|
+
*
|
|
298
|
+
* Runs entirely inside page.evaluate() — no Node-side DOM access.
|
|
299
|
+
*/
|
|
300
|
+
async function injectToken(page, captchaType, token) {
|
|
301
|
+
await page.evaluate(({ ct, tk }) => {
|
|
302
|
+
function setTextareaValue(selector, value) {
|
|
303
|
+
const el = document.querySelector(selector);
|
|
304
|
+
if (!el)
|
|
305
|
+
return false;
|
|
306
|
+
el.value = value;
|
|
307
|
+
el.style.display = "none";
|
|
308
|
+
return true;
|
|
309
|
+
}
|
|
310
|
+
function setInputValue(selector, value) {
|
|
311
|
+
const el = document.querySelector(selector);
|
|
312
|
+
if (!el)
|
|
313
|
+
return false;
|
|
314
|
+
el.value = value;
|
|
315
|
+
return true;
|
|
316
|
+
}
|
|
317
|
+
switch (ct) {
|
|
318
|
+
case "recaptcha-v2": {
|
|
319
|
+
// Set the response textarea
|
|
320
|
+
setTextareaValue("#g-recaptcha-response", tk);
|
|
321
|
+
// Also check for multiple response textareas (multi-widget pages)
|
|
322
|
+
const textareas = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
|
|
323
|
+
for (const ta of textareas) {
|
|
324
|
+
ta.value = tk;
|
|
325
|
+
}
|
|
326
|
+
// Trigger the callback if available
|
|
327
|
+
try {
|
|
328
|
+
// Standard callback location
|
|
329
|
+
const cb = window.___grecaptcha_cfg?.clients?.[0]?.["*"]?.callback
|
|
330
|
+
?? window.___grecaptcha_cfg?.clients?.[0];
|
|
331
|
+
if (typeof cb === "function") {
|
|
332
|
+
cb(tk);
|
|
333
|
+
}
|
|
334
|
+
else if (cb && typeof cb.callback === "function") {
|
|
335
|
+
cb.callback(tk);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
catch {
|
|
339
|
+
// Callback not found — the form may still work via textarea value
|
|
340
|
+
}
|
|
341
|
+
// Also try the data-callback attribute on the widget div
|
|
342
|
+
try {
|
|
343
|
+
const widget = document.querySelector("div.g-recaptcha[data-callback]");
|
|
344
|
+
if (widget) {
|
|
345
|
+
const cbName = widget.getAttribute("data-callback");
|
|
346
|
+
if (cbName && typeof window[cbName] === "function") {
|
|
347
|
+
window[cbName](tk);
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
catch {
|
|
352
|
+
// skip
|
|
353
|
+
}
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
case "recaptcha-v3": {
|
|
357
|
+
setTextareaValue("#g-recaptcha-response", tk);
|
|
358
|
+
const textareas = document.querySelectorAll('textarea[name="g-recaptcha-response"]');
|
|
359
|
+
for (const ta of textareas) {
|
|
360
|
+
ta.value = tk;
|
|
361
|
+
}
|
|
362
|
+
// v3 callback is typically on ___grecaptcha_cfg.clients
|
|
363
|
+
try {
|
|
364
|
+
const clients = window.___grecaptcha_cfg?.clients;
|
|
365
|
+
if (clients) {
|
|
366
|
+
for (const clientKey of Object.keys(clients)) {
|
|
367
|
+
const client = clients[clientKey];
|
|
368
|
+
// Walk the client object tree to find the callback
|
|
369
|
+
for (const propKey of Object.keys(client)) {
|
|
370
|
+
const prop = client[propKey];
|
|
371
|
+
if (prop && typeof prop === "object") {
|
|
372
|
+
for (const subKey of Object.keys(prop)) {
|
|
373
|
+
if (typeof prop[subKey]?.callback === "function") {
|
|
374
|
+
prop[subKey].callback(tk);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
catch {
|
|
383
|
+
// skip
|
|
384
|
+
}
|
|
385
|
+
break;
|
|
386
|
+
}
|
|
387
|
+
case "hcaptcha": {
|
|
388
|
+
setTextareaValue('textarea[name="h-captcha-response"]', tk);
|
|
389
|
+
setTextareaValue('textarea[name="g-recaptcha-response"]', tk);
|
|
390
|
+
// Trigger hcaptcha callback
|
|
391
|
+
try {
|
|
392
|
+
const widget = document.querySelector("div.h-captcha[data-callback]");
|
|
393
|
+
if (widget) {
|
|
394
|
+
const cbName = widget.getAttribute("data-callback");
|
|
395
|
+
if (cbName && typeof window[cbName] === "function") {
|
|
396
|
+
window[cbName](tk);
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
// Also try the global hcaptcha object
|
|
400
|
+
if (typeof window.hcaptcha?.getRespKey === "function") {
|
|
401
|
+
// hcaptcha stores the response internally; some sites check via hcaptcha.getResponse()
|
|
402
|
+
// Setting textarea is usually sufficient, but dispatch a custom event as a hint
|
|
403
|
+
document.dispatchEvent(new CustomEvent("hcaptchaCallback", { detail: { token: tk } }));
|
|
404
|
+
}
|
|
405
|
+
}
|
|
406
|
+
catch {
|
|
407
|
+
// skip
|
|
408
|
+
}
|
|
409
|
+
break;
|
|
410
|
+
}
|
|
411
|
+
case "turnstile": {
|
|
412
|
+
setInputValue('input[name="cf-turnstile-response"]', tk);
|
|
413
|
+
// Also set any hidden textarea variants
|
|
414
|
+
setTextareaValue('textarea[name="cf-turnstile-response"]', tk);
|
|
415
|
+
// Try the Turnstile callback
|
|
416
|
+
try {
|
|
417
|
+
const widget = document.querySelector("div.cf-turnstile[data-callback]");
|
|
418
|
+
if (widget) {
|
|
419
|
+
const cbName = widget.getAttribute("data-callback");
|
|
420
|
+
if (cbName && typeof window[cbName] === "function") {
|
|
421
|
+
window[cbName](tk);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
// Also try turnstile.getResponse style — trigger via the global callback registry
|
|
425
|
+
if (typeof window.turnstile?.render === "function") {
|
|
426
|
+
document.dispatchEvent(new CustomEvent("turnstileCallback", { detail: { token: tk } }));
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
catch {
|
|
430
|
+
// skip
|
|
431
|
+
}
|
|
432
|
+
break;
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
}, { ct: captchaType, tk: token });
|
|
436
|
+
}
|
|
437
|
+
// ─── Main entry point ───────────────────────────────────────────────────
|
|
438
|
+
/**
|
|
439
|
+
* Attempts to solve a CAPTCHA on the given page using the configured
|
|
440
|
+
* external solving service.
|
|
441
|
+
*
|
|
442
|
+
* @param page - Playwright page with the CAPTCHA
|
|
443
|
+
* @param captchaType - Free-form type string (e.g. "recaptcha", "hcaptcha", "turnstile")
|
|
444
|
+
* @param elementSelector - Optional CSS selector hint for the CAPTCHA element
|
|
445
|
+
* @returns - Result object with solve status, timing, and any error
|
|
446
|
+
*/
|
|
447
|
+
export async function solveCaptcha(page, captchaType, elementSelector) {
|
|
448
|
+
const startTime = Date.now();
|
|
449
|
+
const provider = getProvider();
|
|
450
|
+
const apiKey = getApiKey();
|
|
451
|
+
if (!provider || !apiKey) {
|
|
452
|
+
return {
|
|
453
|
+
solved: false,
|
|
454
|
+
provider: provider ?? "none",
|
|
455
|
+
captchaType,
|
|
456
|
+
solveTimeMs: Date.now() - startTime,
|
|
457
|
+
error: "CAPTCHA solver not configured. Set LEAP_CAPTCHA_PROVIDER and LEAP_CAPTCHA_API_KEY env vars.",
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
const normalized = normalizeCaptchaType(captchaType);
|
|
461
|
+
if (!normalized) {
|
|
462
|
+
return {
|
|
463
|
+
solved: false,
|
|
464
|
+
provider,
|
|
465
|
+
captchaType,
|
|
466
|
+
solveTimeMs: Date.now() - startTime,
|
|
467
|
+
error: `Unsupported CAPTCHA type: "${captchaType}". Supported: recaptcha-v2, recaptcha-v3, hcaptcha, turnstile.`,
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
logger.info("captcha:solve:start", {
|
|
471
|
+
provider,
|
|
472
|
+
captchaType: normalized,
|
|
473
|
+
url: page.url(),
|
|
474
|
+
elementSelector: elementSelector ?? null,
|
|
475
|
+
});
|
|
476
|
+
try {
|
|
477
|
+
// Step 1: Extract sitekey from the page
|
|
478
|
+
const sitekeyInfo = await extractSitekey(page, normalized);
|
|
479
|
+
if (!sitekeyInfo) {
|
|
480
|
+
const error = `Could not extract sitekey for ${normalized} from page`;
|
|
481
|
+
logger.warn("captcha:solve:no-sitekey", { captchaType: normalized, url: page.url() });
|
|
482
|
+
return {
|
|
483
|
+
solved: false,
|
|
484
|
+
provider,
|
|
485
|
+
captchaType: normalized,
|
|
486
|
+
solveTimeMs: Date.now() - startTime,
|
|
487
|
+
error,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
logger.debug("captcha:solve:sitekey", {
|
|
491
|
+
sitekey: sitekeyInfo.sitekey,
|
|
492
|
+
pageURL: sitekeyInfo.pageURL,
|
|
493
|
+
});
|
|
494
|
+
// Step 2: Send to solving service
|
|
495
|
+
let token;
|
|
496
|
+
switch (provider) {
|
|
497
|
+
case "capsolver":
|
|
498
|
+
token = await solveWithCapSolver(apiKey, normalized, sitekeyInfo.sitekey, sitekeyInfo.pageURL);
|
|
499
|
+
break;
|
|
500
|
+
case "2captcha":
|
|
501
|
+
token = await solveWith2Captcha(apiKey, normalized, sitekeyInfo.sitekey, sitekeyInfo.pageURL);
|
|
502
|
+
break;
|
|
503
|
+
case "nopecha":
|
|
504
|
+
token = await solveWithNopeCHA(apiKey, normalized, sitekeyInfo.sitekey, sitekeyInfo.pageURL);
|
|
505
|
+
break;
|
|
506
|
+
default:
|
|
507
|
+
throw new Error(`Unknown provider: ${provider}`);
|
|
508
|
+
}
|
|
509
|
+
// Step 3: Inject the token into the page
|
|
510
|
+
await injectToken(page, normalized, token);
|
|
511
|
+
const solveTimeMs = Date.now() - startTime;
|
|
512
|
+
logger.info("captcha:solve:success", {
|
|
513
|
+
provider,
|
|
514
|
+
captchaType: normalized,
|
|
515
|
+
solveTimeMs,
|
|
516
|
+
url: page.url(),
|
|
517
|
+
});
|
|
518
|
+
return {
|
|
519
|
+
solved: true,
|
|
520
|
+
provider,
|
|
521
|
+
captchaType: normalized,
|
|
522
|
+
solveTimeMs,
|
|
523
|
+
};
|
|
524
|
+
}
|
|
525
|
+
catch (err) {
|
|
526
|
+
const solveTimeMs = Date.now() - startTime;
|
|
527
|
+
const errorMsg = err instanceof Error ? err.message : String(err);
|
|
528
|
+
logger.error("captcha:solve:failed", {
|
|
529
|
+
provider,
|
|
530
|
+
captchaType: normalized,
|
|
531
|
+
solveTimeMs,
|
|
532
|
+
error: errorMsg,
|
|
533
|
+
url: page.url(),
|
|
534
|
+
});
|
|
535
|
+
return {
|
|
536
|
+
solved: false,
|
|
537
|
+
provider,
|
|
538
|
+
captchaType: normalized,
|
|
539
|
+
solveTimeMs,
|
|
540
|
+
error: errorMsg,
|
|
541
|
+
};
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
// ─── Utilities ──────────────────────────────────────────────────────────
|
|
545
|
+
function sleep(ms) {
|
|
546
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
547
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { Browser } from "playwright-core";
|
|
2
|
+
export interface CdpDiscoveryResult {
|
|
3
|
+
endpoint: string;
|
|
4
|
+
browser: string;
|
|
5
|
+
source: "env" | "param" | "devtools-port-file" | "port-scan";
|
|
6
|
+
}
|
|
7
|
+
/** Parse DevToolsActivePort file: line 1 = port, line 2 = path */
|
|
8
|
+
export declare function parseDevToolsActivePort(contents: string): number | null;
|
|
9
|
+
/** Probe a single port via HTTP GET /json/version with a tight timeout */
|
|
10
|
+
export declare function probePort(port: number, timeoutMs?: number): Promise<string | null>;
|
|
11
|
+
export declare class CdpConnector {
|
|
12
|
+
/**
|
|
13
|
+
* Discover available CDP endpoints on this machine.
|
|
14
|
+
* Checks env var, DevToolsActivePort files, and scans common ports in parallel.
|
|
15
|
+
* Total time target: <2s.
|
|
16
|
+
*/
|
|
17
|
+
static discover(): Promise<CdpDiscoveryResult[]>;
|
|
18
|
+
/** Check all known DevToolsActivePort file locations */
|
|
19
|
+
static discoverFromPortFiles(): Promise<CdpDiscoveryResult[]>;
|
|
20
|
+
/** Scan common debug ports (9222-9229) for active CDP endpoints */
|
|
21
|
+
static discoverFromPortScan(): Promise<CdpDiscoveryResult[]>;
|
|
22
|
+
/** Connect to a CDP endpoint. Returns a Playwright Browser object. */
|
|
23
|
+
static connect(endpoint: string): Promise<Browser>;
|
|
24
|
+
/**
|
|
25
|
+
* Get the best available endpoint and connect.
|
|
26
|
+
* Priority: LEAP_CDP_ENDPOINT env > DevToolsActivePort file > port scan
|
|
27
|
+
*/
|
|
28
|
+
static autoConnect(): Promise<{
|
|
29
|
+
browser: Browser;
|
|
30
|
+
info: CdpDiscoveryResult;
|
|
31
|
+
}>;
|
|
32
|
+
}
|
|
33
|
+
export default CdpConnector;
|