captchaforge 0.2.20

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers (Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio, sliders).
Documentation
//! captchaforge — automatic CAPTCHA detection and solving for
//! `chromiumoxide`-driven headless browsers.
//!
//! Extracted from [golemn-browser](https://github.com/santhsecurity/golemn)
//! (originally GPL-3.0) and re-licensed MIT OR Apache-2.0 by the
//! original author so it can stand alone and be embedded by other
//! Santh-ecosystem tools (wafrift, gossan, sear, …).
//!
//! Two modules:
//! - [`detect`] — heuristic + DOM-based CAPTCHA identification
//! - [`solver`] — multi-strategy solving chain
//!     - **Behavioural** — realistic mouse + timing for Turnstile /
//!       reCAPTCHA v3
//!     - **Vision-LLM** — screenshot → multimodal model → click /
//!       type the answer
//!     - **Audio bypass** — accessibility audio challenge → STT →
//!       transcribed answer
//!     - **Crowd-sourced** — per-domain pattern memory of which
//!       method has succeeded before
//!     - **Behavioural / human fallback** — return unsolved for
//!       human-in-the-loop
//!
//! The crate exposes a `CaptchaSolver` trait so consumers can plug
//! in their own solver strategies without forking.
//!
//! # Example — detect and solve
//!
//! ```rust,no_run
//! use captchaforge::{detect, solver::CaptchaSolverChain};
//! # async fn run(page: &chromiumoxide::Page) -> anyhow::Result<()> {
//! let info = detect::detect(page).await?;
//! if detect::is_captcha(&info) {
//!     let chain = CaptchaSolverChain::default_chain();
//!     // chain.solve(page, &info).await?;
//! }
//! # Ok(()) }
//! ```
//!
//! # Example — pattern store
//!
//! ```rust
//! use captchaforge::solver::{CaptchaType, PatternStore, SolveMethod};
//!
//! let store = PatternStore::default();
//! store.record("example.com", &CaptchaType::CloudflareTurnstile, true, 1200, SolveMethod::BehavioralBypass);
//! assert_eq!(store.best_method("example.com", &CaptchaType::CloudflareTurnstile), Some(SolveMethod::BehavioralBypass));
//! ```
//!
//! # Example — frame evaluation
//!
//! ```rust,no_run
//! use captchaforge::frame::evaluate_in_all_frames;
//! # async fn run(page: &chromiumoxide::Page) -> anyhow::Result<()> {
//! let titles: Vec<String> = evaluate_in_all_frames(page, "document.title").await?;
//! # Ok(()) }
//! ```
//!
//! # Example — behavior simulation
//!
//! ```rust,no_run
//! use captchaforge::behavior::{mouse_move_bezier, click_realistic};
//! # async fn run(page: &chromiumoxide::Page) -> anyhow::Result<()> {
//! mouse_move_bezier(page, 0.0, 0.0, 400.0, 300.0).await?;
//! click_realistic(page, 400.0, 300.0).await?;
//! # Ok(()) }
//! ```

#![forbid(unsafe_code)]

pub mod behavior;
pub mod config;
pub mod cookies;
pub mod detect;
pub mod frame;
pub mod provider;
pub mod solver;
pub mod stealth;
pub mod stealth_profiles;
pub mod telemetry;
pub mod warmup;

pub use config::Config;
pub use cookies::CapturedCookie;

// Back-compat re-export so code that used `golemn_browser::captcha_detect`
// can `use captchaforge::captcha_detect` instead.
pub use detect as captcha_detect;

pub use provider::{CaptchaProvider, ProviderRegistry};

/// One-call convenience: detect + solve in a single API.
///
/// Equivalent to:
///
/// ```ignore
/// let info = captchaforge::detect::detect(page).await?;
/// if !captchaforge::detect::is_captcha(&info) {
///     return Ok(None);
/// }
/// let chain = captchaforge::solver::CaptchaSolverChain::default_chain();
/// Ok(Some(chain.solve(page, &info).await))
/// ```
///
/// Returns:
///   * `Ok(None)` — no captcha was detected on the page.
///   * `Ok(Some(result))` — captcha detected; `result.success` indicates
///     whether the chain solved it. Failed solves still return Some
///     so the caller can inspect the screenshot for human fallback.
///   * `Err(_)` — the detection step itself failed (page eval error,
///     CDP transport error). Solver errors are swallowed into a
///     no-op `CaptchaSolveResult` and don't surface here.
///
/// Power users that need a custom chain (telemetry, token cache,
/// pattern store, custom solvers) construct their own
/// [`solver::CaptchaSolverChain`] and call `chain.solve()` directly.
pub async fn auto_solve(
    page: &chromiumoxide::Page,
) -> anyhow::Result<Option<solver::CaptchaSolveResult>> {
    // Best-effort stealth — overrides navigator.webdriver, fakes
    // plugins/languages/WebGL fingerprint, etc. Errors are non-fatal:
    // some chromium builds reject `addScriptToEvaluateOnNewDocument`
    // mid-session, and we'd rather still try to solve than refuse.
    // Production deployments should call `stealth::apply_stealth(page)`
    // themselves immediately after creating the page (BEFORE the
    // first goto) — that catches first-page detection probes too.
    if let Err(e) = stealth::apply_stealth(page).await {
        tracing::debug!("stealth::apply_stealth failed (continuing): {e}");
    }
    let info = detect::detect(page).await?;
    if !detect::is_captcha(&info) {
        return Ok(None);
    }
    // Discover Tier-A config (.captchaforge.toml etc) and build the
    // chain from it — so a user's TOML overrides for cache TTL, VLM
    // endpoint, third-party service all reach this one-call path.
    // Discovery returns Config::default() when no file is present, so
    // a no-config install gets the same chain shape as before.
    // Config::build_chain() also installs the bundled provider registry.
    let cfg = config::Config::discover()?;
    let chain = cfg.build_chain()?;
    Ok(Some(chain.solve(page, &info).await))
}