/** * CAPTCHA auto-solving via 2captcha or CapSolver APIs. * Supports reCAPTCHA v2, hCaptcha, and Cloudflare Turnstile. */ export type CaptchaSolverProvider = '2captcha' | 'capsolver'; interface SolveResult { token: string; provider: CaptchaSolverProvider; } // Poll interval and max attempts const POLL_INTERVAL_MS = 3_000; const MAX_ATTEMPTS = 40; // 2 min max /** * Detect CAPTCHA type and sitekey on the page, then solve it. * Returns null if no CAPTCHA found or solver not configured. * Throws on solve failure. */ export async function solveCaptcha( page: any, pageUrl: string, apiKey: string, provider: CaptchaSolverProvider = '2captcha' ): Promise { // Detect CAPTCHA type const captchaInfo = await page.evaluate(() => { // reCAPTCHA v2 const recaptchaEl = document.querySelector('.g-recaptcha, [data-sitekey]') as HTMLElement | null; if (recaptchaEl) { const sitekey = recaptchaEl.getAttribute('data-sitekey') || ''; return { type: 'recaptcha', sitekey }; } // hCaptcha const hcaptchaEl = document.querySelector('.h-captcha, [data-hcaptcha-sitekey]') as HTMLElement | null; if (hcaptchaEl) { const sitekey = hcaptchaEl.getAttribute('data-sitekey') || hcaptchaEl.getAttribute('data-hcaptcha-sitekey') || ''; return { type: 'hcaptcha', sitekey }; } // Cloudflare Turnstile const turnstileEl = document.querySelector('.cf-turnstile') as HTMLElement | null; if (turnstileEl) { const sitekey = turnstileEl.getAttribute('data-sitekey') || ''; return { type: 'turnstile', sitekey }; } return null; }); if (!captchaInfo?.sitekey) return null; const token = provider === 'capsolver' ? await solveWithCapSolver(captchaInfo.type, captchaInfo.sitekey, pageUrl, apiKey) : await solveWith2Captcha(captchaInfo.type, captchaInfo.sitekey, pageUrl, apiKey); // Inject the token into the page await page.evaluate((t: string, type: string) => { // reCAPTCHA if (type === 'recaptcha') { const el = document.querySelector('textarea.g-recaptcha-response') as HTMLTextAreaElement | null; if (el) { el.value = t; } // Also trigger the callback if registered if ((window as any).grecaptcha?.getResponse && document.querySelector('[data-callback]')) { const cb = (document.querySelector('[data-callback]') as HTMLElement)?.getAttribute('data-callback'); if (cb && (window as any)[cb]) (window as any)[cb](t); } } // hCaptcha if (type === 'hcaptcha') { const el = document.querySelector('textarea[name="h-captcha-response"]') as HTMLTextAreaElement | null; if (el) { el.value = t; } } // Turnstile if (type === 'turnstile') { const el = document.querySelector('[name="cf-turnstile-response"]') as HTMLInputElement | null; if (el) { el.value = t; } } }, token, captchaInfo.type); return { token, provider }; } async function solveWith2Captcha(type: string, sitekey: string, pageUrl: string, apiKey: string): Promise { const method = type === 'hcaptcha' ? 'hcaptcha' : type === 'turnstile' ? 'turnstile' : 'userrecaptcha'; const submitRes = await fetch('http://2captcha.com/in.php', { method: 'POST', body: new URLSearchParams({ key: apiKey, method, googlekey: sitekey, pageurl: pageUrl, json: '1', }), }); const submitData = await submitRes.json() as { status: number; request: string }; if (submitData.status !== 1) throw new Error(`2captcha submit failed: ${submitData.request}`); const taskId = submitData.request; // Poll for result for (let i = 0; i < MAX_ATTEMPTS; i++) { await new Promise(r => setTimeout(r, POLL_INTERVAL_MS)); const pollRes = await fetch(`http://2captcha.com/res.php?key=${apiKey}&action=get&id=${taskId}&json=1`); const pollData = await pollRes.json() as { status: number; request: string }; if (pollData.status === 1) return pollData.request; if (pollData.request !== 'CAPCHA_NOT_READY') throw new Error(`2captcha error: ${pollData.request}`); } throw new Error('2captcha timed out after 2 minutes'); } async function solveWithCapSolver(type: string, sitekey: string, pageUrl: string, apiKey: string): Promise { const taskType = type === 'hcaptcha' ? 'HCaptchaTaskProxyless' : type === 'turnstile' ? 'AntiTurnstileTaskProxyless' : 'ReCaptchaV2TaskProxyless'; const createRes = await fetch('https://api.capsolver.com/createTask', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientKey: apiKey, task: { type: taskType, websiteURL: pageUrl, websiteKey: sitekey }, }), }); const createData = await createRes.json() as { errorId: number; taskId?: string; errorDescription?: string }; if (createData.errorId !== 0) throw new Error(`CapSolver create failed: ${createData.errorDescription}`); const taskId = createData.taskId!; for (let i = 0; i < MAX_ATTEMPTS; i++) { await new Promise(r => setTimeout(r, POLL_INTERVAL_MS)); const pollRes = await fetch('https://api.capsolver.com/getTaskResult', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ clientKey: apiKey, taskId }), }); const pollData = await pollRes.json() as { errorId: number; status: string; solution?: { gRecaptchaResponse?: string; token?: string }; errorDescription?: string }; if (pollData.errorId !== 0) throw new Error(`CapSolver error: ${pollData.errorDescription}`); if (pollData.status === 'ready') { return pollData.solution?.gRecaptchaResponse ?? pollData.solution?.token ?? ''; } } throw new Error('CapSolver timed out after 2 minutes'); }