captchaforge 0.2.39

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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
# captchaforge

> ## ⚠️ DO NOT USE: UNDER ACTIVE DEVELOPMENT
>
> This crate is **not production-ready**: broad live-vendor success across
> real anti-bot sites is **not yet benchmarked or guaranteed**. The
> architecture is in place (vendor solvers, retry-loop iframe walking, VLM
> provider abstraction, real-WAF bench harness) and the two historical
> blockers below have been resolved or disproven, but a measured success
> rate on the full real-site set is still pending.
>
> **The earlier "0%, cross-origin click never delivers / iframe never
> attaches" diagnosis was empirically DISPROVEN.** A deterministic oracle
> (`foxdriver/tests/cross_origin_click.rs`, 3/3 live) proves a trusted
> top-context click routes into a *doubly-nested* cross-origin OOPIF
> (the Turnstile/hCaptcha/reCAPTCHA shape) and arrives `isTrusted = true`,
> and that BiDi `input.performActions` is accepted inside a child-iframe
> context. The real bug was synthetic JS clicks (`el.click()`,
> `isTrusted = false`) at scored controls, every solver path now
> locates-in-JS and clicks **trusted** in Rust.
>
> | Real-WAF fixture | Status |
> |---|---|
> | Cloudflare Turnstile, managed (CF demo) | ✅ auto-passes with reynard (token obtained, no click needed) |
> | Cross-origin trusted click delivery (nested OOPIF) | ✅ proven (`cross_origin_click`: trusted positives + an `isTrusted=false` negative) |
> | Cloudflare Turnstile, always-block (`2x…AB`) | ✅ INTEGRITY, refuses to fake-pass |
> | Cloudflare Turnstile, force-interactive (`3x…FF`) | ⚪ DEAD ORACLE, differentially proven that *no* Firefox (stock or reynard) builds the challenge iframe for this test key on a self-hosted page; there is nothing to click |
>
> The deeper wall was always **fingerprint** (TLS JA3/JA4 + automation
> coherence), not the click. That is now substantially addressed: the
> reynard stealth engine (`navigator.webdriver = false` natively, FF-150-exact
> TLS) is the **default** launch engine, and a persona coherence gate is
> enforced JS-to-TCP at every launch. Broad live-vendor proof across real
> sites is the remaining open work.
>
> **Do not depend on this for any real workload.** The repo is
> private. Reach out to the Santh team if you have a use case that
> justifies early collaboration.

