| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import { chromium, type Browser, type BrowserContext } from "playwright-core"; |
|
|
| |
| |
| |
| |
| const TARGET_URL = "https://geminigen.ai/app/video-gen"; |
| const TOKEN_TTL_MS = 270_000; |
|
|
| interface TokenCache { |
| token: string; |
| expiresAt: number; |
| } |
|
|
| let _cache: TokenCache | null = null; |
|
|
| |
| async function applyStealthPatches(context: BrowserContext): Promise<void> { |
| await context.addInitScript(() => { |
| |
| Object.defineProperty(navigator, "webdriver", { |
| get: () => undefined, |
| }); |
|
|
| |
| (window as any).chrome = { |
| runtime: { |
| onConnect: null, |
| onMessage: null, |
| }, |
| }; |
|
|
| |
| const originalQuery = window.navigator.permissions.query.bind( |
| window.navigator.permissions |
| ); |
| (window.navigator.permissions as any).query = (params: any) => { |
| if (params.name === "notifications") { |
| return Promise.resolve({ state: Notification.permission }); |
| } |
| return originalQuery(params); |
| }; |
|
|
| |
| Object.defineProperty(navigator, "plugins", { |
| get: () => [ |
| { name: "Chrome PDF Plugin" }, |
| { name: "Chrome PDF Viewer" }, |
| { name: "Native Client" }, |
| ], |
| }); |
|
|
| |
| Object.defineProperty(navigator, "languages", { |
| get: () => ["zh-TW", "zh", "en-US", "en"], |
| }); |
|
|
| |
| const origToDataURL = HTMLCanvasElement.prototype.toDataURL; |
| HTMLCanvasElement.prototype.toDataURL = function (type?: string) { |
| const dataURL = origToDataURL.call(this, type); |
| return dataURL; |
| }; |
| }); |
| } |
|
|
| |
| async function waitForTurnstileToken( |
| context: BrowserContext, |
| timeoutMs = 30_000 |
| ): Promise<string | null> { |
| const page = await context.newPage(); |
|
|
| try { |
| |
| await page.route( |
| "**/*.{png,jpg,jpeg,gif,webp,svg,ico,woff,woff2,ttf,otf,mp4,mp3,wav}", |
| (route) => route.abort() |
| ); |
|
|
| await page.goto(TARGET_URL, { |
| waitUntil: "domcontentloaded", |
| timeout: 30_000, |
| }); |
|
|
| |
| |
| const token = await page |
| .waitForFunction( |
| () => { |
| |
| const standard = document.querySelector<HTMLInputElement>( |
| 'input[name="cf-turnstile-response"]' |
| ); |
| if (standard?.value && standard.value.length > 20) return standard.value; |
|
|
| |
| const ta = document.querySelector<HTMLTextAreaElement>( |
| 'textarea[name="cf-turnstile-response"]' |
| ); |
| if (ta?.value && ta.value.length > 20) return ta.value; |
|
|
| |
| const win = window as any; |
| if (win.__cfTurnstileToken && win.__cfTurnstileToken.length > 20) { |
| return win.__cfTurnstileToken; |
| } |
|
|
| return null; |
| }, |
| { timeout: timeoutMs, polling: 500 } |
| ) |
| .then((h) => h.jsonValue() as Promise<string>) |
| .catch(() => null); |
|
|
| return token; |
| } finally { |
| await page.close().catch(() => {}); |
| } |
| } |
|
|
| export interface SolveResult { |
| token: string; |
| cached: boolean; |
| solvedInMs?: number; |
| } |
|
|
| export async function solveTurnstile(): Promise<SolveResult> { |
| |
| if (_cache && _cache.expiresAt > Date.now()) { |
| return { token: _cache.token, cached: true }; |
| } |
|
|
| const start = Date.now(); |
| let browser: Browser | null = null; |
|
|
| try { |
| |
| |
| |
| let executablePath: string; |
|
|
| if (process.env.AWS_LAMBDA_FUNCTION_NAME || process.env.VERCEL) { |
| |
| const chromiumMin = await import("@sparticuz/chromium-min"); |
| const chromiumPack = |
| process.env.CHROMIUM_PACK_URL || |
| "https://github.com/Sparticuz/chromium/releases/download/v133.0.0/chromium-v133.0.0-pack.tar"; |
| executablePath = await chromiumMin.default.executablePath(chromiumPack); |
|
|
| browser = await chromium.launch({ |
| args: chromiumMin.default.args, |
| executablePath, |
| headless: true, |
| }); |
| } else { |
| |
| browser = await chromium.launch({ headless: true }); |
| } |
|
|
| const context = await browser.newContext({ |
| userAgent: |
| "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36", |
| viewport: { width: 1280, height: 800 }, |
| locale: "zh-TW", |
| timezoneId: "Asia/Taipei", |
| }); |
|
|
| await applyStealthPatches(context); |
|
|
| const token = await waitForTurnstileToken(context, 25_000); |
| await context.close().catch(() => {}); |
|
|
| if (!token) { |
| throw new Error("Turnstile token not found in page DOM after 25s"); |
| } |
|
|
| _cache = { token, expiresAt: Date.now() + TOKEN_TTL_MS }; |
| return { token, cached: false, solvedInMs: Date.now() - start }; |
| } finally { |
| if (browser) await browser.close().catch(() => {}); |
| } |
| } |
|
|
| |
| export function invalidateCache(): void { |
| _cache = null; |
| } |
|
|