Releases: KeyCode17/px-solver
Release list
v1.10.0
v1.10.0 — Per-crate documentation on crates.io
No code change from v1.9.0. This release exists to carry documentation to the registry: crates.io renders each crate's own README, versions are immutable, and the publish is gated on a minor bump — so the READMEs added after v1.9.0 needed a version of their own to become visible.
What's new
- A README for all 19 workspace members. Each published crate's page now shows its purpose, an install snippet, its public surface, and the caveats specific to it — proxy credentials for
pxsolver-harvester, the rotation math forpxsolver-camoufox, stub status forpxsolver-turnstile/pxsolver-captcha/pxsolver-datadome, and the v1.9.0 signature change forpxsolver-pipeline. Before this, all 16 pages showed a one-line description and nothing else. - The package/crate name split is documented. You depend on
pxsolver-corebut youuse px_core::…. Thepxsolver-prefix namespaces the family on crates.io; the source-level name stays short. This was not discoverable from a crates.io page. .cargo/config.tomladded, wiring up thecargo xtaskalias. ADR-0016, the root README andxtask/README.mdall documentedcargo xtask <cmd>, but no alias file existed — every documented invocation failed witherror: no such command: xtask.
Corrected
The root README told readers to run cargo install pxsolver-server pxsolver-cli. Both binaries are publish = false and have never been on crates.io, so that command has never worked. Only the 16 library crates are published; the binaries build from source:
git clone https://github.com/KeyCode17/px-solver
cd px-solver
cargo build --release -p px-server -p px-cliUpgrading from v1.9.0
Nothing to do — the code is identical. If you are coming from v1.8.0 or earlier, note that v1.9.0 was source-breaking: ChallengeHandler::solve and SolveDispatcher::solve take a SolveAction / SolveRequest instead of &PageHtml / &str. See the v1.9.0 notes.
Verification
cargo fmt --check · clippy clean under the unwrap/expect/panic ban · full test suite green · 200-LOC gate green · all 16 crates published at 1.10.0 with readme_path populated.
PRs merged: #21 (per-crate READMEs), #22 (bump + xtask alias).
v1.9.0
v1.9.0 — Egress proxies actually reach the browser
Four proxy surfaces shipped through v1.8.0 and only one of them reached a browser. This release makes the per-request proxy real end to end, and corrects the docs that described behaviour the code never had.
The bug, precisely
| Surface | Before v1.9.0 |
|---|---|
PX_PROXIES → ProxyPool → SessionPool |
worked, /v1/fetch sessions only |
POST /v1/solve body "proxy" |
deserialized into SolveRequestDto, never read |
px-cli solve --proxy |
sent in the request body, dropped server-side |
pxsolver-core::SolveRequest::with_proxy() |
published builder with zero consumers |
HarvestRequest.proxy |
honoured by CamoufoxPool, ignored by ChromiumoxidePool |
The cause was structural rather than a missed call site: SolveDispatcher::solve took &str and ChallengeHandler::solve took &PageHtml, so a proxy had no parameter to travel in, and both browser handlers built HarvestRequest::new(url) — which defaults proxy to None. ChromiumoxidePool::launch_browser had no --proxy-server argument at all.
What's new
- Per-request egress on
/v1/solve.ChallengeHandler::solvenow takes aSolveAction { page, proxy }constructed at the HTTP edge;SolveDispatcher::solvetakespxsolver_core::SolveRequest, so the published builder is the type the edge maps into. Chromium, Camoufox and the native sensor path all honour it. - Native path proxying.
SolveContextcarries the proxy andSensorNativeSolverposts through a per-proxyreqwestclient, cached inProxyClientsso the pooled TLS connection survives repeat solves. - The egress is part of the cache key. A bundle earned through proxy A is no longer replayed to a caller who asked for proxy B. A direct solve keeps
fp_key = 0, so existing entries still resolve. (This is the fix for the reported "differentproxyvalues return the same cached bundle" behaviour.) - Credentials are handled honestly. No browser engine can answer a proxy
407— geckodriver's W3Cproxycapability has no credential field and Chromium ignores userinfo without a CDPFetch.authRequiredhandler.user:pass@is now stripped with a warning naming the sanitized URL.reqwestdoes support proxy auth, so the native sensor path is exempt. socks5h://is normalized for Chromium, which has no such scheme and silently ignores specs it cannot parse.
Deliberately unchanged
/v1/solve never falls back to the PX_PROXIES rotation. A _px3 bundle is bound to the IP that earned it, so a caller who named no egress could not use a bundle harvested through a rotating one. Rotation stays where it pays: long-lived /v1/fetch Camoufox sessions, which consume the bundle themselves.
Docs corrected
docs/deployment.mdclaimedN × len(proxies)parallel egress paths. A session takes its proxy at spawn and holds it for the 300s TTL, so distinct egress IPs per domain ismin(PX_FETCH_MAX_PER_DOMAIN, len(PX_PROXIES))— ten proxies at the defaultN=2gives a domain two IPs, not twenty.- The
PX_PROXIESexample showeduser:pass@for paths that cannot authenticate. docs/runbook-native-bypass.mdtold operatorsPX_PROXIEScovered the solve path, and its throughput soak built a directreqwestclient while claiming to run through their proxy. The soak now readsNATIVE_SOAK_PROXY(or the firstPX_PROXIESentry) and prints the egress it used.- README gains a Proxies section. Rationale: ADR-0025.
⚠️ Source-breaking despite the minor bump
ChallengeHandler::solve and SolveDispatcher::solve changed signature. If you implement ChallengeHandler outside this workspace:
// before
async fn solve(&self, page: &PageHtml) -> Result<HandlerOutcome, AppError>
// after
async fn solve(&self, action: &SolveAction) -> Result<HandlerOutcome, AppError>
// action.page is the old argument; action.proxy is the requested egressEverything else in the published surface is unchanged. A pxsolver-* = "1" pin will fail to compile on cargo update — this was shipped as a minor knowingly, and the trade is recorded in ADR-0025.
Release mechanics
xtask bump only ever rewrote [workspace.package] version, leaving the internal pxsolver-* dependency pins at 1.4.0 since that release. Minor bumps hid it (1.8.0 satisfies ^1.4.0). bump now re-pins them with the workspace version, and all pins track 1.9.0.
Also clears RUSTSEC-2026-0185 (quinn-proto) and RUSTSEC-2026-0190 (anyhow) — both transitive, lock-only.
Verification
cargo fmt --check · cargo clippy --workspace --all-targets --all-features under the unwrap/expect/panic ban, zero warnings · full workspace test suite, zero failures · 200-LOC gate green · cargo audit --deny warnings clean. New tests cover the proxy reaching the routing dispatcher, the Cloudflare harvester and the native solver, the --proxy-server argument shape and socks5h normalization, egress-scoped cache entries, and absent optionals staying off the wire.
PRs merged
- #20 —
feat(proxy): honour the per-request egress proxy end to end
Published to crates.io
All 16 pxsolver-* library crates are published at 1.9.0. The pxsolver-server and
pxsolver-cli binaries are publish = false — build them from source.
Commits since v1.8.0:
- Merge pull request #20 from KeyCode17/feat/proxy-end-to-end (f5b3ee8)
- chore(deps): clear RUSTSEC-2026-0185 and RUSTSEC-2026-0190 (2094431)
- chore: bump to 1.9.0 (d6e263a)
- fix(proxy): stop two silent drops on the wired-up egress path (24be22e)
- style(errors): capitalize error messages in the proxy-touched harvester files (f534b63)
- fix(xtask): re-pin internal deps when bumping the workspace version (ed4bf67)
- feat(proxy): honour the per-request egress proxy end to end (9d3cdba)
v1.8.0
v1.8.0 — Native PerimeterX bypass: dispatcher + capture/calibrate/soak
This release ships the wiring for the native px-3 sensor synthesis path from v1.7.0. The native solver is no longer just a library — it's runnable end-to-end via the dispatcher, and there's a complete operator workflow for capturing ground-truth payloads, calibrating field grammar, and measuring live throughput.
What's new
- Dispatcher overlay (P1, #15). New
PX_NATIVE_PROFILES=domain=path/to/profile.tomlenv var. When set, the solve dispatcher wraps the per-domain handler with aNativeFirstHandler(try native, fall back to Camoufox on error or non-solved). Opt-in; default behaviour unchanged. - XHR-hook captures (P2, #16).
pxsolver-camoufox::capture_sensorspawns a session, injects aJSON.stringify+XMLHttpRequest.sendmonkey-patch, and dumps every plaintext[{t,d},…]sensor batch + every/b/swire payload to JSON. - Calibrate command (P3, #16).
px-cli calibrate <capture.json>diffs a real capture againstdefault_batch, reporting missing tags / extra tags / per-tag missing keys. The closer the diff converges, the higher the trust score the native path will earn. - Throughput soak (P4, #17).
cargo test -p pxsolver-native --test throughput_soak -- --ignoredrunsSensorNativeSolver::solveN× concurrently and asserts a configurable req/min target (default 40). - Operator runbook (P4, #17).
docs/runbook-native-bypass.mdwalks the four-step procedure: capture → calibrate → enable → soak.
Status
- Code complete. 132 workspace tests pass, clippy clean under the unwrap/expect/panic ban, all files ≤200 LOC.
- Live validation pending. The bet from v1.7.0 ("can we actually hit ≥40 req/min through native synthesis?") still needs an operator with an AR residential proxy + Camoufox install to run the runbook. The infrastructure is now in place — the open question is whether
default_batchearns a high enough trust score after calibration.
PRs merged
- #15 —
feat(server): wire SensorNativeSolver into solve dispatcher - #16 —
feat: XHR-hook captures + calibrate command for native synthesis - #17 —
feat(native): throughput soak + operator runbook for live bypass - #18 —
chore: bump to 1.8.0
🤖 Generated with Claude Code
v1.7.0
R-track decode + native sensor synthesis
This release completes the four open R-track phases for the pedidosya eT15wiaE PerimeterX tenant and lands the first five sprints of the native synthesis plan from ADR-0024 (supersedes ADR-0010).
What's new
- R2 — gB(t) static decoder. Custom Babel transform inlines every
gC(N)/gF(N)indirection in the captured init.js. 1 457 string literals decoded. Tools and outputs land underpx-research/tools/decoder/andpx-research/deobf/eT15wiaE/. - R3 — sensor RE. The px-3 payload encryption is XOR(0x32) + base64 +
vQstring-splice VM. No AES and no HMAC at the payload tier; integrity is the separatepxhcMD5 cookie. Notes inpx-research/notes/eT15wiaE/r3-sensor-grammar-2026-05-20.md. - ADR-0024 activates native synthesis. ADR-0010 marked
Superseded by ADR-0024. - N1 — cipher core. Dependency-free Rust ports of
jw,h_p,v_m,v_l,v_n,v_qunderpx-native/src/cipher/. Each module is <100 LOC and round-trips byte-for-byte against a JS reference (px-research/tools/decoder/cipher_ref.js). - N2 — sensor encryptor.
encrypt_sensor(events_json, pf, cu)chains the six primitives end-to-end matching the JSvPfunction. Fixedv_qto honour JSString.prototype.substringsemantics (clamp + swap). - N3 — event collector.
events/module:SensorEvent,EventField,SyntheticIdentity,default_batchemittingPX561/PX11978/PX12457baseline events. - N4 —
SensorNativeSolver. ImplementsNativeSolver::solve: builds the payload, POSTs to${origin}/${app_id_tag}${sensor_path}, parsesSet-CookieintoPxCookieBundle. - N5 — TOML tenant profile.
profile/schema.rs+ bundledpx-native/profiles/eT15wiaE.toml. - Stealth hardening (#10) — synthetic-user pool, humanize hooks, SOCKS5 auth-relay caps in
px-camoufox.
Test coverage
- 121 workspace tests pass, 0 failures
- 30 in
px-native(lib unit + cipher_fixtures + end_to_end) cargo clippy --workspace --all-targets --all-features -- -D warnings -D clippy::unwrap_used -D clippy::expect_used -D clippy::panic -D clippy::dbg_macro -D clippy::todo -D clippy::unimplementedclean- All files ≤200 LOC (largest:
sensor_solver.rsat 147)
Strategic constraints honoured
- No third-party CAPTCHA services. Native synthesis is the only bypass path.
- Throughput target ≥40 req/min remains gated on integrating
SensorNativeSolverinto the composition root and routing it ahead of the Camoufox harvester (planned for v1.8.0).
PRs merged
- #10 —
feat(camoufox): synthetic-user pool + humanize + SOCKS5 auth-relay caps - #11 —
docs(r-track): R0 + R2-pass-1 captures for pedidosya eT15wiaE - #13 —
feat(native): R2-R3 decode + N1-N5 native sensor synthesis (ADR-0024) - #14 —
chore: bump to 1.7.0
🤖 Generated with Claude Code
v1.6.0
v1.5.0
Release v1.5.0 (minor)
Changes since v1.4.0:
- chore: bump to 1.5.0 (f9bba1b)
- Merge pull request #8 from KeyCode17/feat/fetcher-throughput-tuning (c9c75ca)
- perf(camoufox): tune fetcher for 30+ req/min target (0e7ab47)
- feat(camoufox): per-domain persistent session for /v1/fetch (#7) (7acd612)
Published to crates.io: yes (16 pxsolver-* crates)
v1.4.0
Release v1.4.0 (minor)
Changes since v1.3.0:
- chore: bump to 1.4.0 (79090b4)
- Merge pull request #6 from KeyCode17/feat/v1-fetch-endpoint (7e142f7)
- Merge pull request #5 from KeyCode17/fix/cache-preserve-harvester-ua (7b4f879)
- feat(server): /v1/fetch endpoint backed by handler-owned browsers (17b9c9e)
- fix(server): cache hits preserve harvester user-agent (0b93d4a)
Published to crates.io: yes (16 pxsolver-* crates)
v1.3.0
Release v1.3.0 (minor)
Changes since v1.2.0:
- chore: bump to 1.3.0 (46354ee)
- ci(release): port rust-ai-surfer publish pattern (search+retry) (19028a3)
- Merge pull request #2 from KeyCode17/docs/readme-refresh (2764ce7)
- Merge pull request #3 from KeyCode17/feat/cli-phase04 (d42cacc)
- feat(server): drain in-flight solves on SIGTERM / Ctrl-C (#4) (c24338b)
- refactor(auth): make allowlist list() order deterministic (#1) (7ccc300)
- feat(cli): complete Phase 04 subcommand surface (5719089)
- docs(readme): reflect v1.2.0 published state (1de8e24)
- ci(release): send User-Agent to crates.io API (skip-if-exists) (a6cd163)
- ci(release): pre-flight skips already-published layer-0 crates (bae493e)
- ci(release): rate-limit aware publish loop (1ca83d5)
Published to crates.io: yes (16 pxsolver-* crates)