| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { db, configTable } from "@workspace/db"; |
| import { eq } from "drizzle-orm"; |
|
|
| const CAPSOLVER_URL = "https://api.capsolver.com"; |
| const YESCAPTCHA_URL = "https://api.yescaptcha.com"; |
| const TURNSTILE_SITE_KEY = "0x4AAAAAACDBydnKT0zYzh2H"; |
| const TURNSTILE_PAGE_URL = "https://geminigen.ai"; |
| const TOKEN_TTL_MS = 270_000; |
|
|
| interface CachedToken { |
| token: string; |
| expiresAt: number; |
| } |
|
|
| let cachedToken: CachedToken | null = null; |
|
|
| |
|
|
| async function getConfig(key: string): Promise<string | null> { |
| try { |
| const rows = await db |
| .select() |
| .from(configTable) |
| .where(eq(configTable.key, key)) |
| .limit(1); |
| return rows[0]?.value?.trim() || null; |
| } catch { |
| return null; |
| } |
| } |
|
|
| |
|
|
| async function solveWithPlaywright(solverUrl: string): Promise<string | null> { |
| try { |
| const solverSecret = await getConfig("playwright_solver_secret"); |
| const headers: Record<string, string> = { |
| "Content-Type": "application/json", |
| }; |
| if (solverSecret) headers["X-Solver-Secret"] = solverSecret; |
|
|
| console.log(`[captcha] Calling Playwright solver: ${solverUrl}`); |
| const resp = await fetch(solverUrl, { |
| method: "POST", |
| headers, |
| body: JSON.stringify({}), |
| signal: AbortSignal.timeout(55_000), |
| }); |
|
|
| const data = (await resp.json()) as { |
| token?: string; |
| cached?: boolean; |
| solvedInMs?: number; |
| error?: string; |
| }; |
|
|
| if (!resp.ok || !data.token) { |
| console.error( |
| `[captcha] Playwright solver error (${resp.status}): ${data.error || "no token"}` |
| ); |
| return null; |
| } |
|
|
| console.log( |
| `[captcha] Playwright solved โ cached=${data.cached}, ms=${data.solvedInMs ?? "N/A"}` |
| ); |
| return data.token; |
| } catch (err: any) { |
| console.error(`[captcha] Playwright solver fetch error: ${err?.message}`); |
| return null; |
| } |
| } |
|
|
| |
|
|
| async function solveWithCapsolver(apiKey: string): Promise<string | null> { |
| try { |
| const createResp = await fetch(`${CAPSOLVER_URL}/createTask`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ |
| clientKey: apiKey, |
| task: { |
| type: "AntiTurnstileTaskProxyless", |
| websiteURL: TURNSTILE_PAGE_URL, |
| websiteKey: TURNSTILE_SITE_KEY, |
| }, |
| }), |
| }); |
|
|
| const createData = (await createResp.json()) as { |
| errorId?: number; |
| errorCode?: string; |
| errorDescription?: string; |
| taskId?: string; |
| }; |
|
|
| if (createData.errorId || !createData.taskId) { |
| console.error( |
| `[captcha] CapSolver create error: ${createData.errorCode} โ ${createData.errorDescription}` |
| ); |
| return null; |
| } |
|
|
| const taskId = createData.taskId; |
| console.log(`[captcha] CapSolver task created: ${taskId}`); |
|
|
| const maxWait = 120_000; |
| const start = Date.now(); |
| while (Date.now() - start < maxWait) { |
| await new Promise((r) => setTimeout(r, 3000)); |
|
|
| const resultResp = await fetch(`${CAPSOLVER_URL}/getTaskResult`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ clientKey: apiKey, taskId }), |
| }); |
|
|
| const resultData = (await resultResp.json()) as { |
| errorId?: number; |
| errorCode?: string; |
| status?: string; |
| solution?: { token?: string }; |
| }; |
|
|
| if (resultData.errorId) { |
| console.error(`[captcha] CapSolver result error: ${resultData.errorCode}`); |
| return null; |
| } |
|
|
| if (resultData.status === "ready") { |
| const token = resultData.solution?.token; |
| if (token) { |
| console.log(`[captcha] CapSolver solved! Token: ${token.substring(0, 20)}...`); |
| return token; |
| } |
| return null; |
| } |
| } |
|
|
| console.error("[captcha] CapSolver timed out after 120s"); |
| return null; |
| } catch (err: any) { |
| console.error(`[captcha] CapSolver error: ${err?.message}`); |
| return null; |
| } |
| } |
|
|
| |
|
|
| async function solveWithYesCaptcha(apiKey: string): Promise<string | null> { |
| try { |
| const createResp = await fetch(`${YESCAPTCHA_URL}/createTask`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ |
| clientKey: apiKey, |
| task: { |
| type: "TurnstileTaskProxylessM1", |
| websiteURL: TURNSTILE_PAGE_URL, |
| websiteKey: TURNSTILE_SITE_KEY, |
| }, |
| }), |
| }); |
|
|
| const createData = (await createResp.json()) as { |
| errorId?: number; |
| errorCode?: string; |
| errorDescription?: string; |
| taskId?: string; |
| }; |
|
|
| if (createData.errorId || !createData.taskId) { |
| console.error( |
| `[captcha] YesCaptcha create error: ${createData.errorCode} โ ${createData.errorDescription}` |
| ); |
| return null; |
| } |
|
|
| const taskId = createData.taskId; |
| console.log(`[captcha] YesCaptcha task created: ${taskId}`); |
|
|
| const maxWait = 120_000; |
| const start = Date.now(); |
| while (Date.now() - start < maxWait) { |
| await new Promise((r) => setTimeout(r, 3000)); |
|
|
| const resultResp = await fetch(`${YESCAPTCHA_URL}/getTaskResult`, { |
| method: "POST", |
| headers: { "Content-Type": "application/json" }, |
| body: JSON.stringify({ clientKey: apiKey, taskId }), |
| }); |
|
|
| const resultData = (await resultResp.json()) as { |
| errorId?: number; |
| errorCode?: string; |
| status?: string; |
| solution?: { token?: string }; |
| }; |
|
|
| if (resultData.errorId) { |
| console.error(`[captcha] YesCaptcha result error: ${resultData.errorCode}`); |
| return null; |
| } |
|
|
| if (resultData.status === "ready") { |
| const token = resultData.solution?.token; |
| if (token) { |
| console.log(`[captcha] YesCaptcha solved! Token: ${token.substring(0, 20)}...`); |
| return token; |
| } |
| return null; |
| } |
| } |
|
|
| console.error("[captcha] YesCaptcha timed out after 120s"); |
| return null; |
| } catch (err: any) { |
| console.error(`[captcha] YesCaptcha error: ${err?.message}`); |
| return null; |
| } |
| } |
|
|
| |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| export async function getTurnstileToken(): Promise<string> { |
| if (cachedToken && cachedToken.expiresAt > Date.now()) { |
| console.log("[captcha] Using cached Turnstile token"); |
| return cachedToken.token; |
| } |
|
|
| let token: string | null = null; |
|
|
| |
| const solverUrl = await getConfig("playwright_solver_url"); |
| if (solverUrl) { |
| token = await solveWithPlaywright(solverUrl); |
| if (token) { |
| cachedToken = { token, expiresAt: Date.now() + TOKEN_TTL_MS }; |
| return token; |
| } |
| console.warn("[captcha] Playwright solver failed โ trying YesCaptcha"); |
| } |
|
|
| |
| const yescaptchaKey = await getConfig("yescaptcha_api_key"); |
| if (yescaptchaKey) { |
| token = await solveWithYesCaptcha(yescaptchaKey); |
| if (token) { |
| cachedToken = { token, expiresAt: Date.now() + TOKEN_TTL_MS }; |
| return token; |
| } |
| console.warn("[captcha] YesCaptcha failed โ trying CapSolver"); |
| } |
|
|
| |
| const capsolverKey = await getConfig("capsolver_api_key"); |
| if (capsolverKey) { |
| token = await solveWithCapsolver(capsolverKey); |
| if (token) { |
| cachedToken = { token, expiresAt: Date.now() + TOKEN_TTL_MS }; |
| return token; |
| } |
| console.warn("[captcha] CapSolver failed โ no further fallback"); |
| } |
|
|
| if (!solverUrl && !yescaptchaKey && !capsolverKey) { |
| console.warn( |
| "[captcha] No Turnstile solver configured โ set playwright_solver_url, yescaptcha_api_key, or capsolver_api_key in Admin โ Config" |
| ); |
| } |
|
|
| return "TURNSTILE_BYPASS"; |
| } |
|
|
| |
| export function invalidateTurnstileToken(): void { |
| cachedToken = null; |
| } |
|
|