kioai / artifacts /api-server /src /captcha.ts
kinaiok
Initial deployment setup for Hugging Face Spaces
5ef6e9d
/**
* Turnstile token resolver โ€” ไธ‰ๅฑคๅ„ชๅ…ˆ็ญ–็•ฅ๏ผš
*
* 1. ่‡ชๅปบ Playwright ๆฑ‚่งฃๅ™จ๏ผˆplaywright_solver_url ๅทฒ่จญๅฎš๏ผ‰
* 2. CapSolver API๏ผˆcapsolver_api_key ๅทฒ่จญๅฎš๏ผ‰
* 3. ๅ›ž้€€ bypass token๏ผˆๅฟ…็„ถๅคฑๆ•—๏ผŒไฝ†ๆœ‰ๆธ…ๆฅš้Œฏ่ชค่จŠๆฏ๏ผ‰
*
* Token ๅฟซๅ– 4.5 ๅˆ†้˜๏ผˆgeminigen.ai Turnstile token ็ด„ 5 ๅˆ†้˜ๆœ‰ๆ•ˆ๏ผ‰ใ€‚
*/
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; // 4.5 minutes
interface CachedToken {
token: string;
expiresAt: number;
}
let cachedToken: CachedToken | null = null;
// โ”€โ”€โ”€ DB helper โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
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;
}
}
// โ”€โ”€โ”€ Strategy 1: Self-hosted Playwright solver โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
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;
}
}
// โ”€โ”€โ”€ Strategy 2: CapSolver API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
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;
}
}
// โ”€โ”€โ”€ Strategy 3: YesCaptcha API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
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;
}
}
// โ”€โ”€โ”€ Public API โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€
/**
* Get a valid Cloudflare Turnstile token for geminigen.ai.
*
* Priority:
* 1. Self-hosted Playwright solver (playwright_solver_url in config) โ€” ๅ…่ฒป
* 2. YesCaptcha API (yescaptcha_api_key in config) โ€” ๆœ‰ๅ…่ฒป้กๅบฆ
* 3. CapSolver API (capsolver_api_key in config) โ€” ไป˜่ฒปๅ‚™ๆด
* 4. Bypass placeholder (will fail โ€” no solver configured)
*/
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;
// Strategy 1: Self-hosted Playwright solver
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");
}
// Strategy 2: 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");
}
// Strategy 3: 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";
}
/** Force-clear the cached token (e.g., if geminigen.ai rejects it) */
export function invalidateTurnstileToken(): void {
cachedToken = null;
}