captchaforge 0.2.38

[DO NOT USE — UNDER ACTIVE DEVELOPMENT, NOT PRODUCTION-READY] Captcha solver scaffolding for Firefox + BiDi-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 / BiDi fingerprint layer that no flag-based stealth has cleared. Watch the repo; do not depend on this for any real workload.
Documentation
//! Characterisation of the `navigator.webdriver` tell on plain BiDi Firefox.
//!
//! Load-bearing platform fact, proven live (not assumed): on a Firefox driven
//! over BiDi, the active remote agent (which `--remote-debugging-port` activates
//! and which BiDi control requires) sets `navigator.webdriver = true`, and the
//! launch pref `dom.webdriver.enabled=false` does NOT override it. So:
//!
//!   - the **native** value cannot be hidden by prefs on plain BiDi — full
//!     coverage (Workers / fresh iframes / pre-preload re-reads, i.e. the
//!     `webDriverAdvanced` signal) needs the **reynard engine build**, which
//!     patches out the automation→`webdriver` coupling;
//!   - what we CAN do without the engine is mask the **main world** via the JS
//!     getter override in `apply_stealth`.
//!
//! This test pins both facts so a future "just set the pref" attempt is caught
//! immediately (the pref attempt was tried and falsified here). If a Firefox /
//! rustenium change ever makes the native value `false` with the pref, the first
//! assertion flips — that's a gap CLOSING, and worth knowing.
//!
//! Live: requires Firefox. Offline otherwise (navigates only `about:blank`).

use captchaforge::browser::{launch_firefox, FoxBrowserConfig};

fn profile_dir(tag: &str) -> String {
    std::env::temp_dir()
        .join(format!("cf-webdriver-pref-{}-{}", std::process::id(), tag))
        .to_string_lossy()
        .to_string()
}

#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn pref_cannot_hide_native_webdriver_but_apply_stealth_masks_main_world() {
    let page = launch_firefox(FoxBrowserConfig {
        headless: true,
        profile_dir: Some(profile_dir("native")),
        viewport_width: 1280,
        viewport_height: 720,
        // The full guise pref set, including dom.webdriver.enabled=false.
        user_js_content: Some(captchaforge::stealth::automation_prefs()),
        ..Default::default()
    })
    .await
    .expect("launch Firefox");
    page.goto("about:blank").await.expect("navigate about:blank");

    // 1) NATIVE value (no JS override yet): the remote agent forces it true even
    //    with dom.webdriver.enabled=false. This is the engine-level tell.
    let native: String = page
        .evaluate("String(navigator.webdriver)")
        .await
        .expect("evaluate")
        .into_value()
        .expect("string");
    eprintln!("[webdriver] native (pref only)    = {native}");
    assert_eq!(
        native, "true",
        "plain BiDi: the remote agent overrides dom.webdriver.enabled — native \
         navigator.webdriver stays true (engine/reynard path required to flip it). \
         If this is now \"false\", the platform changed and the tell is closing."
    );

    // 2) After apply_stealth, the MAIN-WORLD value is masked false — the part we
    //    can do on plain Firefox without the engine build.
    captchaforge::apply_stealth(&page)
        .await
        .expect("apply_stealth");
    let masked: String = page
        .evaluate("String(navigator.webdriver)")
        .await
        .expect("evaluate")
        .into_value()
        .expect("string");
    eprintln!("[webdriver] main world (stealthed) = {masked}");
    assert!(
        masked == "false" || masked == "undefined",
        "apply_stealth must mask navigator.webdriver in the main world (got {masked:?})"
    );

    let _ = page.close().await;
}