TypeScript 97.5%
SQL 1.4%
Python 0.8%
1/**2 * Captcha solving via 2captcha (https://2captcha.com, JSON API v2).3 *4 * Used for Cloudflare Turnstile — both the standalone widget (`data-sitekey`) and the managed5 * challenge page ("Just a moment…"), where the `turnstile.render` call must be intercepted to grab6 * `sitekey`, `action`, `cData` and `chlPageData`, and the returned token handed to the widget's7 * callback. (The solver also returns the User-Agent it used; changing the page's UA to match makes8 * Cloudflare reject the token — keep the browser's own UA.)9 */10export interface TurnstileParams {11 sitekey: string;12 action?: string | null;13 cData?: string | null;14 chlPageData?: string | null;15 /** Whether a JS callback was captured (challenge page / explicit render). */16 hasCallback: boolean;17}1819export interface TurnstileSolution {20 token: string;21 userAgent: string | null;22 taskId: string;23 ms: number;24 costUsd: number;25}2627export interface CaptchaSolverOptions {28 apiKey: string;29 endpoint?: string;30 /** Overall budget for a solve (default 110 s). */31 timeoutMs?: number;32 /** First poll delay / subsequent poll interval (2captcha recommends ~5 s then 3 s). */33 firstPollMs?: number;34 pollMs?: number;35 /** Price per Turnstile solve, for cost accounting (2captcha lists ~$1.45 / 1000). */36 pricePerSolveUsd?: number;37 fetch?: typeof globalThis.fetch;38 log?: { info: (m: string) => void; warn: (m: string) => void };39}4041export class CaptchaSolverError extends Error {42 constructor(43 readonly code: string,44 message: string,45 ) {46 super(message);47 this.name = "CaptchaSolverError";48 }49}5051interface TaskResult {52 errorId: number;53 errorCode?: string;54 errorDescription?: string;55 status?: "processing" | "ready";56 solution?: { token?: string; userAgent?: string };57 taskId?: number | string;58 balance?: number;59}6061export class TwoCaptchaSolver {62 readonly provider = "2captcha" as const;63 private readonly opts: Required<Omit<CaptchaSolverOptions, "log">> & { log: NonNullable<CaptchaSolverOptions["log"]> };64 private stats = { requested: 0, solved: 0, failed: 0, spentUsd: 0, lastError: null as string | null };6566 constructor(opts: CaptchaSolverOptions) {67 if (!opts.apiKey) throw new Error("2captcha: apiKey is required");68 this.opts = {69 apiKey: opts.apiKey,70 endpoint: (opts.endpoint ?? "https://api.2captcha.com").replace(/\/$/, ""),71 timeoutMs: opts.timeoutMs ?? 110_000,72 firstPollMs: opts.firstPollMs ?? 5000,73 pollMs: opts.pollMs ?? 3000,74 pricePerSolveUsd: opts.pricePerSolveUsd ?? 0.00145,75 fetch: opts.fetch ?? globalThis.fetch.bind(globalThis),76 log: opts.log ?? { info: (m) => console.log("[2captcha]", m), warn: (m) => console.warn("[2captcha]", m) },77 };78 }7980 status() {81 return { provider: this.provider, ...this.stats };82 }8384 private async call(path: string, body: Record<string, unknown>, timeoutMs = 20_000): Promise<TaskResult> {85 const ac = new AbortController();86 const t = setTimeout(() => ac.abort(), timeoutMs);87 try {88 const res = await this.opts.fetch(`${this.opts.endpoint}${path}`, {89 method: "POST",90 headers: { "content-type": "application/json", "user-agent": "fetcha/0.2 (+https://www.fetcha.co)" },91 body: JSON.stringify({ clientKey: this.opts.apiKey, ...body }),92 signal: ac.signal,93 });94 const data = (await res.json().catch(() => ({}))) as TaskResult;95 if (!res.ok) throw new CaptchaSolverError("HTTP_ERROR", `2captcha HTTP ${res.status}`);96 return data;97 } finally {98 clearTimeout(t);99 }100 }101102 async balance(): Promise<number> {103 const r = await this.call("/getBalance", {});104 if (r.errorId) throw new CaptchaSolverError(r.errorCode ?? "ERROR", r.errorDescription ?? "2captcha error");105 return Number(r.balance ?? 0);106 }107108 /**109 * Solve a Turnstile challenge. `deadline` (performance.now() based) bounds the wait so that the110 * caller's request timeout is honoured.111 */112 async solveTurnstile(input: { websiteURL: string; params: TurnstileParams; deadline?: number }): Promise<TurnstileSolution> {113 const t0 = performance.now();114 const hardDeadline = Math.min(t0 + this.opts.timeoutMs, input.deadline ?? Infinity);115 this.stats.requested++;116 const task: Record<string, unknown> = { type: "TurnstileTaskProxyless", websiteURL: input.websiteURL, websiteKey: input.params.sitekey };117 if (input.params.action) task.action = input.params.action;118 if (input.params.cData) task.data = input.params.cData;119 if (input.params.chlPageData) task.pagedata = input.params.chlPageData;120 let created: TaskResult;121 try {122 created = await this.call("/createTask", { task });123 } catch (e) {124 this.fail((e as Error).message);125 throw e instanceof CaptchaSolverError ? e : new CaptchaSolverError("NETWORK", (e as Error).message);126 }127 if (created.errorId || !created.taskId) {128 const code = created.errorCode ?? "CREATE_FAILED";129 this.fail(`${code}: ${created.errorDescription ?? ""}`);130 throw new CaptchaSolverError(code, created.errorDescription ?? "2captcha refused the task");131 }132 const taskId = String(created.taskId);133 let wait = this.opts.firstPollMs;134 for (;;) {135 const remaining = hardDeadline - performance.now();136 if (remaining <= 0) {137 this.fail("timeout");138 throw new CaptchaSolverError("TIMEOUT", "2captcha did not solve the challenge in time");139 }140 await new Promise((r) => setTimeout(r, Math.min(wait, remaining)));141 wait = this.opts.pollMs;142 let r: TaskResult;143 try {144 r = await this.call("/getTaskResult", { taskId: created.taskId });145 } catch (e) {146 // transient network error: keep polling147 this.opts.log.warn(`poll error: ${(e as Error).message}`);148 continue;149 }150 if (r.errorId) {151 const code = r.errorCode ?? "SOLVE_FAILED";152 this.fail(`${code}: ${r.errorDescription ?? ""}`);153 throw new CaptchaSolverError(code, r.errorDescription ?? "2captcha could not solve the challenge");154 }155 if (r.status === "ready" && r.solution?.token) {156 const ms = Math.round(performance.now() - t0);157 this.stats.solved++;158 this.stats.spentUsd += this.opts.pricePerSolveUsd;159 this.opts.log.info(`turnstile solved in ${ms} ms (task ${taskId})`);160 return { token: r.solution.token, userAgent: r.solution.userAgent ?? null, taskId, ms, costUsd: this.opts.pricePerSolveUsd };161 }162 }163 }164165 private fail(msg: string) {166 this.stats.failed++;167 this.stats.lastError = msg.slice(0, 200);168 this.opts.log.warn(`solve failed: ${msg}`);169 }170}171172/**173 * Init script: intercept `turnstile.render` (challenge pages and explicit widgets) to capture the174 * parameters 2captcha needs, and keep the callback so the token can be injected later. Stored on a175 * non-enumerable, obscurely named window property.176 */177export const TURNSTILE_HOOK_KEY = "__fx_ts_" + "9c1";178179export function turnstileHookScript(): string {180 return `(() => {181 const KEY = ${JSON.stringify(TURNSTILE_HOOK_KEY)};182 const store = { params: null, callback: null, rendered: 0, hooked: [] };183 try { Object.defineProperty(window, KEY, { value: store, enumerable: false, configurable: true, writable: false }); } catch { window[KEY] = store; }184 const patchTs = (ts) => {185 if (!ts || typeof ts !== 'object' || ts.__fx) return false;186 const origRender = ts.render;187 const patched = function (container, params) {188 try {189 const p = params || {};190 store.rendered++;191 store.params = { sitekey: p.sitekey || null, action: p.action || null, cData: p.cData || null, chlPageData: p.chlPageData || null, hasCallback: typeof p.callback === 'function' };192 if (typeof p.callback === 'function') store.callback = p.callback;193 } catch {}194 // Challenge pages: do NOT render the real widget (its own attempt would consume the challenge195 // data we are about to solve out-of-band); return a fake widget id instead (2captcha's guidance).196 if (store.params && store.params.chlPageData) return 'fx-' + Math.random().toString(36).slice(2, 10);197 return typeof origRender === 'function' ? origRender.apply(this, arguments) : undefined;198 };199 try { Object.defineProperty(patched, 'toString', { value: () => 'function render() { [native code] }' }); } catch {}200 try { ts.render = patched; Object.defineProperty(ts, '__fx', { value: true, enumerable: false, configurable: true }); } catch { return false; }201 return true;202 };203 // 1) Challenge pages load api.js with ?onload=<name>&render=explicit and call turnstile.render from that204 // callback. Wrapping the callback catches the turnstile object at the exact moment it exists.205 const hookOnload = (name) => {206 if (!name || store.hooked.includes(name)) return;207 store.hooked.push(name);208 let cur = window[name];209 const wrap = (fn) => function () {210 try { patchTs(arguments[0] && typeof arguments[0].render === 'function' ? arguments[0] : window.turnstile); } catch {}211 return fn.apply(this, arguments);212 };213 try {214 Object.defineProperty(window, name, { configurable: true, enumerable: true, get() { return cur; }, set(v) { cur = typeof v === 'function' ? wrap(v) : v; } });215 if (typeof cur === 'function') cur = wrap(cur);216 } catch {}217 };218 const onloadName = (src) => { const m = /[?&]onload=([A-Za-z0-9_$]+)/.exec(src || ''); return m ? m[1] : null; };219 const scan = (root) => { try { for (const el of root.querySelectorAll('script[src*="turnstile"]')) hookOnload(onloadName(el.src)); } catch {} };220 try {221 new MutationObserver((muts) => {222 for (const m of muts) for (const n of m.addedNodes) {223 if (n.nodeName === 'SCRIPT' && n.src && /turnstile/.test(n.src)) hookOnload(onloadName(n.src));224 else if (n.querySelectorAll) scan(n);225 }226 }).observe(document, { childList: true, subtree: true });227 } catch {}228 document.addEventListener('DOMContentLoaded', () => scan(document));229 // 2) Standalone widgets / other pages: poll for window.turnstile (api.js sets it on load).230 let n = 0;231 const iv = setInterval(() => { if (patchTs(window.turnstile) || ++n > 1200) clearInterval(iv); }, 25);232})();`;233}234