# 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.21] - 2026-05-11
### Real-human mouse trajectory replay (2026-05-11, round 23)
`mouse_move_bezier` (the only mouse-move helper since 0.1) dispatches a cubic Bézier path sampled at 8–20 evenly-spaced points with a random control polygon. Modern bot detectors (Akamai, DataDome, PerimeterX/HUMAN) train classifiers on per-pointer-event features that distinguish parametric curves from biomechanical motion: inter-event time distribution, trajectory curvature, two-stage velocity profiles (ballistic + corrective), and event density. A parametric Bézier with ease-in-out has a single smooth bell-shaped speed curve and monotone curvature — that's a fingerprint.
This release ships a small library of trajectories whose per-event features sit inside the human envelope, plus a replay helper that rotates/scales them onto a real `(x0, y0) → (x1, y1)` request.
#### Added
- **`captchaforge::mouse_traces`** — new top-level module with:
- `Trace` / `TracePoint` — `(x_norm, y_norm, dt_ms)` triples in a normalised `(0,0) → (1,0)` start→end frame.
- 6 hand-encoded bundled trajectories spanning the legitimate distribution: `direct_fast` (~370ms, two-stage velocity with mid-motion dip), `slow_careful` (~750ms, three corrective sub-movements + tremor), `arc_above` and `arc_below` (~470ms, natural elbow-pivot arcs), `overshoot_correct` (~580ms, snap-past-then-correct), `two_phase_pause` (~960ms, includes a 280ms mid-motion pause).
- `pick_trace(rng)` — uniform random selection.
- `trace_by_name(name)` — exact lookup.
- `replay_trace(page, x0, y0, x1, y1, trace, jitter_max_px)` — rotates/scales onto the request, dispatches one `Input.dispatchMouseEvent` per point with the recorded inter-event delay and ±`jitter` per-axis pixel jitter so back-to-back replays don't produce byte-identical event streams.
- `project_point(x_norm, y_norm, jitter, x0, y0, x1, y1)` — pure rotation+scale+translate, doctested.
- `trace_duration_ms(trace)` — sum of recorded gaps; useful for budget calculations.
- **`captchaforge::behavior::mouse_move_human`** — picks a random bundled trace and replays it. Drop-in alternative to `mouse_move_bezier` that takes the same `(page, x0, y0, x1, y1)` shape; per-call cost ranges 250–1500ms depending on the picked trace.
#### Changed
- `TurnstileInteractiveSolver` warmup and approach phases now use `mouse_move_human` instead of `mouse_move_bezier`. Turnstile's risk model is one of the more aggressive parametric-curve fingerprinters; the trace-replay path produces an event distribution that doesn't match Bézier sampling.
#### Verified
- 386 → 403 lib tests (`+17`): 14 trace-module unit tests covering shape contracts (start at origin, end near target, monotonic-x except overshoot, arc orientation, pause presence, duration envelope) + project_point invariants (start lands at start, end lands at end, perpendicular rotation correctness, diagonal moves, degenerate zero-distance), plus 3 helper tests (pick_trace distribution coverage, builtin count constant, trace_by_name unknown returns None).
- 31 → 36 doctest (`+5`): `builtin_traces`, `trace_by_name`, `project_point` × 2, `trace_duration_ms`. All green.
- Clippy clean with `-D warnings`.
#### Notes
- The bundled trace shapes were synthesised, not literally recorded — what matters for fingerprint resistance is that they don't sample like a parametric Bézier. Each trace has been hand-tuned to exhibit one or more biomechanical signatures: two-stage velocity profile, non-monotone jerk, non-uniform sample density, or mid-motion pauses.
- Existing `mouse_move_bezier` callers continue working unchanged; trace replay is opt-in via the new helper.
## [0.2.20] - 2026-05-11
### reCAPTCHA v2 — dedicated audio-fallback solver (2026-05-11, round 22)
Adds `RecaptchaAudioSolver` under `src/solver/vendors/recaptcha_audio.rs`. The generic `AudioCaptchaSolver` already had reCAPTCHA-aware selectors but called `page.find_element` directly — and reCAPTCHA's audio widget lives inside the cross-origin `api2/bframe` iframe where the main-frame `DOM.querySelector` never sees it. The new dedicated solver walks frames via the shared `crate::frame` helpers (the same layer Turnstile-interactive and Behavioral use), so the bframe-internal controls are actually reachable.
#### Added
- **`src/solver/vendors/recaptcha_audio.rs`** — `RecaptchaAudioSolver` with:
- Bframe-aware control discovery (`#recaptcha-audio-button`, `#audio-source`, `#audio-response`, `#recaptcha-verify-button`) via `frame::find_element_centre_in_frames`, so the cross-origin iframe origin is respected.
- Passive-token short-circuit — if `g-recaptcha-response` is already populated when the solver runs, returns `SolveMethod::AutoPass` without touching the widget.
- Bounded retry loop (`MAX_AUDIO_RETRIES = 2`) for the multi-clip flow reCAPTCHA sometimes serves after a correct first answer.
- Deterministic rate-limit detection — `RATE_LIMIT_PHRASES` matched case-insensitively across all frame bodies. Returns failure with solution literal `"recaptcha:rate_limited"` so callers can branch on it.
- Realistic mouse + keyboard via `crate::behavior::{mouse_move_bezier, click_realistic, type_realistic}` — bots that paste via `set value` get caught by reCAPTCHA's input-event timing model.
- Configurable STT endpoint (`with_stt_endpoint`) defaulting to a local Whisper-compatible API. HTTP body is `audio/mpeg`; transcript is trimmed.
- **Pure helpers** (doctested):
- `clean_transcript(s)` — lowercases, strips punctuation, collapses whitespace. STT often returns "Hello, World." but the response field is space-separated tokens.
- `is_rate_limited(text)` — substring-matches reCAPTCHA's "automated queries" / "try again later" / "unusual traffic" phrases.
- `looks_like_audio_url(s)` — guards against `data:`, `blob:`, relative URLs, and `//`-protocol-relative tricks before fetching.
- Chain wiring — `RecaptchaAudioSolver::new()` is added to `default_chain()` between `VlmCaptchaSolver` and `AudioCaptchaSolver`. For a `RecaptchaV2` detection, the bframe-aware path now runs BEFORE the page-only fallback.
#### Changed
- Default chain grew from 16 → 17 solvers. The order assertion test was renamed `default_chain_has_seventeen_solvers_in_documented_order` and updated to include the new entry at index 14.
- Six pre-existing vendor doctests (`akamai_interstitial`, `arkose`, `aws_waf`, `datadome`, `perimeterx`, `geetest` × 2) used `use captchaforge::solver::<vendor>::...` import paths that broke when the 0.2.19 vendors collation moved them under `solver::vendors::<vendor>`. Doctests updated to the new paths; all 31 doctests now pass (was 24 + 7 failing).
#### Verified
- 370 → 386 lib tests (`+16` from new solver — 8 unit tests + 5 helper tests + 3 selector/contract tests). All green.
- Clippy clean with `-D warnings`.
- New solver's `supports()` returns true for `RecaptchaV2` and `AudioCaptcha`; false for HCaptcha, Turnstile, and all vendor-dedicated kinds.
## [0.2.19] - 2026-05-11
### Vendors collation — `src/solver/vendors/` namespace (2026-05-11, round 21)
Eight vendor-dedicated solvers shipped over 0.2.10 → 0.2.18 (Cloudflare interstitial, Akamai, DataDome, PerimeterX, GeeTest, AWS WAF, Arkose, Turnstile interactive). Mixed in the flat `src/solver/` directory alongside cross-cutting generic solvers (audio, behavioral, math_captcha, pow, slider, vlm, wait_for_token), the layout had become hard to scan. This release moves vendor solvers under their own `vendors/` subdirectory while preserving the public re-export surface byte-for-byte.
#### Changed
- **`src/solver/vendors/`** — new subdirectory containing the 8 vendor-dedicated solvers (`akamai_interstitial.rs`, `arkose.rs`, `aws_waf.rs`, `cloudflare_interstitial.rs`, `datadome.rs`, `geetest.rs`, `perimeterx.rs`, `turnstile_interactive.rs`).
- `vendors/mod.rs` re-exports every vendor symbol; `src/solver/mod.rs` re-exports the entire `vendors::*` set so existing `use captchaforge::solver::ArkoseSolver` paths continue to work without change. Pure file-move + re-export refactor; no logic delta.
- README-style "Adding a new vendor" runbook now lives in `vendors/mod.rs` doc comment so the next vendor lands in the right place by default.
#### Verified
- 370 → 370 lib tests (no regression; every test that referenced a vendor solver continues to pass through the re-exports). Clippy clean.
## [0.2.18] - 2026-05-11
### ArkoseSolver — Arkose Labs / FunCaptcha dedicated handler (2026-05-11, round 20)
Arkose Labs (still trades the FunCaptcha brand for the consumer-facing puzzle) is the dominant rotation/orientation challenge vendor. Used by Twitter/X, EA, Roblox, LinkedIn for at-scale anti-abuse. Detection landed in 0.2.0's community pack but no dedicated solver; all Arkose sites fell through to VLM/slider — wrong shape for the silent-enforcement case (most common with proper stealth) and unable to surface the verification-token cleanly.
#### Added
- **`solver::arkose::ArkoseSolver`.** Polls page for Arkose state via JS probe — distinguishes `passed` (verification-token / fc-token populated, classifier validates), `puzzle` (challenge iframe rendered — yields to VLM via screenshot+failure for the rotation/match-this-image solve), `interstitial` (SDK loaded, no token yet — keep waiting), `unknown`. Reads token from hidden inputs OR window globals (handles both new SDK and legacy integrations).
- **`solver::arkose::is_valid_arkose_token`.** Pure validator. Two accepted shapes: new SDK 3-tuple (`<id>.<epoch>.<sig>` with epoch in [2020, 2100)) OR legacy fc-token pipe-separated parameters (must contain `|r=<region>` segment). Permissive on segment encoding since Arkose has rotated; firm on structural skeleton. Doctest covers both shapes.
- **Default chain now ships 16 solvers** (was 15). `ArkoseSolver` slots between `AwsWafSolver` and `MathCaptchaSolver`.
#### Changed
- 357 → 370 lib tests (+13). Clippy clean across `--all-targets`.
## [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