captchaforge 0.2.34

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, …).
//!
//! # Module map
//!
//! - [`detect`] — heuristic + DOM-based CAPTCHA identification.
//! - [`solver`] — multi-strategy solving chain (behavioural, VLM,
//!   audio, third-party, pattern-cached, human fallback).
//! - [`stealth`] / [`stealth_profiles`] — pre-navigation
//!   fingerprint hardening + named browser-vendor profiles.
//! - [`warmup`] — natural pre-captcha activity (mouse / scroll
//!   entropy) so passive challenges score the visitor as human.
//! - [`behavior`] — primitive realistic gestures (Bézier mouse,
//!   variable-cadence typing, hover dwell, touch swipe, jitter).
//! - [`cookies`] — capture / replay session cookies, vendor-filter.
//! - [`frame`] — same-origin iframe + cross-origin CDP frame walk.
//! - [`provider`] — pluggable vendor-rule registry.
//! - [`config`] — Tier-A `.captchaforge.toml` discovery.
//! - [`backends`] — local backend probe (Ollama / Whisper / Tesseract).
//! - [`stt`] — speech-to-text endpoint ladder.
//! - [`sdk`] — one-call orchestration ([`solve_url`], [`prepare_page`],
//!   [`auto_solve_with_retries`], [`dismiss_cookie_consent`],
//!   [`dismiss_chat_widget`], [`wait_for_no_captcha`]).
//! - [`prelude`] — glob-import target for the most-used types.
//!
//! Three layered consumption modes:
//!
//! 1. **One-call** — `captchaforge::solve_url(&page, url, None).await?`
//!    when you want the whole chain wired for you.
//! 2. **Building blocks** — call [`detect::detect`] then drive a
//!    custom [`solver::CaptchaSolverChain`] when you need
//!    telemetry, custom solvers, or non-default cache semantics.
//! 3. **Primitives** — embed individual modules ([`stealth`],
//!    [`behavior`], [`frame`], [`cookies`]) inside an existing
//!    chromiumoxide flow without touching the solver chain.
//!
//! Originally extracted from
//! [golemn-browser](https://github.com/santhsecurity/golemn) 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, …).
//!
//! # Example — one-call solve
//!
//! ```rust,no_run
//! use captchaforge::prelude::*;
//! # async fn run(page: &chromiumoxide::Page) -> anyhow::Result<()> {
//! match solve_url(page, "https://example.com", Some(StealthProfile::ChromeMacStable)).await? {
//!     None => println!("no captcha detected"),
//!     Some(r) if r.success => println!("solved via {:?} in {}ms", r.method, r.time_ms),
//!     Some(r) => println!("failed: {:?}", r.method),
//! }
//! # Ok(()) }
//! ```
//!
//! # Example — detect + custom chain
//!
//! ```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 — primitives only (no chain)
//!
//! ```rust,no_run
//! use captchaforge::behavior::{mouse_move_bezier, click_realistic};
//! use captchaforge::frame::evaluate_in_all_frames;
//! # 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?;
//! let titles: Vec<String> = evaluate_in_all_frames(page, "document.title").await?;
//! # let _ = titles;
//! # Ok(()) }
//! ```

#![forbid(unsafe_code)]

pub mod audio_dsp;
pub mod backends;
pub mod behavior;
pub mod chromium_sidecar;
pub mod config;
pub mod cookies;
pub mod detect;
pub mod frame;
pub mod frame_graph;
pub mod keystroke_timing;
pub mod mouse_sampler;
pub mod mouse_traces;
pub mod prelude;
pub mod provider;
pub mod rule_watcher;
pub mod sdk;
pub mod solver;
pub mod stealth;
pub mod stealth_profiles;
pub mod stt;
pub mod adversarial_replay;
pub mod fingerprint_lru;
pub mod mobile_screenshot;
pub mod mobile_webview;
pub mod plugin;
pub mod proxy_pool;
pub mod telemetry;
pub mod trace_ingest;
pub mod training_corpus;
pub mod vendor_scraper;
pub mod vlm_dataset;
pub mod warmup;

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

// 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};

// Top-level re-exports for the most common SDK surface so users can
// `use captchaforge::{Capabilities, AudioCaptchaSolver, ...}` without
// remembering which submodule each name lives in.
pub use backends::{Capabilities, OcrBackend, SttBackend, SttKind, VlmBackend};
pub use solver::{
    AudioCaptchaSolver, BehavioralCaptchaSolver, CaptchaSolveResult, CaptchaSolver,
    CaptchaSolverChain, CaptchaType, OcrCaptchaSolver, SolveMethod, TokenCache, VlmCaptchaSolver,
};

// One-call SDK helpers. Their definitions live in [`sdk`] — these
// re-exports give SDK consumers a flat `captchaforge::solve_url(…)`
// import path matching the established `captchaforge::auto_solve(…)`
// shape, without forcing a `captchaforge::sdk::` prefix.
pub use sdk::{
    auto_solve_with_retries, dismiss_chat_widget, dismiss_cookie_consent, prepare_page,
    recommended_chromium_args, solve_url, wait_for_no_captcha,
};

/// 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.
///
/// For a "navigate + solve in one call" entry point, see
/// [`solve_url`] (re-exported from [`sdk`]).
pub async fn auto_solve(
    page: &chromiumoxide::Page,
) -> anyhow::Result<Option<solver::CaptchaSolveResult>> {
    // Probe + log local backends on the first call per process so
    // operators see at a glance which solver paths are wired (Ollama
    // VLM / Whisper STT / Tesseract OCR). Cached behind a OnceLock
    // so the network probes only run once even if auto_solve is
    // called per-request in a high-throughput service.
    {
        use std::sync::OnceLock;
        static LOGGED: OnceLock<()> = OnceLock::new();
        if LOGGED.get().is_none() {
            let caps = backends::probe().await;
            tracing::info!(
                capabilities = %caps.summary(),
                "captchaforge: backend probe (run `captchaforge doctor` for details)"
            );
            for hint in caps.install_hints() {
                tracing::warn!("captchaforge install hint: {hint}");
            }
            let _ = LOGGED.set(());
        }
    }
    // 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))
}