[![ci](https://github.com/santhreal/captchaforge/actions/workflows/ci.yml/badge.svg)](https://github.com/santhreal/captchaforge/actions/workflows/ci.yml)
[![license](https://img.shields.io/badge/license-MIT%20OR%20Apache--2.0-blue.svg)](#license)

Automatic CAPTCHA detection and multi-strategy solving for Firefox/BiDi
live pages (`runtime_foxdriver::Page`, re-exported as
`captchaforge::Page`). Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image
grids, audio challenges, sliders, plus a community-extensible TOML rule
pack for vendors like AWS WAF Captcha, DataDome, Akamai Bot Manager,
PerimeterX/HUMAN, Arkose/FunCaptcha, Geetest v3+v4, Friendly Captcha,
MTCaptcha, and WordPress math captchas.

> Part of the [Santh]https://santh.dev security research ecosystem.
> **Beta**, used in production by [wafrift]https://github.com/santhreal/wafrift
> and [golemn-browser]https://github.com/santhreal/golemn but the
> public API may shift before 1.0. Pin the version in `Cargo.toml`.

## Quick start

Add to `Cargo.toml`:

```toml
[dependencies]
captchaforge = { git = "https://github.com/santhreal/captchaforge", branch = "main" }
tokio = { version = "1", features = ["full"] }
```

### CLI

A standalone binary ships with the workspace:

```sh
cargo install --path cli                       # builds `captchaforge`
captchaforge doctor                            # probe Ollama / Whisper / Tesseract
captchaforge setup --auto                      # auto-pull default vision model
captchaforge selftest                          # confirm the 11-solver chain wires up
captchaforge transcribe sample.wav             # smoke-test the STT backend
captchaforge solve https://example.com         # solve a captcha on a real URL
captchaforge serve --port 8080                 # HTTP API for non-Rust embeds
```

The `serve` mode exposes:
- `GET  /doctor`: capabilities JSON + install hints (works headless)
- `POST /solve`: `{"url": "https://...", "headless": true}` → solve result
- `POST /transcribe`: `{"audio_b64": "..."}``{"raw","normalized"}`

Use `serve` to embed solving in any language that speaks HTTP.
Meridian should not use the standalone `solve`/`serve` browser path for
auth-preserving hunting; it calls CaptchaForge inside the existing Guise
Firefox/BiDi page through `guise-bridge`.

### Library

The shortest path from "fresh page" to "solved captcha" is one call 
[`captchaforge::solve_url`] wires stealth + named profile before
navigation, warms the loaded page, then runs `auto_solve`:

```rust
use captchaforge::prelude::*;

async fn open(page: &captchaforge::Page) -> anyhow::Result<()> {
    match solve_url(page, "https://example.com",
                    Some(StealthProfile::FirefoxLinux)).await? {
        None                          => println!("no captcha"),
        Some(r) if r.success          => println!("solved via {:?} ({}ms)", r.method, r.time_ms),
        Some(r)                       => println!("failed: {:?}", r.method),
    }
    Ok(())
}
```

If you already navigated and just want the detect-then-solve loop on
the current page, use [`captchaforge::auto_solve`]:

```rust
async fn handle_captcha(page: &captchaforge::Page) -> anyhow::Result<()> {
    // Returns Ok(None) for no-captcha pages.
    if let Some(outcome) = captchaforge::auto_solve(page).await? {
        if outcome.success {
            tracing::info!(method = ?outcome.method, "captcha solved");
        } else if let Some(screenshot) = outcome.screenshot {
            tracing::warn!(bytes = screenshot.len(), "human review needed");
        }
    }
    Ok(())
}
```

For high-stakes flows (checkout, account creation, bot-blocked
public APIs) wrap the call in
[`captchaforge::auto_solve_with_retries`], it dismisses
cookie-consent banners between attempts and retries on transient
failure. Pair with [`captchaforge::dismiss_chat_widget`] when an
Intercom / Drift / HubSpot launcher overlaps the captcha widget.

For full control, telemetry, token cache, custom solvers, drop down
to the explicit chain:

```rust
use std::sync::Arc;
use captchaforge::{detect, solver::{CaptchaSolverChain, TokenCache}};

let cache = Arc::new(TokenCache::new());     // 60s TTL by default
let chain = CaptchaSolverChain::default_chain()
    .with_token_cache(cache);                 // hit cache before solving

let info = detect::detect(page).await?;
if detect::is_captcha(&info) {
    let outcome = chain.solve(page, &info).await;
    // ...
}
```

## Capability map

Each capability has its own module + at least one production
wiring point. Re-export paths are at the crate root; the internal
home is one of `captchaforge::*`.

| Capability                                  | Module                          | Wired into                                 |
| ------------------------------------------- | ------------------------------- | ------------------------------------------ |
| One-call solve / prepare / dismiss helpers  | `sdk`                           | `captchaforge::{solve_url, …}` re-exports  |
| 158-vendor TOML rule pack                   | `detect::rules`                 | `ProviderRegistry::with_built_in_rules`    |
| 15 named browser stealth profiles           | `stealth_profiles`              | `prepare_page(_, Some(profile))`           |
| Real-human mouse-trace sampler (8-trace corpus + jitter) | `mouse_sampler`     | opt-in; `behavior::mouse_move_human` ships the legacy path |
| Bezier mouse / hover-dwell / touch-swipe / pointer-jitter / triple-click | `behavior` | bench-suite + production solvers           |
| Audio captcha DSP (denoise + bandpass + normalise) | `audio_dsp`              | `AudioCaptchaSolver::transcribe`           |
| DOM/frame graph (BFS / deepest-captcha / ancestors) | `frame_graph`           | opt-in for solver authors                  |
| Multi-hop captcha planner                   | `solver::planner`               | opt-in via `MultiHopPlanner`               |
| Read-only artifact pre-flight handle        | `solver::speculation`           | opt-in via `PreflightHandle`               |
| Per-vendor token shape oracle               | `solver::token_shapes`          | chain post-solve check (auto)              |
| Adversarial training corpus                 | `training_corpus`               | `chain.with_training_corpus(...)`          |
| VLM training dataset (content-addressed)    | `vlm_dataset`                   | training pipeline (`training/`)            |
| Mouse-trace ingest store                    | `trace_ingest`                  | server side of `B1`                        |
| Vendor JS scraper + probe-surface diff      | `vendor_scraper`                | `captchaforge scrape-vendors` CLI          |
| TOML rule hot-reload (mtime-poll, atomic swap) | `rule_watcher`               | opt-in via `HotReloadRegistry::start`      |
| Patched-Chromium sidecar detector           | `chromium_sidecar`              | opt-in legacy diagnostic via `PatchedChromiumDetector`       |
| Per-solver telemetry + Prometheus exposition | `telemetry`                    | `chain.with_telemetry(...)` + `serve /metrics` |
| Network-layer WAF challenge replay (no browser) | crate `captchaforge-challenge` | direct dep                            |
| Pure-data rule schema (no browser dep)     | crate `captchaforge-rules`      | direct dep for static-analysis tools       |

## Architecture surface (post-extraction)

- **`detect::Detector`**, single per-vendor detector trait. Eleven
  built-ins (`TurnstileDetector`, `RecaptchaDetector`, …) live one per
  file under `src/detect/`. Add a new detector by writing
  `src/detect/<vendor>.rs` and registering it.
- **`detect::rules`**, community-extensible TOML rule layer. Drop a
  `[[provider]]` entry in `rules/community.toml` (or your own file)
  and the vendor is detected without writing any Rust. Bundled pack
  covers DataDome, Akamai, PerimeterX/HUMAN, Arkose/FunCaptcha,
  Geetest v3+v4, AWS WAF Captcha, Friendly Captcha, MTCaptcha,
  WordPress math captchas.
- **`provider::CaptchaProvider`**, bundles a detector + the
  recommended solver methods for that vendor in one trait. Built-in
  providers wired via macro; new providers implement both traits in
  one file.
- **`provider::ProviderRegistry::with_built_in_rules()`**, single
  call returning the built-in detectors PLUS the bundled community
  rule pack, all priority-sorted.
- **`solver::CaptchaSolverChain`**, multi-strategy chain with cache
  + telemetry + pattern-store routing. See "Strategies" below.
- **`solver::TokenCache`**, per-`(domain, captcha_type)` solved
  token cache with configurable TTL. Cache hits skip the solver run
  entirely and report through telemetry as `solver: "TokenCache"`.
- **`telemetry::SolverTelemetry`**, implement to capture every
  solver attempt as a `SolveEvent`. Default is `NoopTelemetry`.
  Production deployments plug in Prometheus / OTel / SQLite / Sentry.
- **`solver::PatternStore`** (per-domain learned routing).
  `save_to_path` / `load_from_path` / `merge` for cross-restart and
  cross-instance sharing.
- **`captchaforge::auto_solve()`**, top-level one-call API for the
  90% case. Auto-detects local backends (Ollama VLM / Whisper / Tesseract)
  on first call and logs the capability summary so operators see what's
  wired without reading the source.
- **`captchaforge::backends::probe()`**, non-blocking parallel probe
  of Ollama (`/api/tags`), `whisper` / `whisper-cli` on PATH,
  `OPENAI_API_KEY` env, `tesseract` on PATH. Returns a `Capabilities`
  struct with `install_hints()` for any missing backend so consumers
  can tell their users exactly what to install.
- **`captchaforge::backends::Capabilities::ensure_vlm_model()`** 
  spawns `ollama pull llama3.2-vision:11b` when Ollama is reachable
  but no vision model is present. Auto-install for the SDK consumer.

## Strategies

For each detected captcha the chain runs through a layered short
circuit + strategy stack. First match wins.

0. **Token cache**: if a `TokenCache` is installed and holds a
   fresh `(domain, captcha_type)` token (default 60s TTL), return it
   immediately tagged as `CrowdSourced`. Telemetry sees this as a
   `Success` with `solver: "TokenCache"`. Skip the rest of the chain.
1. **Provider routing**: for `DetectedCaptcha::Custom` (TOML rule
   layer), only solvers whose `SolveMethod` appears in the bundled
   provider's `recommended_solver_methods` are eligible, in declared
   order. Built-in detectors fall back to the standard `supports()`
   filter.
2. **Pattern store ordering**: for built-in kinds, solvers eligible
   under (1) get re-ordered so the historically-best method for this
   domain runs first. PatternStore can be persisted across restarts
   (`save_to_path` / `load_from_path`) and merged across worker
   instances (`merge`).
3. **Behavioural**: realistic mouse movement + timing for Turnstile
   and reCAPTCHA v3. Solvers verify the response token is actually
   present before reporting `success`. On success, captures session
   cookies into `CaptchaSolveResult::cookies` for replay.
4. **Vision-LLM**: screenshot the challenge, hand it to a multimodal
   model ([Ollama]https://ollama.com `qwen3-vl:30b` out of the box,
   configurable), parse the model's grid / click answer, simulate the
   mouse. Captures session cookies on success.
5. **Audio bypass**: click the accessibility audio challenge, run
   STT, type the transcribed answer, then verify the reCAPTCHA token
   was accepted. Captures session cookies on success.
6. **Telemetry**: every attempt at every layer fires a `SolveEvent`
   to the installed `SolverTelemetry` impl (default `NoopTelemetry`).
   Outcomes: `Success`, `Failure`, `Error`, `Timeout`. Cache hits
   surface as `Success` with `solver: "TokenCache"` so dashboards
   distinguish solved-from-scratch from cache-served.
7. **Human fallback**: when every solver fails, return unsolved
   with a base64 JPEG screenshot in `CaptchaSolveResult::screenshot`
   so a downstream human-in-the-loop can finish.

Apply captured cookies to subsequent navigations via
`captchaforge::cookies::apply_to_page(&page, &result.cookies)` so the
WAF/vendor's trusted session is preserved and the next request does
not re-trigger the captcha.

You can plug in your own solver by implementing the
[`CaptchaSolver`](src/solver.rs) trait and adding it to the chain via
`chain.add_solver(...)` before `default_chain()` strategies.

## Detection coverage

Built-in detectors (one per file under `src/detect/`):

- Cloudflare Turnstile (`.cf-turnstile`, `[data-turnstile-sitekey]`,
  `script[src*="challenges.cloudflare.com"]`)
- Cloudflare bot interstitial ("Just a moment", `#challenge-form`)
- reCAPTCHA v2 + v3 (`.g-recaptcha`, `[data-sitekey]`,
  `script[src*="google.com/recaptcha"]`)
- hCaptcha (`.h-captcha`, `[data-hcaptcha-sitekey]`,
  `script[src*="hcaptcha.com"]`)
- Generic image / audio CAPTCHAs
- PoW CAPTCHAs (ALTCHA, mCaptcha, Friendly Captcha, Cap.dev)
- Slider / swipe / rotate / drag CAPTCHAs
- Canvas / SVG / math / icon / color CAPTCHAs
- Multi-step / wizard CAPTCHAs
- Shadow-DOM-hidden CAPTCHAs

Bundled community rule pack (`rules/community.toml`, registered
via `ProviderRegistry::with_built_in_rules()`):

- AWS WAF Captcha
- DataDome
- Akamai Bot Manager
- PerimeterX / HUMAN
- Arkose Labs / FunCaptcha
- Geetest v3 + v4
- Friendly Captcha
- MTCaptcha
- WordPress math captchas (Captcha by BestWebSoft, generic forms)

Adding a new family is two lines of TOML, see `rules/community.toml`
for the schema. Per-rule positive + negative HTML fixtures live under
`tests/rule_fixtures/<vendor>/`; the `community_rules` integration
test asserts every rule fires on its positive and rejects its
negative AND doesn't cross-fire on any other vendor's negative.

## Configuration

Two layers, lightest to heaviest:

**Env vars (zero-config)**

- `CAPTCHAFORGE_VLM_ENDPOINT`: Ollama endpoint URL.
- `CAPTCHAFORGE_VLM_MODEL`: VLM model name.
- `CAPTCHAFORGE_THIRDPARTY_API_KEY`: 2captcha-compatible service key.
- `CAPTCHAFORGE_THIRDPARTY_ENDPOINT`: override the service base URL.
- `CAPTCHAFORGE_CONFIG`: explicit Tier-A config file path.

**Tier-A operational config (`.captchaforge.toml`)**

Drop `.captchaforge.toml` in your project (or use the discovery search
path: `$CAPTCHAFORGE_CONFIG`, `./.captchaforge.toml`,
`./captchaforge.toml`, `$XDG_CONFIG_HOME/captchaforge/config.toml`).
The schema covers chain timeouts, cache TTL, VLM endpoint/model,
third-party service selection, and every `SolveConfig` field. See
`.captchaforge.toml.example` for the reference. Unknown keys are a
parse error so typos surface instead of silently being ignored.

```toml
per_solver_timeout_ms = 60000

[cache]
ttl_seconds = 90

[vlm]
endpoint = "http://gpu-host:11434"

[third_party]
service = "cap_solver"
api_key = "..."
```

**Builder methods (fine-grained)**

For fine-grained control inside one process, use the builder methods 
they always beat env + TOML.

```rust
use captchaforge::solver::{VlmCaptchaSolver, CaptchaSolverChain};

let mut chain = CaptchaSolverChain::empty();
chain.add_solver(
    VlmCaptchaSolver::new()
        .with_endpoint("http://my-gpu-host:11434")
        .with_model("qwen3-vl:30b"),
);
```

Audio STT endpoint is similarly configurable on `AudioCaptchaSolver`.

**Third-party solver service**

`ThirdPartyCaptchaSolver` talks the 2captcha-compatible HTTP protocol
used by 2captcha, CapMonster, and CapSolver, every TOML-bundled vendor
that recommends `ThirdPartyService` now actually has a solver to land
on. Without an API key configured, `supports()` returns false across
the board so the solver is skipped instead of issuing 401s.

```rust
use captchaforge::solver::{ThirdPartyCaptchaSolver, CaptchaSolverChain};

let mut chain = CaptchaSolverChain::empty();
chain.add_solver(
    // Pick the service flavour, then pass the key:
    ThirdPartyCaptchaSolver::cap_solver().with_api_key("..."),
);
```

## CLI

```sh
cargo install --path cli
captchaforge inspect-rules                         # list bundled + extra rules
captchaforge detect https://example.com/login      # navigate, run detector
captchaforge solve  https://example.com/login      # navigate, run full chain
captchaforge inspect-rules --rules my-pack.toml    # additive extra rules
captchaforge rules                                 # 158-vendor coverage table
captchaforge profiles                              # 15 named StealthProfiles
captchaforge corpus stats /var/lib/cf/corpus       # adversarial-training samples
captchaforge dataset stats /var/lib/cf/datasets    # VlmDataset shape
captchaforge scrape-vendors --out report.txt       # diff vendor JS probe surface
captchaforge validate-rules                        # static-check every TOML rule
captchaforge serve --port 8080                     # HTTP API (incl. /metrics)
```

## Telemetry

Two batteries-included impls plus the trait for custom sinks.

```rust
use std::sync::Arc;
use captchaforge::solver::CaptchaSolverChain;
use captchaforge::telemetry::{JsonTelemetry, MetricsTelemetry};

// Option 1: route every event through `tracing::info!` with structured
// fields. Pairs with `tracing-subscriber`'s JSON layer for ELK / Loki
// / Datadog without any extra integration code.
let chain = CaptchaSolverChain::default_chain()
    .with_telemetry(Arc::new(JsonTelemetry));

// Option 2: in-process Prometheus-shaped aggregator. `serve` wires
// this into `/metrics` and `/metrics.json` automatically.
let metrics = Arc::new(MetricsTelemetry::new());
let chain = CaptchaSolverChain::default_chain()
    .with_telemetry(metrics.clone());
println!("{}", metrics.snapshot().to_prometheus());
```

The chain fires one event per solver attempt at every outcome
(`Success` / `Failure` / `Error` / `Timeout`). Cache hits also fire as
`Success` with `solver: "TokenCache"` so dashboards distinguish
solved-from-scratch from cache-served.

## Adversarial training loop

Every failed solve auto-appends to a `TrainingCorpus` when one is
configured. After ~1 week of production traffic you have a labelled-
by-failure dataset ready to feed back into a fine-tune.

```rust
use std::sync::Arc;
use captchaforge::solver::CaptchaSolverChain;
use captchaforge::training_corpus::TrainingCorpus;

let corpus = Arc::new(TrainingCorpus::open("/var/lib/captchaforge/corpus")?);
let chain = CaptchaSolverChain::default_chain()
    .with_training_corpus(corpus.clone());
```

The Python compute side lives in [`training/`](./training/README.md):
Dockerfile + `train_lora.py` (LoRA fine-tune of Qwen2-VL-7B on a
captchaforge VlmDataset), `distill.py` (7B teacher → 2B student
distillation), `auto_label.py` (GPT-4V / Claude vision teacher
labelling), `bench_compare.py` (CI gate for model promotion based on
per-vendor regression bound), `promote_to_ollama.sh` (HF → GGUF →
Ollama registration).

## Hot-reload TOML rules

Operators drop a new vendor rule into the watched dir; the registry
atomically swaps in <2s without a process restart.

```rust
use std::time::Duration;
use captchaforge::rule_watcher::HotReloadRegistry;

let watcher = HotReloadRegistry::start("/etc/captchaforge/rules.d", Duration::from_secs(2))?;
let registry = watcher.current(); // Arc<ProviderRegistry>, atomically swapped on TOML change
```

Bad TOML keeps the previous registry active and logs at warn
(no restart spam). The `validate-rules` CLI subcommand pre-flights
every rule before promotion.

## Network-layer challenge replay

For WAF challenges that are JS-only (no visual captcha), the
`captchaforge-challenge` workspace crate solves them in pure HTTP 
typically 50-150ms instead of the 8-12s a browser round-trip
takes, no GPU, no browser binary. Cloudflare interstitial reference
impl ships today; DataDome / Akamai / Imperva / Kasada slot into the
same `ChallengeSolver` trait as their JS is reverse-engineered.

## Origin and licensing

Extracted from [golemn-browser](https://github.com/santhreal/golemn)
(originally GPL-3.0). The original author (Santh Project) re-licensed
the extracted slice as MIT OR Apache-2.0 so it can be embedded in
MIT/Apache-licensed downstream tools (notably
[wafrift](https://github.com/santhreal/wafrift) for managed-WAF
challenge handling and any future Santh tool that needs a captcha
layer).

## Testing

```sh
cargo test --workspace        # unit + integration (no chromium required)
cargo test --doc              # doctest in lib.rs
cargo test --test community_rules    # asserts every TOML rule fires on its
                              # positive HTML fixture, rejects its negative,
                              # and doesn't cross-fire on other vendors' negatives
cargo test --test integration_chain_wires  # cache + telemetry + chain wires
cargo test --test wirings    # production wirings: H-series modules
                             # actually reachable from the chain (oracle,
                             # corpus, hot-reload, telemetry, planner)
cargo test --test property_token_shapes   # ~10k randomised cases per
                             # token-oracle invariant (decoy never
                             # Plausible, classify deterministic, etc.)
```

### Offline module micro-bench

```sh
cd bench
cargo run --release -- --suite modules
```

In-process measurements (no browser) of the H-series hot paths:
- token-oracle classify (1k iters)
- token-oracle resolve (1k)
- frame-graph construction (1k)
- mouse-sampler trace synthesis (1k)
- training-corpus append (100)

Total runtime <1s. Output uses the same regression-delta tooling as
the live-browser suites, so per-module slowdowns surface in the
`bench-baseline.json` diff. CI runs this on every PR via the
`--suite quick` aggregate.

## Local proof (benches)

```sh
cargo run --example architecture_proof --release
```

Prints a markdown report covering the latency-critical paths added by
the architecture sweep:
- TokenCache get hit / miss
- ProviderRegistry::find_by_kind for Custom kinds
- RuleDetector probe generation (one-shot at registration)
- Per-rule HTML fixture match latency (one row per bundled vendor)

Browser-free, runs in well under a second after the release build.
Use to sanity-check that no future change regresses the hot path.

The test suite does **not** require a real browser binary or a real
Ollama server, browser-bound paths are pure Rust unit tests
against the JavaScript / Rust glue layer. Tests that exercise the
real headless browser live in
[`wafrift-captchaforge-bridge`](https://github.com/santhreal/wafrift/tree/main/crates/captchaforge-bridge)
where a live browser runtime is available.

## License

MIT OR Apache-2.0 at your option. Copyright 2026 CORUM COLLECTIVE LLC.


## Scorecard badges

These measure **structural** coverage, that the solver chain, the community
rule set, the challenge-fingerprint catalogue, and the test/property suites are
complete and wired. They are **not** a live-vendor success rate: a measured
pass rate against real anti-bot sites is still open work (see the banner at the
top). A 100% here means "every stage/fixture we model is exercised", not "we
beat every real CAPTCHA".

<!-- START-SCORECARD-BADGES -->
![real_waf_solver_chain_completeness](https://img.shields.io/badge/real_waf_solver_chain_completeness-100.0%25-brightgreen)
![real_waf_community_rule_coverage](https://img.shields.io/badge/real_waf_community_rule_coverage-100.0%25-brightgreen)
![real_waf_bench_fixture_coverage](https://img.shields.io/badge/real_waf_bench_fixture_coverage-89.7%25-green)
![real_waf_challenge_fingerprint_coverage](https://img.shields.io/badge/real_waf_challenge_fingerprint_coverage-100.0%25-brightgreen)
![real_waf_test_suite_coverage](https://img.shields.io/badge/real_waf_test_suite_coverage-100.0%25-brightgreen)
![real_waf_property_test_coverage](https://img.shields.io/badge/real_waf_property_test_coverage-80.0%25-green)
<!-- END-SCORECARD-BADGES -->