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.
Files changed (57) hide show
  1. package/dist/adaptive-wait.d.ts +72 -0
  2. package/dist/adaptive-wait.js +695 -0
  3. package/dist/api-intelligence.d.ts +82 -0
  4. package/dist/api-intelligence.js +575 -0
  5. package/dist/captcha-solver.d.ts +26 -0
  6. package/dist/captcha-solver.js +547 -0
  7. package/dist/cdp-connector.d.ts +33 -0
  8. package/dist/cdp-connector.js +176 -0
  9. package/dist/consent-dismiss.d.ts +33 -0
  10. package/dist/consent-dismiss.js +358 -0
  11. package/dist/crash-recovery.d.ts +74 -0
  12. package/dist/crash-recovery.js +242 -0
  13. package/dist/domain-knowledge.d.ts +149 -0
  14. package/dist/domain-knowledge.js +449 -0
  15. package/dist/harness-intelligence.d.ts +65 -0
  16. package/dist/harness-intelligence.js +432 -0
  17. package/dist/humanize-fingerprint.d.ts +42 -0
  18. package/dist/humanize-fingerprint.js +161 -0
  19. package/dist/humanize-mouse.d.ts +95 -0
  20. package/dist/humanize-mouse.js +275 -0
  21. package/dist/humanize-pause.d.ts +48 -0
  22. package/dist/humanize-pause.js +111 -0
  23. package/dist/humanize-scroll.d.ts +67 -0
  24. package/dist/humanize-scroll.js +185 -0
  25. package/dist/humanize-typing.d.ts +60 -0
  26. package/dist/humanize-typing.js +258 -0
  27. package/dist/humanize-utils.d.ts +62 -0
  28. package/dist/humanize-utils.js +100 -0
  29. package/dist/intervention.d.ts +65 -0
  30. package/dist/intervention.js +591 -0
  31. package/dist/logger.d.ts +13 -0
  32. package/dist/logger.js +47 -0
  33. package/dist/network-intelligence.d.ts +70 -0
  34. package/dist/network-intelligence.js +424 -0
  35. package/dist/page-classifier.d.ts +33 -0
  36. package/dist/page-classifier.js +1000 -0
  37. package/dist/paginate.d.ts +42 -0
  38. package/dist/paginate.js +693 -0
  39. package/dist/recording.d.ts +72 -0
  40. package/dist/recording.js +934 -0
  41. package/dist/script-executor.d.ts +31 -0
  42. package/dist/script-executor.js +249 -0
  43. package/dist/session-hud.d.ts +20 -0
  44. package/dist/session-hud.js +134 -0
  45. package/dist/snapshot-differ.d.ts +26 -0
  46. package/dist/snapshot-differ.js +225 -0
  47. package/dist/ssrf.d.ts +28 -0
  48. package/dist/ssrf.js +290 -0
  49. package/dist/stealth-audit.d.ts +27 -0
  50. package/dist/stealth-audit.js +719 -0
  51. package/dist/stealth.d.ts +195 -0
  52. package/dist/stealth.js +1157 -0
  53. package/dist/tab-manager.d.ts +14 -0
  54. package/dist/tab-manager.js +306 -0
  55. package/dist/tiles-coordinator.d.ts +106 -0
  56. package/dist/tiles-coordinator.js +358 -0
  57. 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;