#![forbid(unsafe_code)]
pub mod adversarial_replay;
pub mod audio_dsp;
pub mod backends;
pub use guise::human::behavior;
pub mod browser;
pub mod browser_runtime;
pub mod config;
pub mod cookies;
pub mod detect;
pub mod fingerprint_lru;
pub mod frame;
pub mod frame_graph;
pub(crate) mod http_client;
pub mod keystroke_timing;
pub mod mobile_screenshot;
pub mod mobile_webview;
pub mod plugin;
pub mod prelude;
pub mod provider;
pub mod rule_watcher;
pub mod sdk;
pub mod solver;
pub mod stealth_profiles;
pub mod stt;
pub mod telemetry;
pub mod trace_ingest;
pub mod training_corpus;
pub mod vendor_scraper;
pub mod vision;
pub mod vlm_dataset;
pub mod waf_gate;
pub mod warmup;
pub use config::Config;
pub use cookies::CapturedCookie;
pub use guise as stealth;
pub use guise::{ProfileBundle, StealthProfile};
pub use runtime_foxdriver::Page;
pub use stealth_profiles::{
apply_default_stealth_profile, apply_stealth, apply_stealth_profile, profile_js,
profile_to_overrides, ProfileOverrides,
};
pub use detect as captcha_detect;
pub use provider::{CaptchaProvider, ProviderRegistry};
pub use backends::{Capabilities, OcrBackend, SttBackend, SttKind, VlmBackend};
pub use solver::{
AudioCaptchaSolver, BehavioralCaptchaSolver, CaptchaSolveResult, CaptchaSolver,
CaptchaSolverChain, CaptchaType, OcrCaptchaSolver, SolveMethod, TokenCache, VlmCaptchaSolver,
};
pub use sdk::{
auto_solve_with_retries, dismiss_chat_widget, dismiss_cookie_consent, prepare_page, solve_url,
wait_for_no_captcha,
};
pub async fn auto_solve(page: &crate::Page) -> anyhow::Result<Option<solver::CaptchaSolveResult>> {
{
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(());
}
}
if let Err(e) = crate::apply_stealth(page).await {
tracing::warn!(
error = %e,
"captchaforge: apply_stealth failed; SOLVING ANYWAY on a page that may \
still expose automation tells (webdriver/CDP), detection recall is degraded"
);
}
let info = detect::detect(page).await?;
if !detect::is_captcha(&info) {
return Ok(None);
}
let cfg = config::Config::discover()?;
let chain = cfg.build_chain()?;
Ok(Some(chain.solve(page, &info).await))
}
#[cfg(test)]
mod law10_audit {
#[test]
fn no_continuing_failure_is_swallowed_at_debug_level() {
let sources = [
("src/lib.rs", include_str!("lib.rs")),
("src/warmup.rs", include_str!("warmup.rs")),
("src/solver/chain.rs", include_str!("solver/chain.rs")),
(
"src/solver/behavioral.rs",
include_str!("solver/behavioral.rs"),
),
];
let degraded_markers = ["(continuing)", "fallback"];
for (name, src) in sources {
for (idx, line) in src.lines().enumerate() {
let trimmed = line.trim_start();
let is_debug_macro =
trimmed.starts_with("tracing::debug!") || trimmed.starts_with("debug!(");
if !is_debug_macro {
continue;
}
let lowered = line.to_ascii_lowercase();
if let Some(marker) = degraded_markers
.iter()
.find(|m| lowered.contains(&m.to_ascii_lowercase()))
{
panic!(
"{name}:{}: a degraded/continuing path (marker {marker:?}) is announced \
at tracing::debug! (Law 10, a silent fallback; debug is off by default). \
Use tracing::warn! so the operator sees the degraded solve: {}",
idx + 1,
trimmed
);
}
}
}
}
#[test]
fn no_solve_path_action_is_bound_and_discarded() {
use std::fs;
use std::path::{Path, PathBuf};
fn rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() {
rs_files(&p, out);
} else if p.extension().and_then(|e| e.to_str()) == Some("rs")
&& p.file_name().and_then(|n| n.to_str()) != Some("tests.rs")
{
out.push(p);
}
}
}
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let mut files = Vec::new();
rs_files(&root.join("src/solver"), &mut files);
assert!(
files.len() >= 10,
"solver-tree walk found only {} files, mis-rooted",
files.len()
);
let banned = concat!("let _", " = ");
for f in &files {
let Ok(src) = fs::read_to_string(f) else {
continue;
};
for (idx, line) in src.lines().enumerate() {
let trimmed = line.trim_start();
if trimmed.starts_with("//") {
continue;
}
if trimmed.contains(banned) && trimmed.contains(".await") {
panic!(
"{}:{}: a solve-path action is bound-and-discarded (`let _ = …await`). Law 10. \
A swallowed click/move/evaluate either wastes the solve silently or (vlm/math) \
fabricates `success: true`. Surface it (`if let Err(e) = … {{ warn!(…) }}`) or \
gate the result on the action's outcome, never bind-and-discard. Line: {}",
f.display(),
idx + 1,
trimmed
);
}
}
}
}
}
#[cfg(test)]
mod no_second_browser_audit {
use std::fs;
use std::path::{Path, PathBuf};
fn rs_files(dir: &Path, out: &mut Vec<PathBuf>) {
let Ok(entries) = fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let p = entry.path();
if p.is_dir() {
rs_files(&p, out);
} else if p.extension().and_then(|e| e.to_str()) == Some("rs")
&& p.file_name().and_then(|n| n.to_str()) != Some("browser_runtime.rs")
{
out.push(p);
}
}
}
fn is_full_line_comment(line: &str) -> bool {
let t = line.trim_start();
t.starts_with("//")
}
#[test]
fn solve_path_never_launches_a_browser() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let mut files = Vec::new();
rs_files(&root.join("src/solver"), &mut files);
for extra in ["src/lib.rs", "src/sdk.rs", "src/warmup.rs", "src/detect.rs"] {
files.push(root.join(extra));
}
let launch_tokens = [
concat!("launch_", "firefox"),
concat!("launch_", "profiled_firefox"),
concat!("connect_over", "_cdp"),
];
for f in &files {
let Ok(src) = fs::read_to_string(f) else {
continue;
};
for (idx, line) in src.lines().enumerate() {
if is_full_line_comment(line) {
continue;
}
for tok in launch_tokens {
assert!(
!line.contains(tok),
"{}:{}: a solve-path file references `{tok}`: the README invariant is that \
captchaforge solves on an already-open reynard page and NEVER launches a \
browser in the solve path (only browser_runtime.rs may launch). Line: {}",
f.display(),
idx + 1,
line.trim()
);
}
}
}
}
#[test]
fn no_chrome_driver_token_anywhere_firefox_only_tool() {
let root = Path::new(env!("CARGO_MANIFEST_DIR"));
let mut files = Vec::new();
rs_files(&root.join("src"), &mut files);
let chrome_driver_tokens = [
concat!("chromium", "oxide"),
concat!("chrome", "driver"),
concat!("Chrome", "Builder"),
concat!("BrowserType", "::Chrom"),
];
for f in &files {
let Ok(src) = fs::read_to_string(f) else {
continue;
};
for (idx, line) in src.lines().enumerate() {
if is_full_line_comment(line) {
continue;
}
for tok in chrome_driver_tokens {
assert!(
!line.contains(tok),
"{}:{}: Chrome-driver token `{tok}` in a Firefox/reynard-only tool, a \
coherence break with the 'never launches Chrome' invariant. Line: {}",
f.display(),
idx + 1,
line.trim()
);
}
}
}
}
}