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
//! Proving test for the warmup trusted-input contract.
//!
//! `warmup::warm_session` exists to make a visitor look human to the *passive*
//! scorers (PerimeterX, DataDome, Akamai, reCAPTCHA v3) — all of which gate on
//! `event.isTrusted`. A previous implementation dispatched its mouse motion with
//! `document.dispatchEvent(new MouseEvent('mousemove', …))`, which carries
//! `isTrusted === false`: a burst of those is a *stronger* bot signal than no
//! motion, so the warmup was actively counterproductive. The fix routes every
//! pointer/scroll action through the trusted BiDi `input.performActions` path.
//!
//! These tests install an in-page recorder, run real input against a live
//! headless Firefox, and assert the invariant that matters: the action produced
//! pointer events, and **every** event it produced is trusted. They are truth
//! tests (assert `isTrusted`), not "did-not-panic" tests. The second test covers
//! the press→drag→release primitives the slider solver's `drag_handle` relies on
//! (`mouse_down` / `move_mouse_to` / `mouse_up`), which is also an isTrusted-gated
//! path (GeeTest / AWS WAF / DataDome puzzle sliders).

use std::time::Duration;

use captchaforge::warmup::warm_session;

mod common;

#[tokio::test]
async fn warm_session_emits_only_trusted_events() {
    let browser = common::launch_browser().await;
    let page = browser.new_page("about:blank").await.unwrap();

    // A page with the elements warm_session hovers (a/p/h1/button) plus a
    // recorder that captures isTrusted for every mouse/wheel event, in the
    // capture phase so nothing can stop it before we read it back.
    let setup: String = page
        .evaluate(
            r#"(() => {
                document.body.innerHTML =
                    '<h1>warm</h1><p>lorem ipsum dolor sit amet</p>' +
                    '<a id="lnk">a link</a><button id="b">btn</button>' +
                    '<div style="height:3000px"></div>';
                window.__rec = { move: 0, wheel: 0, untrusted: [] };
                const note = (e) => {
                    if (e.type === 'mousemove') window.__rec.move++;
                    if (e.type === 'wheel') window.__rec.wheel++;
                    if (e.isTrusted !== true) window.__rec.untrusted.push(e.type);
                };
                document.addEventListener('mousemove', note, true);
                window.addEventListener('wheel', note, { capture: true, passive: true });
                return 'ready';
            })()"#,
        )
        .await
        .unwrap()
        .into_value()
        .unwrap();
    assert_eq!(setup, "ready");

    // Run a short but real warm. 2.5s is enough for several meander/scroll
    // actions under the loop's action weights.
    warm_session(&page, Duration::from_millis(2500))
        .await
        .expect("warm_session should succeed on a healthy page");

    let rec = page
        .evaluate("window.__rec")
        .await
        .unwrap()
        .into_value::<serde_json::Value>()
        .unwrap();

    let moves = rec["move"].as_u64().unwrap_or(0);
    let wheels = rec["wheel"].as_u64().unwrap_or(0);
    let untrusted = rec["untrusted"].as_array().cloned().unwrap_or_default();

    browser.close().await.unwrap();

    // The core invariant: NOTHING warm_session dispatched may be untrusted.
    assert!(
        untrusted.is_empty(),
        "warm_session emitted untrusted (isTrusted=false) event(s): {untrusted:?} — \
         a synthetic-JS regression; every pointer/scroll must be trusted BiDi input"
    );
    // And it must actually have moved the pointer (a no-op warm proves nothing).
    assert!(
        moves > 0,
        "warm_session produced no mousemove events (moves={moves}, wheels={wheels}) — \
         the trusted pointer path is not reaching the page"
    );
}

#[tokio::test]
async fn drag_primitives_emit_only_trusted_events() {
    let browser = common::launch_browser().await;
    let page = browser.new_page("about:blank").await.unwrap();

    let setup: String = page
        .evaluate(
            r#"(() => {
                document.body.innerHTML =
                    '<div id="k" style="position:absolute;left:30px;top:30px;width:60px;height:60px;background:#ccc"></div>';
                window.__d = { down: 0, move: 0, up: 0, untrusted: [] };
                const note = (e) => {
                    if (e.type === 'mousedown') window.__d.down++;
                    if (e.type === 'mousemove') window.__d.move++;
                    if (e.type === 'mouseup') window.__d.up++;
                    if (e.isTrusted !== true) window.__d.untrusted.push(e.type);
                };
                for (const t of ['mousedown', 'mousemove', 'mouseup']) {
                    document.addEventListener(t, note, true);
                }
                return 'ready';
            })()"#,
        )
        .await
        .unwrap()
        .into_value()
        .unwrap();
    assert_eq!(setup, "ready");

    // The slider drag's exact mechanism: trusted press → moves → release.
    page.mouse_down(50.0, 50.0).await.unwrap();
    for i in 1..=10 {
        page.move_mouse_to(50.0 + i as f64 * 8.0, 50.0).await.unwrap();
    }
    page.mouse_up(130.0, 50.0).await.unwrap();

    let d = page
        .evaluate("window.__d")
        .await
        .unwrap()
        .into_value::<serde_json::Value>()
        .unwrap();
    browser.close().await.unwrap();

    let untrusted = d["untrusted"].as_array().cloned().unwrap_or_default();
    assert!(
        untrusted.is_empty(),
        "drag primitives emitted untrusted event(s): {untrusted:?} — slider drag must be trusted BiDi input"
    );
    assert!(d["down"].as_u64().unwrap_or(0) >= 1, "no trusted mousedown delivered");
    assert!(d["move"].as_u64().unwrap_or(0) >= 1, "no trusted mousemove delivered");
    assert!(d["up"].as_u64().unwrap_or(0) >= 1, "no trusted mouseup delivered");
}