captchaforge 0.2.17

Automatic CAPTCHA detection and multi-strategy solving for chromiumoxide-driven headless browsers (Cloudflare Turnstile, reCAPTCHA v2/v3, hCaptcha, image grids, audio, sliders).
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
# Changelog

All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## [Unreleased]

## [0.2.17] - 2026-05-11

### AwsWafSolver — AWS WAF Captcha cookie-pass + iv/context capture (2026-05-11, round 19)

AWS WAF's CAPTCHA action protects a substantial slice of AWS-hosted SaaS (Shopify, Lyft, Yelp, etc. via the AWS WAF JS Integration SDK). Detection landed in the 0.2.0 community rule pack but no dedicated solver existed; sites fell through to the generic slider, which doesn't see the cookie-pass case (the most common one with proper stealth) and which couldn't surface the `iv` + `context` primitives that custom verification flows need.

#### Added
- **`solver::aws_waf::AwsWafSolver`.** Polls page for AWS WAF state via JS probe — distinguishes `passed` (cookie set, classifier validates), `passed_with_iv` (SDK has staged `iv` + `context` but cookie hasn't landed yet — surfaces them as JSON solution payload for callers driving custom verification), `puzzle` (visible challenge iframe — yields to slider/VLM via screenshot+failure), `interstitial` (SDK loaded, no cookie yet — keep waiting), `unknown`.
- **`solver::aws_waf::is_valid_aws_waf_token`.** Pure validator. Enforces AWS's documented `<dotted>.<seg>:<expiry>:<sig>` shape: ≥40 chars total, ≥4 dot-separated segments before the first colon (no empty segment), numeric expiry in [2020, 2100), non-empty signature tail. Permissive on segment-byte encoding since AWS rotates between base64 and base64url; firm on the structural skeleton because that's held since GA in 2022. Doctest covers the structural cases.
- **Default chain now ships 15 solvers** (was 14). `AwsWafSolver` slots between `GeeTestSolver` and `MathCaptchaSolver`.

#### Changed
- 343 → 357 lib tests (+14). Clippy clean across `--all-targets`.

## [0.2.16] - 2026-05-11

### GeeTestSolver — v3 + v4 token-watch (2026-05-11, round 18)

GeeTest is the dominant CN bot-management captcha vendor (~3% top-1M globally, far higher inside China). Two on-the-wire protocols coexist (v3 = `gt+challenge` triple, v4 = `captchaId` quad), and a single solver that doesn't distinguish them either misses passes or false-positives. This release ships a version-aware token-watch that returns success on the right shape per protocol.

#### Added
- **`solver::geetest::GeeTestSolver`.** Polls page for v3 success triple (`geetest_challenge` / `geetest_validate` / `geetest_seccode`) OR v4 success quad (`lot_number` / `pass_token` / `gen_time` / `captcha_output`). Reads from hidden inputs OR window globals OR documented callback payloads. Returns success with the full token bundle as JSON in the `solution` field so downstream verification can use it directly. Yields cleanly to slider/VLM for the actual interaction (we don't reinvent that).
- **`solver::geetest::is_valid_v3_triple`** + **`is_valid_v4_quad`.** Pure validators. v3: each field non-empty AND seccode contains pipe OR ≥32 hex chars. v4: each field non-empty AND `gen_time` parses as a unix epoch in 2020–2100. Doctests demonstrate both.
- **`solver::geetest::GeeTestProtocol { V3, V4 }`** public enum for callers that want to introspect which protocol the page is using.
- **Default chain now ships 14 solvers** (was 13). `GeeTestSolver` slots between `PerimeterXSolver` and `MathCaptchaSolver`.

#### Changed
- 329 → 343 lib tests (+14). Clippy clean across `--all-targets`.

## [0.2.15] - 2026-05-11

### PerimeterXSolver — dedicated PX / HUMAN Security handler (2026-05-11, round 17)

PerimeterX (rebranded HUMAN Bot Defender, 2022) protects ~5% of top-1M sites. Detection landed in the 0.2.0 community rule pack, but every PX-protected site fell through to the generic slider — wrong shape for PX's distinctive press-and-hold widget and cookie-pass interstitial. This release ships dedicated handling for the cookie-pass case and clean yield-to-VLM signaling for press-and-hold.

#### Added
- **`solver::perimeterx::PerimeterXSolver`.** Polls page for PX state via JS probe — distinguishes `passed` (cookie set, classifier validates), `press_and_hold` (yields to VLM via screenshot+failure when `#px-captcha` widget present — we don't ship dedicated press-and-hold geometry yet), `interstitial` (script loaded, no cookie yet — keep waiting), `blocked` (PX block page with reference code), `unknown`. Returns success when `_px3` (v3) or `_pxhd` (legacy device-history) cookie carries a real-shape session token.
- **`solver::perimeterx::classify_px_cookie`** + **`PxCookie { Missing, Pending, Valid }`.** Pure classifier — Missing (no cookie), Pending (<30 chars or no separator), Valid (≥30 chars and contains `:` or `_` separator). Permissive on inner-segment shape since PX has rotated payload format twice in three years; firm on length floor since that's held across all observed revisions. Doctest covers all three cases.
- **Default chain now ships 13 solvers** (was 12). `PerimeterXSolver` slots between `DataDomeSolver` and `MathCaptchaSolver`.

#### Changed
- 317 → 329 lib tests (+12). Clippy clean across `--all-targets`.

## [0.2.14] - 2026-05-11

### DataDomeSolver — dedicated DataDome cookie-pass + interstitial handler (2026-05-11, round 16)

DataDome is the largest pure-play bot-management vendor outside Cloudflare and Akamai (~7% of top-1M sites per BuiltWith Q1 2026). Detection landed in 0.2.0's community rule pack, but every DataDome-protected site fell through to the generic slider solver — the wrong shape when DataDome's protection is the cookie-pass / interstitial JS challenge (the slider only appears as the third-tier escalation). This release ships dedicated handling for the cookie-pass and interstitial cases.

#### Added
- **`solver::datadome::DataDomeSolver`.** Polls page for DataDome state via JS probe — distinguishes `passed` (cookie set, classifier validates), `slider` (yields to `SliderCaptchaSolver` via screenshot+failure when `captcha-delivery.com` iframe present), `interstitial` (script loaded, no cookie yet — keep waiting), `blocked` (block page), `unknown`. Returns success when the `datadome` cookie carries the long-form base64-ish session token.
- **`solver::datadome::classify_cookie`** + **`DataDomeCookie { Missing, Pending, Valid }`.** Pure classifier — Missing (no cookie), Pending (placeholder / sensor not yet POSTed — short or >50% tildes), Valid (≥80 chars, ≤50% tildes). Public so callers can replay-and-classify outside the chain. Doctest covers all three cases.
- **Default chain now ships 12 solvers** (was 11). `DataDomeSolver` slots between `AkamaiInterstitialSolver` and `MathCaptchaSolver`.

#### Changed
- 307 → 317 lib tests (+10). Clippy clean across `--all-targets`.

## [0.2.13] - 2026-05-11

### AkamaiInterstitialSolver — dedicated Akamai Bot Manager handler (2026-05-11, round 15)

Akamai Bot Manager (ABM) protects ~30% of the Fortune-500 web footprint. Captchaforge previously only detected Akamai (via the bundled community rule) but had no dedicated solver — every Akamai-protected site fell through to behavioural simulation, which is the wrong shape (Akamai isn't a click-the-checkbox vendor; it's a sensor-data interstitial). This release ships a dedicated solver for the same pattern that already worked for Cloudflare's "Just a moment" page.

#### Added
- **`solver::akamai_interstitial::AkamaiInterstitialSolver`.** Polls the page for Akamai signals (sensor-script tags, `bmak` global, "Press & Hold" challenge text) and the `_abck` cookie. When the cookie's third hyphen-segment flips to `-1`, harvests cookies and returns success. Suspicious states (positive flag) and Akamai's own "Access Denied / Reference #" block page return failure so the chain falls through.
- **`solver::akamai_interstitial::classify_abck`** + **`AbckValidity { Pending, Valid, Suspicious }`.** Pure classifier exposed publicly so callers can replay-then-classify outside the chain (e.g. session-warming flows that only want to know if the cookie is good). Doctest demonstrates the four cases.
- **Default chain now ships 11 solvers** (was 10). `AkamaiInterstitialSolver` slots between `CloudflareInterstitialSolver` and `MathCaptchaSolver` so any Akamai-shaped detection routes through it before generic fallbacks.

#### Changed
- 295 → 307 lib tests (+12). Clippy clean across `--all-targets`.

## [0.2.12] - 2026-05-11

### Bench oracle wiring — every Observation row carries the page-state verdict (2026-05-11, round 14)

The bench's `success` column reports "outcome matched expectation" — useful, but says nothing about *why* the outcome was what it was. Was the solver successful AND the page actually advanced? Was the solver successful but the captcha was still on screen (recycled — green-checkmark lie)? This release plumbs the 0.2.10 oracle's verdict into every bench row so the audit trail is complete.

#### Added
- **`Observation.verified_outcome: Option<String>`.** The chain's `verified_outcome` (one of `"advanced"` / `"recycled"` / `"hard_block"` / `"silent_fail"` / `"unknown"`, or `None` when verify_outcome was off / detection failed before any solver ran). Serialised in JSON reports with `skip_serializing_if = "Option::is_none"` so old reports stay parseable but new ones surface the verdict.
- **`real_waf` suite populates `verified_outcome` from `result.verified_outcome`.** The chain runs with `verify_outcome: true` by default, so every real_waf row now reports the oracle's view alongside the expectation match.
- **All 7 other suites** (accuracy, consistency, detection, solve, stress, throughput, regression) initialised the field as `None` so old behaviour is preserved; opt-in plumbing per-suite as those suites grow oracle-aware.

#### Changed
- 30 → 30 int test bins green (none broken). Workspace clippy clean.

## [0.2.11] - 2026-05-11

### TurnstileInteractiveSolver — dedicated managed-Turnstile solver (2026-05-11, round 13)

Cloudflare Turnstile is the most-deployed captcha on the public internet (~12M sites per BuiltWith Q1 2026). The generic `BehavioralCaptchaSolver` already attempts a checkbox click for it, but the production iframe is cross-origin (so `iframe.contentDocument` is null), and Cloudflare's risk model penalises clicks that arrive without warmup. Real-world solve rate on managed Turnstile was therefore nowhere near "legendary." This release ships a dedicated solver that closes those gaps.

#### Added
- **`solver::turnstile_interactive::TurnstileInteractiveSolver`.** Dedicated managed-Turnstile solver. Locates the cross-origin widget by reading the iframe's `getBoundingClientRect` from the *parent* document (which we always have access to) and applying the stable in-iframe checkbox offset (`CHECKBOX_OFFSET_X = 28`, `CHECKBOX_OFFSET_Y = 32`). Pre-click warmup: two bezier mouse meanders with 120–380ms gaps so Cloudflare's risk model sees activity that doesn't immediately resolve to the checkbox. Approach with overshoot+correction, jittered press hold, then poll for the `cf-turnstile-response` token. When Cloudflare escalates to a managed-challenge sub-iframe (`challenge-platform/scripts/`), the solver short-circuits with a screenshot so VLM gets a turn rather than spinning blindly. 6 unit tests pin the iframe selector, checkbox offsets, support gating (Turnstile only — never claims hCaptcha or other vendors), and SolveConfig wiring. Public constants: `TURNSTILE_IFRAME_SELECTOR`, `CHECKBOX_OFFSET_X`, `CHECKBOX_OFFSET_Y`.
- **Default chain now ships 10 solvers** (was 9). `TurnstileInteractiveSolver` slots between `SliderCaptchaSolver` and `BehavioralCaptchaSolver`, so Turnstile-specific logic always wins for `DetectedCaptcha::Turnstile` while every other vendor still falls through to the generic behavioural path.

#### Changed
- 289 → 295 lib tests (+6). Clippy clean across `--all-targets`.

## [0.2.10] - 2026-05-11

### Outcome-verification oracle + stealth profiles + token-replay oracle (2026-05-11, round 12)

A token returned by a solver is a *claim*, not a *proof*. This release closes that gap on three fronts: page-state verification (did the page actually advance?), browser fingerprint coherence (does the UA agree with the platform/GPU/screen?), and token replay (does the verify endpoint accept the token?). Wafrift-quality bar: trust nothing, verify everything.

#### Added
- **`solver::oracle` — outcome-verification oracle.** `PageSnapshot { url, title, body_excerpt, captcha_present, cookie_names }` and `classify(before, after) -> OutcomeClassification` (Advanced / Recycled / HardBlock / SilentFail / Unknown). Pure classifier — testable without CDP. `take_snapshot(page)` is the CDP-bound counterpart. 11 unit tests + 3 doctests covering the full transition matrix. `BLOCK_PHRASES` constant exposes the curated WAF block-marker list.
- **`ChainConfig::verify_outcome` (default `true`).** Chain now snapshots page state before each solver attempt and re-snapshots after each claimed-success result. When the oracle classifies the outcome as HardBlock / Recycled / SilentFail, `success` is overwritten to `false` and the failure-path side effects fire (telemetry, pattern store) so naive callers can't be fooled by a green-checkmark token that didn't actually advance the page. Eliminates the entire "solver lied" failure mode.
- **`CaptchaSolveResult.verified_outcome: Option<OutcomeClassification>`.** Every result now carries the oracle's verdict. `Some(Advanced)` is the only value that *proves* the page advanced; every other variant downgrades a token to "claimed but unverified". `None` only when the chain ran with `verify_outcome` off.
- **`stealth_profiles` — named browser fingerprints.** `StealthProfile` with 5 coherent variants: `ChromeWindowsStable`, `ChromeMacStable`, `EdgeWindowsStable`, `FirefoxLinux`, `ChromeAndroid`. Each pins a coherent (UA, platform, brands, GPU vendor/renderer, screen, languages, hardwareConcurrency, deviceMemory) tuple — UA-vs-platform mismatches are *more* suspicious than vanilla headless, so the profiles never cross-pollinate. `apply_stealth_profile(page, profile)` injects via CDP `addScriptToEvaluateOnNewDocument` after `apply_stealth`. 7 unit tests including the cross-coherence guard (Edge profile must NOT list Google Chrome brand).
- **`solver::token_oracle` — token-replay validator.** `TokenValidator` POSTs the solved token to a configured verify endpoint and classifies the response (`Accepted` / `Rejected` / `Inconclusive`). Catches demo-sitekey tokens, expired tokens, IP-rep rejections, origin-bound tokens — every "we have a token" failure mode that page-state oracle alone misses. Builder API: `with_field` / `with_encoding` (FormUrlEncoded / Json / BearerHeader) / `with_method` (Post / Get) / `with_timeout`. `classify_response(status, body)` exposed as a pure function for synthetic tests. 7 unit tests.
- **`Config.verify_outcome: Option<bool>` (TOML).** Override `ChainConfig.verify_outcome` from `.captchaforge.toml`. Defaults to chain default (`true`).

#### Changed
- 274 → 289 lib tests (+15), 18 → 21 doctests (+3).
- `MathCaptchaSolver::supports`, `PowCaptchaSolver::supports`, `SliderCaptchaSolver::supports`, `WaitForTokenSolver::supports` continue to apply the per-vendor name guards added in 0.2.9; verify_outcome now closes the residual gap where a name-permitted vendor responds differently than expected.

### Architecture sweep + community rule pack (2026-05-11)

#### Added
- **Per-detector module split.** `detect.rs` (1442 LOC) → `src/detect/` with one file per built-in detector (`turnstile.rs`, `recaptcha.rs`, `hcaptcha.rs`, `image_captcha.rs`, `audio_captcha.rs`, `pow_captcha.rs`, `slider_captcha.rs`, `canvas_captcha.rs`, `multi_step.rs`, `shadow_dom.rs`, `challenge_page.rs`). New captcha types ship as one self-contained file.
- **`CaptchaProvider` trait + `ProviderRegistry`.** Bundles a detector with its recommended `SolveMethod` ordering. Built-in providers wired via macro. `ProviderRegistry::with_built_in_rules()` returns built-in providers + bundled community rule pack, priority-sorted.
- **TOML rule layer (`detect::rules`).** Community-extensible captcha detectors as data files. Selector / window-global / script-src triggers compile to deterministic, escaped JS probes. Bundled `rules/community.toml` covers AWS WAF Captcha, DataDome, Akamai Bot Manager, PerimeterX/HUMAN, Arkose/FunCaptcha, Geetest v3+v4, Friendly Captcha, MTCaptcha, WordPress math captchas — 10 vendors with zero Rust per vendor.
- **`SolverTelemetry` trait + `SolveEvent`.** One event per solver attempt with outcome (`Success` / `Failure` / `Error` / `Timeout`), time, confidence, domain, captcha kind, method. Default `NoopTelemetry`. Cache hits also fire as Success with `solver: "TokenCache"` so dashboards distinguish cache-served from solved-from-scratch. Install via `chain.with_telemetry(Arc::new(...))`.
- **`TokenCache` with TTL.** Per-`(domain, captcha_type)` solved-token cache (60s default). Chain checks before solving; cache hit returns immediately and reports through telemetry. `chain.cached_solution(info)` exposes the standalone short-circuit for callers without a Page.
- **`PatternStore` persistence.** `save_to_path` / `load_from_path` (atomic JSON via temp+rename), `merge` (cross-instance share, higher `sample_count` wins), `load_or_default` (graceful first-boot path).
- **`auto_solve(page)` top-level convenience.** One-call detect + solve with the bundled provider registry + default chain.
- **CLI binary (`captchaforge`).** Subcommands `inspect-rules` / `detect <url>` / `solve <url>` with `--rules <file>` for additive extra TOML rule packs. Lives in `cli/` workspace member.
- **Per-rule HTML fixtures.** Every bundled rule has positive + negative HTML fixtures under `tests/rule_fixtures/<vendor>/`. The `community_rules` integration test asserts each rule fires on positive, rejects negative, and doesn't cross-fire on any other vendor's negative.
- **Chain ↔ cache ↔ telemetry integration tests.** Verify cache hit fires exactly one Success event with `solver: "TokenCache"`, miss fires nothing, domain isolation works, builder methods install passed instances.

#### Changed
- **`DetectedCaptcha` is `#[non_exhaustive]`.** Adding vendors is no longer a breaking change for downstream `match`. New `Custom(String)` variant lets TOML rules surface vendor identifiers without enum edits.

#### Fixed
- **Custom captchas now route through provider hints.** Previously every solver's `supports()` returned false for `DetectedCaptcha::Custom`, so all 10 TOML-detected vendors detected successfully but got no solver attempt — chain silently returned unsolved. Chain now consults the installed `ProviderRegistry` for Custom kinds and filters solvers by the provider's `recommended_solver_methods`. Install via `chain.with_provider_registry(...)` (`auto_solve` does this for you).

### Legendary push 3: name-routing + 6 new WAFs + CF interstitial (2026-05-11, round 11)

#### Architecture
- **`CaptchaProvider::recommended_solver_names`** trait method (default
  empty). Chain prefers name-keyed routing over method-keyed when set,
  eliminating collisions between solvers sharing a `SolveMethod`
  (Math, Pow, Slider, Behavioral all = `BehavioralBypass`).
- **Provider routing for built-in kinds** — chain consults the
  `ProviderRegistry` for `Turnstile` / `RecaptchaV2` etc. when the
  provider declares names. Lets dedicated vendor solvers
  (`CloudflareInterstitialSolver`) win over generics.
- **`ProviderRule::solver_names`** TOML field. Vendors declare
  preferred solvers by name in `community.toml`; RuleDetector exposes
  them via the new trait method.

#### New solvers
- **`CloudflareInterstitialSolver`** — handles the "Just a moment..."
  5-second JS challenge that fronts CF-protected sites. Distinct from
  the Turnstile widget. Passive wait + cf_clearance cookie capture.
  With stealth, the challenge passes; without, falls through cleanly.

#### New WAF rules (community.toml)
- **Imperva Incapsula** — JS interstitial → routes via
  CloudflareInterstitialSolver pattern.
- **Kasada** — x-kpsdk-ct token challenge → WaitForToken + Interstitial
  fallback.
- **F5 Distributed Cloud Bot Defense** (formerly Shape Security) →
  CloudflareInterstitialSolver pattern.
- **Yandex SmartCaptcha** — slider + image-grid → Slider + VLM.
- **Tencent Captcha** — slider + click-image → Slider + VLM.
- **KeyCaptcha** — drag-puzzle → Slider.

Each has positive + negative HTML fixtures under
`tests/rule_fixtures/<vendor>/` and asserts no cross-firing on other
vendors' negatives.

#### Slider solver coverage expanded
- Selectors added for Yandex / Tencent / KeyCaptcha.
- `supports()` extended to claim those vendor names.

#### Default chain order
- Nine solvers: WaitForToken → CloudflareInterstitial → Math → Pow →
  Slider → Behavioral → VLM → Audio → ThirdParty.

### Legendary push 2: slider, race, warmup, tier-2 stealth (2026-05-11, round 9)

#### Added
- **`solver::SliderCaptchaSolver`** — generic gap-detection + drag
  solver covering GeeTest v3+v4, DataDome, PerimeterX/HUMAN, AWS WAF
  Captcha, Akamai's slider variant, and homegrown sliders. Detects
  gap via canvas pixel-column brightness delta; drags with bezier
  trajectory + overshoot+correction.
- **`solver::RacingSolver`** — wraps N inner solvers and races them
  in parallel via FuturesUnordered, returning the first valid token.
  Cuts p99 latency, routes around third-party outages. Critical for
  the "0.00001% failure" goal.
- **`captchaforge::warmup`** module + `warm_session(page, duration)`.
  Runs natural mouse meander / scroll / hover for a budget of ms
  before captcha solving so reCAPTCHA v3 / Turnstile passive /
  hCaptcha invisible see a session that looks like a real visitor.
- **Tier-2 stealth** — canvas fingerprint noise (toDataURL +
  getImageData alpha-bit jitter), AudioContext fingerprint noise
  (getChannelData ±1e-7 sample noise), WebRTC IP leak prevention
  (RTCPeerConnection setLocalDescription SDP IP rewrite),
  navigator.hardwareConcurrency capped at 8, navigator.deviceMemory
  pinned to 8GB.

#### Changed
- **`SolveMethod` is `#[non_exhaustive]`.** New `SolveMethod::AutoPass`
  variant for the WaitForToken path. Future additions won't break
  downstream `match`.
- **Default chain order** — eight solvers now: WaitForToken → Math
  → Pow → Slider → Behavioral → VLM → Audio → ThirdParty.

### Legendary push: HTTPS bench, no automation flags, math + PoW solvers (2026-05-11, round 8)

#### Added
- **`solver::MathCaptchaSolver`** — parses math-captcha questions
  ("3 + 5 = ?", "what is two plus four", "12 ÷ 4") from page text and
  types the answer. Handles digit numbers, word numbers (zero-twenty),
  and word operators (plus/minus/times/divided by). Free, ~50ms.
  Restricted to math-flavoured Custom names + PowCaptcha + Image/Canvas
  so it doesn't claim slider/click captchas.
- **`solver::PowCaptchaSolver`** — solves ALTCHA / Friendly Captcha /
  MCaptcha / Cap.dev. Computes SHA-256 PoW in-page via SubtleCrypto
  for ALTCHA (active) or polls the widget's own worker output for
  Friendly / MCaptcha / Cap.dev. No third-party API, no LLM.
- **HTTPS bench server (rcgen self-signed TLS).** Bench fixtures now
  load from `https://localhost:<port>` so vendors that refuse
  non-HTTPS origins (modern Cloudflare / hCaptcha / Google) can
  actually issue tokens against test sitekeys. PREREQUISITE for
  honest auto-pass measurement.
- **Bench chromium config strips automation signals.** Removed
  `--enable-automation` (it was never explicit but chromiumoxide
  defaults add it), added `--exclude-switches=enable-automation`,
  `--disable-blink-features=AutomationControlled`, and the
  `--ignore-certificate-errors` family for the self-signed bench
  TLS.

#### Changed
- **Default chain order** — seven solvers now:
  WaitForToken → Math → Pow → Behavioral → VLM → Audio → ThirdParty.
  Cheapest solvers run first.
- **`Config::build_chain()`** mirrors the same order.

### Stealth module + honest bench results (2026-05-11, round 7)

#### Added
- **`captchaforge::stealth` module** + `apply_stealth(page)` helper.
  Injects CDP `Page.addScriptToEvaluateOnNewDocument` overrides
  for `navigator.webdriver`, `navigator.plugins`,
  `navigator.languages`, `Notification.permission`, `window.chrome`,
  `WebGLRenderingContext.getParameter` (vendor + renderer), and
  `HTMLIFrameElement.contentWindow.chrome`. Without these, modern
  WAFs (Cloudflare, Akamai, PerimeterX/HUMAN, DataDome) detect
  headless chromium *trivially* via `navigator.webdriver === true`
  and refuse to issue a passive-pass token regardless of mouse
  cleverness.
- **PRODUCTION.md "Stealth — required for production" section** —
  documents the surface, when to call `apply_stealth`, and the
  things stealth does NOT cover (UA string, TLS fingerprint, IP
  reputation) that operators must handle separately.
- **`auto_solve()` calls `apply_stealth` defensively** before the
  detection step. Production deployments should still call it
  themselves immediately after `Browser::new_page`, before the
  first navigation — that's the only way to catch first-page
  detection probes.
- **Bench `BrowserPool::reset_page` reapplies stealth** so every
  bench iteration starts with fresh stealth overrides — without
  this, the real-WAF suite reports 0% success on auto-pass
  fixtures purely because Cloudflare/hCaptcha/Google detect headless
  chromium and refuse the passive-pass token.
- **Bench `real_waf` suite uses a 12-second `WaitForTokenSolver`
  budget** (vs 3s default) — production passive Cloudflare can take
  5-10s to issue the token.

### WaitForToken passive-pass solver + honest real-WAF fixtures (2026-05-11, round 6)

#### Added
- **`solver::WaitForTokenSolver`** — first solver in the default
  chain. Polls common vendor response fields (`cf-turnstile-response`,
  `g-recaptcha-response`, `h-captcha-response`, plus invisible
  reCAPTCHA's `grecaptcha.getResponse()` and Friendly Captcha /
  MTCaptcha fields) and returns the token when the vendor populates
  it. Handles the most common production case — passive-pass widgets
  (Turnstile passive mode, reCAPTCHA v3, hCaptcha invisible) — without
  ever clicking, screenshotting, or calling a paid API. 3-second
  default max-wait when used as the chain's lead solver; if the field
  stays empty the chain falls through to behavioural / VLM /
  third-party as before. Cheapest possible solve path.
- **`SolveMethod::AutoPass`** + `#[non_exhaustive]` on the enum so
  future additions don't break downstream `match`. Telemetry events
  from the new solver carry this method tag.
- **`REAL_TURNSTILE_BLOCK` fixture** (Cloudflare always-block test
  sitekey `2x00000000000000000000AB`). Bench expects FAILURE — proves
  the chain doesn't fake-pass against a sitekey that issues no token.
- **`REAL_TURNSTILE_INTERACTIVE` fixture** (Cloudflare force-interactive
  test sitekey `3x00000000000000000000FF`). Honest gap measurement for
  the behavioural solver — passive-pass cannot help here, and any
  success% > 0 means real interactive solving worked.
- **`real_waf` suite gained per-fixture `Expectation`** (AutoPass /
  AlwaysBlock / ForceInteractive). The reported `success` column is
  "outcome matched expectation" — solving a block fixture is an
  INTEGRITY FAILURE, NOT a green row.

#### Changed
- **`CaptchaSolverChain::default_chain()` order**:
  `WaitForToken (3s) → Behavioral → VLM → Audio → ThirdParty`. Five
  solvers; existing tests updated. The cheap solver runs first so
  passive-pass widgets short-circuit the chain before any expensive
  layer fires.
- **`Config::build_chain()`** mirrors the same order so
  `auto_solve()` and the CLI inherit the change.

### Production-ready: bench rewire, real-WAF proof, retry, metrics, security warn, PRODUCTION.md (2026-05-11, round 5)

#### Added
- **`real_waf` bench suite (opt-in)** — drives the production
  `Config::default().build_chain()` against the real
  `challenges.cloudflare.com`, `js.hcaptcha.com`, and
  `google.com/recaptcha/api.js` scripts using each vendor's published
  test sitekey. Asserts the chain returns a token that's actually
  shaped like a real one (rejects the local-fixture `XXXX.DUMMY.TOKEN`
  stubs). Skipped by default; pass `--with-network` (or `--suite real-waf`)
  to enable. Three new fixtures: `REAL_TURNSTILE`, `REAL_HCAPTCHA`,
  `REAL_RECAPTCHA_V2`.
- **`solver::CacheStats` + `TokenCache::stats()` / `reset_stats()`** —
  atomic hit/miss/expired/put/invalidate counters and a `hit_rate()`
  helper. Production deployments scrape `stats()` each minute to plot
  cache effectiveness; a high `expired_misses : hits` ratio is the
  signal that TTL is shorter than the typical re-visit window.
- **Transient-error retry + exponential backoff in `ThirdPartyCaptchaSolver`** —
  3 attempts with 250ms→500ms→1s→2s backoff (capped 4s) on `submit_task`
  and per-poll HTTP. Distinct from the terminal-API-error fail-fast
  already shipped: terminal codes (bad key, zero balance, banned IP)
  short-circuit on first hit; transient HTTP / 5xx retries to recover
  from network blips. New tests cover all four retry-state-machine
  branches.
- **`Config::warn_on_inline_secrets(path)`** — `Config::load_from_path`
  emits a `tracing::warn!` when `[third_party] api_key` is set inline,
  recommending `CAPTCHAFORGE_THIRDPARTY_API_KEY` env var instead.
  Stops accidental secret commits to git alongside config.
- **`PRODUCTION.md` deployment guide** — 200+ lines covering
  pre-launch sanity checks, the three configuration layers, telemetry
  sink choice + alert thresholds, token-cache lifecycle gotchas,
  pattern-store persistence patterns, third-party adapter operational
  notes, browser lifecycle / crash recovery, security guidance, and
  upgrade procedure.

#### Changed
- **Bench `solve` / `accuracy` / `throughput` suites rewired to
  `Config::default().build_chain()`.** Previously instantiated
  `BehavioralCaptchaSolver` etc. directly, bypassing the cache,
  provider registry, third-party solver, and telemetry — so the bench
  measured the individual solvers, not the production pipeline.
  Now reports the chain's actual winning method per fixture; cache
  short-circuit is part of the measured path. The `solver` column is
  the chain's `r.method` instead of a hand-coded label.
- **Bench `--suite real-waf` (CLI variant) + `--with-network` flag**
  added so `--suite all --with-network` runs the network-dependent
  proof end-to-end. Without `--with-network`, `--suite all` skips
  `real_waf` so CI offline doesn't fail.
- **Default fixture count: 34 → 37** (added 3 real-WAF fixtures).
  `fixtures_count_is_34` test renamed to `fixtures_count_is_37`.

#### Fixed
- **CLI `cmd_solve` no longer duplicates chain construction** —
  delegates to `Config::build_chain()`, single source of truth shared
  with `auto_solve`.

### Tier-A discovery, cache-cookies bridge, fail-fast third-party (2026-05-11, round 4)

#### Added
- **`Config::build_chain()`** — single canonical chain constructor: chain config + token cache + provider registry + Behavioral + VLM (env/TOML-aware) + Audio + Third-party. Used by both `auto_solve()` and the CLI's `solve` subcommand so they share the same wiring.
- **`TokenCache::put_full(.., cookies)`** + `CachedToken::cookies()`. Cache entries now carry both the solved token AND the cookies captured at the original solve. Cache hits replay the trusted session via `CaptchaSolveResult::cookies` so the next request rides the WAF/vendor's session — without this a hit returned only the token and the next navigation re-triggered the captcha. Back-compat `put` / `put_with_ttl` continue to work (cookies field stays empty).
- **Terminal-error fail-fast in `ThirdPartyCaptchaSolver::poll_result`.** Maps the documented 2captcha error codes (`ERROR_KEY_DOES_NOT_EXIST`, `ERROR_ZERO_BALANCE`, `IP_BANNED`, `ERROR_GOOGLEKEY`, `ERROR_CAPTCHA_UNSOLVABLE`, etc.) to immediate `Err` instead of polling 30 more times — saves up to 150s of wall-clock per doomed task and avoids submitting-without-results for paid services.

#### Changed
- **`captchaforge::auto_solve(page)` discovers Tier-A config.** Calls `Config::discover()` and builds the chain via `build_chain()`, so a user's `.captchaforge.toml` reaches the one-call API automatically. With no config file present, `discover()` returns defaults — the no-config install gets the same chain shape as before.
- **CLI `cmd_solve` builds the chain via `Config::build_chain()`** instead of duplicating the wiring logic. Single source of truth for chain construction across binaries.
- **Chain success path stores cookies in cache** via `cache.put_full(.., r.cookies.clone())` so the cache-hit replay path actually has cookies to replay.
- **`solver/mod.rs` docstring + `ARCHITECTURE.md` ASCII diagram** refreshed to show the four-solver chain (Behavioral / VLM / Audio / Third-party / Human-fallback) and the Tier-A config layer.

### Third-party adapters, EMA pattern store, env + Tier-A config (2026-05-11, round 3)

#### Added
- **`ThirdPartyCaptchaSolver` + `ThirdPartyService`.** Implements `SolveMethod::ThirdPartyService` against 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. Constructors: `two_captcha()`, `cap_monster()`, `cap_solver()`, `custom_endpoint(base_url)`. API key via `with_api_key(...)` or `CAPTCHAFORGE_THIRDPARTY_API_KEY` env var. Wired into `CaptchaSolverChain::default_chain()` as the fourth solver; `supports()` returns false without an API key, so a no-key install sees no behaviour change. Supported shapes: Turnstile, reCAPTCHA v2/v3, hCaptcha, DataDome, Arkose/FunCaptcha, GeeTest v3+v4 (AWS WAF needs `iv`+`context` not in the standard `CaptchaInfo` so it is intentionally skipped). On success injects the token into the page (`cf-turnstile-response` / `g-recaptcha-response` / `h-captcha-response`) and captures session cookies.
- **`captchaforge::config::Config` (Tier-A operational config).** TOML-driven config layer for timeouts, cache TTL, VLM endpoint/model, third-party service selection. Compiled defaults → `.captchaforge.toml` → CLI flags. `Config::discover()` walks `$CAPTCHAFORGE_CONFIG` / `./.captchaforge.toml` / `./captchaforge.toml` / `$XDG_CONFIG_HOME/captchaforge/config.toml`. Materialisers: `chain_config()`, `solve_config()`, `build_token_cache()`, `build_vlm_solver()`, `build_third_party_solver()`. `deny_unknown_fields` so typos in keys surface as parse errors instead of silent ignores.
- **CLI `--config <file>` flag.** Override the discovery search path explicitly; otherwise discovery runs. `cmd_solve` now builds the chain via the loaded config (cache TTL, VLM endpoint, third-party service all reachable from the shell).
- **`.captchaforge.toml.example`.** Bundled schema reference with every Tier-A field commented + defaults inline.
- **`CAPTCHAFORGE_VLM_ENDPOINT` + `CAPTCHAFORGE_VLM_MODEL` env vars.** `VlmCaptchaSolver::new()` reads them once at construction time. Builder methods (`with_endpoint` / `with_model`) still beat env. Empty env values fall through to compiled defaults. New `pub(crate) new_with_env(closure)` overload makes precedence testable without `unsafe` env mutation (the crate is `#![forbid(unsafe_code)]`).
- **`TokenCache::ttl()` accessor.** Exposes the configured default TTL so the config layer can verify it round-trips and tests can assert the wiring.

#### Changed
- **`PatternStore` switched from cumulative-mean to bounded-α EMA.** Old `α = 1/n` made vendor migrations register slowly: 100 historical wins drowned out 30 fresh failures, so the chain kept routing to a method that no longer worked. New behaviour: `α = 1/n` for the first 10 samples (bootstrap matches old semantics), then `α = 0.2` so a flipped vendor surfaces as the new winner within ~10 alternating samples. `CaptchaPattern` now tracks per-method stats in a `methods: HashMap<String, MethodStat>` field; `best_method` is the per-method EMA winner with ties broken by faster `avg_solve_time`. Ships with regression tests that prove (a) bootstrap matches cumulative-mean, (b) EMA flips winner within 10 samples after a vendor migration, (c) ties resolve to the faster method.
- **Built-in solvers' `supports()` accept TOML-rule (`Custom(_)`) kinds.** Behavioral / VLM can attempt any visible challenge — without this opt-in the provider-driven routing for `Custom` had nothing to land on once `ordered_solvers_for_custom` was tightened to honour `supports()`. Audio stays narrow (RecaptchaV2 + AudioCaptcha only) since it depends on the accessibility-audio button.
- **`ordered_solvers_for_custom` now filters on `supports(kind)`.** Previously matched purely on method name, which would route Custom captchas to the third-party solver even without an API key — wasting one round-trip per attempt. The combined filter is now `s.method() == recommended && s.supports(kind)`.
- **CLI `cmd_solve` builds the chain via the loaded config** instead of `default_chain()`, so cache TTL / VLM endpoint / third-party service overrides reach the runtime.

#### Fixed
- **Default chain previously had three solvers with the protocol-recommended `ThirdPartyService` method matching nothing.** Adding `ThirdPartyCaptchaSolver` plugs the gap; `default_chain_has_four_solvers_in_documented_order` pins the order.

### Cookies, polish, local proof (2026-05-11, round 2)

#### Added
- **`captchaforge::cookies` module + cookie capture.** All three built-in solvers (Behavioral, VLM, Audio) now capture session cookies on successful solve and surface them in `CaptchaSolveResult.cookies`. Replay via `cookies::apply_to_page(&page, &result.cookies)` to preserve the WAF/vendor's trusted session across navigations and avoid re-triggering the captcha. New `CapturedCookie` struct with `is_expired_now` / `keep_persistent_alive` helpers.
- **`examples/architecture_proof.rs`** — local-runnable markdown report covering TokenCache hit/miss latency, ProviderRegistry::find_by_kind latency, RuleDetector probe generation cost, and per-rule HTML fixture match latency. Browser-free, runs in well under a second after a release build. Use to spot hot-path regressions; the round-1 numbers were 55ns / 27ns / 12ns / 0.76µs / 0.02–0.07µs per match.

#### Changed
- **README "Strategies" section + ARCHITECTURE.md ASCII diagram** updated to reflect the post-sweep architecture (cache layer, provider routing, telemetry, cookie capture).

### Added
- Property-based testing: 40+ proptest properties across detect, solver, frame, and behavior modules
- Snapshot testing: insta-powered JSON snapshots for all key data structures
- Fuzz testing: 4 fuzz targets (fuzz_detect, fuzz_solve_config, fuzz_selector, fuzz_pattern_key)
- Chaos tests: Resilience testing for extreme inputs, NaN/Inf handling, empty strings
- GitHub Actions CI/CD: ci.yml, coverage.yml, bench.yml, fuzz.yml
- Comprehensive documentation: ARCHITECTURE.md, TESTING.md, BENCHMARKING.md, CONTRIBUTING.md, CHANGELOG.md
- Benchmark suite expansion: 35+ self-hosted CAPTCHA fixtures, 10 benchmark suites, 6 output formats

### Changed
- Test organization refactored into 5-tier pyramid: unit -> property -> snapshot -> integration -> chaos

## [0.2.1] - 2025-05-09

### Added
- CaptchaSolveResult.screenshot field for human-in-the-loop review
- VlmCaptchaSolver.with_endpoint() and .with_model() builder methods
- SolveConfig struct with all formerly-hardcoded timeouts
- CDP frame evaluation module (src/frame.rs) for cross-origin iframe piercing
- verify_token_in_frames() to prevent false-positive success claims
- BehavioralCaptchaSolver now verifies tokens before returning success
- AudioCaptchaSolver now verifies g-recaptcha-response post-submission
- ImageCaptcha and AudioCaptcha detection arms in detect.js
- 8 real-browser integration tests

### Fixed
- Removed 6 credential-leaking println! statements from scanner hot path
- Replaced naive split("://") with url::Url parsing in extract_domain
- Fixed lock poisoning in PatternStore with unwrap_or_else(|e| e.into_inner())
- Turnstile, Audio, reCAPTCHA v3 now verify tokens via verify_token_in_frames()

## [0.2.0] - 2025-05-08

### Added
- CaptchaSolverChain with ordered_solvers() and PatternStore historical success tracking
- BehavioralCaptchaSolver with Bezier mouse curves and realistic timing
- VlmCaptchaSolver with Ollama-compatible multimodal LLM integration
- AudioCaptchaSolver with STT endpoint support
- CaptchaPattern and PatternStore for per-domain success memory

### Changed
- Detection engine expanded to support hCaptcha, generic image/audio CAPTCHAs

## [0.1.0] - 2025-05-07

### Added
- Initial release: Turnstile and reCAPTCHA v2/v3 detection
- Basic CaptchaSolver trait
- captcha_detect back-compat re-export