captchaforge 0.2.35

[DO NOT USE — UNDER ACTIVE DEVELOPMENT, NOT PRODUCTION-READY] Captcha solver scaffolding for chromiumoxide-driven browsers. The architecture is in place (vendor solvers, retry-loop iframe walking, VLM provider abstraction, real-WAF bench harness) but the live-vendor success rate is still 0% — Cloudflare Turnstile / hCaptcha / reCAPTCHA detect us at a TLS / CDP fingerprint layer that no flag-based stealth has cleared. Watch the repo; do not depend on this for any real workload.
Documentation
//! Google reCAPTCHA v2/v3 detector.
//!
//! Distinguishes v3 (invisible badge) from v2 (interactive checkbox)
//! by inspecting `data-size="invisible"` on the matching element.
use anyhow::Result;
use async_trait::async_trait;
use chromiumoxide::Page;

use super::{parse_detection_result, CaptchaInfo, Detector};

/// Detector for Google reCAPTCHA v2 and v3.
pub struct RecaptchaDetector;

impl RecaptchaDetector {
    /// JS payload evaluated against the live page.
    pub const JS: &'static str = r#"(function() {
    const recaptcha = document.querySelector('.g-recaptcha');
    const recaptchaScript = document.querySelector('script[src*="google.com/recaptcha"]');
    if (recaptcha || recaptchaScript) {
        let el = recaptcha;
        if (!el) {
            const fallback = document.querySelector('[data-sitekey]');
            if (fallback && !fallback.hasAttribute('data-hcaptcha-sitekey') && !fallback.hasAttribute('data-turnstile-sitekey')) {
                el = fallback;
            }
        }
        if (el) {
            const size = el.getAttribute('data-size');
            return {
                kind: size === 'invisible' ? 'recaptcha_v3' : 'recaptcha_v2',
                site_key: el.getAttribute('data-sitekey'),
                container: '.g-recaptcha'
            };
        }
        return { kind: 'recaptcha_v3', site_key: null, container: null };
    }
    return { kind: 'none', site_key: null, container: null };
})()"#;
}

#[async_trait]
impl Detector for RecaptchaDetector {
    fn name(&self) -> &'static str {
        "recaptcha"
    }
    fn priority(&self) -> i32 {
        20
    }
    async fn detect(&self, page: &Page) -> Result<Option<CaptchaInfo>> {
        let raw = page.evaluate(Self::JS).await?;
        let val = raw.into_value::<serde_json::Value>()?;
        Ok(parse_detection_result(val))
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn recaptcha_detector_js_has_selectors() {
        assert!(RecaptchaDetector::JS.contains(".g-recaptcha"));
        assert!(RecaptchaDetector::JS.contains("google.com/recaptcha"));
    }

    #[test]
    fn recaptcha_detector_excludes_other_provider_sitekeys() {
        assert!(RecaptchaDetector::JS.contains("!fallback.hasAttribute('data-hcaptcha-sitekey')"));
        assert!(RecaptchaDetector::JS.contains("!fallback.hasAttribute('data-turnstile-sitekey')"));
    }
}