captchaforge 0.2.40

Captcha detection and solving for Firefox and BiDi-driven browsers. Detection, vendor solver scaffolding, trusted cross-origin click delivery into nested OOPIFs, and stealth personas are implemented and tested; broad live-vendor solve rates are not yet benchmarked.
Documentation
//! `cargo run --example solve_url_simple -- <https-url>`
//!
//! The simplest possible captchaforge usage:
//!
//! 1. Launch a Firefox browser via [`captchaforge::browser_runtime`].
//! 2. Hand a fresh page + URL to [`captchaforge::solve_url`].
//! 3. Print the result.
//!
//! `solve_url` wires `prepare_page` (stealth + named profile) BEFORE
//! navigation, warms the loaded page, then runs `auto_solve`. Three
//! lines of captchaforge in total. Pass `--profile <name>` (e.g.
//! `--profile chrome-mac`) to pin a named [`StealthProfile`].

use anyhow::{Context, Result};
use captchaforge::browser_runtime::{drive_browser, BrowserDriveOptions};
use captchaforge::stealth_profiles::named_profile;
use captchaforge::{solve_url, StealthProfile};

#[tokio::main]
async fn main() -> Result<()> {
    let mut args = std::env::args().skip(1);
    let url = args
        .next()
        .context("usage: solve_url_simple <url> [--profile <name>]")?;
    let profile = parse_profile_arg(args)?;

    let target_url = url.clone();
    let outcome = drive_browser(
        "about:blank",
        BrowserDriveOptions::default(),
        |page| async move { solve_url(&page, &target_url, profile).await },
    )
    .await
    .context("solve url in browser")?;

    match outcome {
        None => println!("no captcha detected at {url}"),
        Some(r) => println!(
            "captcha {} via {:?} in {}ms (confidence={:.2}, verified={:?})",
            if r.success { "SOLVED" } else { "FAILED" },
            r.method,
            r.time_ms,
            r.confidence,
            r.verified_outcome,
        ),
    }

    Ok(())
}

/// Parse `--profile <name>` out of the remaining CLI args. Returns
/// `Ok(None)` when the flag is absent.
fn parse_profile_arg(mut args: impl Iterator<Item = String>) -> Result<Option<StealthProfile>> {
    while let Some(a) = args.next() {
        if a == "--profile" {
            let name = args.next().context("--profile requires a value")?;
            let profile = named_profile(&name).ok_or_else(|| {
                anyhow::anyhow!(
                    "unknown stealth profile {name:?}; try `captchaforge profiles` for the full list"
                )
            })?;
            return Ok(Some(profile));
        }
    }
    Ok(None)
}