Moved: on 2026-10-15 (version 0.3.0) this code moved to seleniumbase-rs-stealth (crates
seleniumbase-rs-wire,-scrape,-impersonate); the paths below are the ones it had here.
Status#
Accepted (2026-10-11). HttpSession, MitmProxy and StealthSession have since been merged into the
layout below; their public API is in the plan, not in this ADR.
Context#
ADR 001 covers what a page can read inside the browser. Stealth scraping adds the
wire: the TLS ClientHello, the HTTP/2 connection start and the header order,
plus the layer that keeps an identity, its cookies and its exit address together
while a scraper works through a site’s defences. The design is in
the plan. Its first draft grouped the files
by hexagonal layer (scrape/domain, scrape/adapters). On 2026-10-11 the tree
was regrouped by concern, with the work packages and the type names unchanged.
This record states the layout as it is in the code and the rules that hold it.
Decision#
-
Two opt-in features, and nothing else in the default build.
scrapeis pure Rust and adds onlypsl(the Public Suffix List, for cookie domains).impersonateincludesscrapeand adds wreq and btls, which build BoringSSL (cmake and a C/C++ toolchain), withhyper,brotliand a few small crates. No default build gains a dependency or C code. -
Modules by concern, not by layer.
src/wire(compiled with either feature):ja3,ja4,akamai,ids,client_hello,tls_spec,h2_spec,h2_frames,header_specandemulation, withBrowserKindinwire/mod.rsand the measured JSON inwire/measured.src/scrape(featurescrape):challenge(detect,markers,mitigation),egress(route,pool,geo,redact),jar(store,set_cookie),ledger(state,events),passport(identity,policy),error, andruntime.src/scrape/runtimeholds everything that does I/O: theBrowserScraper, the CDP leg, the egress switch, the JSON state store, the identity file, the tracing sink and the capture tools’ readers. The other directories hold only pure code and the ports it needs (StateStore,EventSink,GeoResolver;EgressSwitchandBrowserLaunchersit inruntime).src/impersonate(featureimpersonate):convertandclient(the only two files that name wreq types, so a change in the wreq release candidate stays in them),capture_h2,tls_server,http_session/(HttpSession),mitm/(the MITM proxy) andstealth_session/(StealthSession).
-
Pure modules are enforced by a test.
tests/scrape_domain_purity.rsparses every file undersrc/wireandsrc/scrape/{challenge,egress,jar,ledger,passport}withsynand fails on any use oftokio,std::fs,std::net,std::process,std::timeclock types, orInstant::nowandSystemTime::now(also behind a rename). Fromsb_cdpit allows only the data typesProxy,Cookie,SameSiteandCaptchaand thesync::lockedhelper. Functions that need the time takenowas an argument. The check is syntactic: it sees the names a file writes, not code reached through a type alias or a dependency, and its self-tests show it rejects an impure source and accepts a clean one. The gate is compiled only with thescrapefeature. -
Measured JSON instead of hand-typed values. Every TLS, HTTP/2, header and client-hint value is written by a capture tool (
capture_tls,capture_hints,capture_h2) against a real browser, stored insrc/wire/measured/<browser>-<major>-<os>.jsontogether with its raw evidence, embedded withinclude_str!and parsed once. An unmeasured browser and system pair isEmulationError::NotMeasured, never a fallback to different values. Today that is Chrome 155.0.8059.40 on macOS. What BoringSSL or wreq cannot send is a namedKnownGapin the file or a named refusal inImpersonateErrorbefore a client exists. -
No dependency on other stealth crates. Nothing from
stealthscraper-rs,impersonate-rs,impcurl,curl-impersonate-cliorwreq-utilis a dependency, optional or not. Their ideas informed the design; the code and the numbers are ours or measured here. wreq (a release candidate) and btls are pinned to exact versions, and the BoringSSL they link allows one copy in the dependency graph. -
House rules for the new modules. One
thiserrorenum per module, mapped toSeleniumBaseError::Stealthat the crate boundary (ScrapeError). URLs travel asRedactedUrland proxies asEgressKey, so a credential cannot reach a log, event, error or state file by construction.#![forbid(unsafe_code)]on the new modules, the pedantic lint setsb_cdpandmcpalready use, andtracingfor logging. Files that hold secrets (the identity file, the state file) are written atomically with owner-only permissions. -
Re-exports. The main types of each layer are re-exported at the top of
wire,scrapeandimpersonate, and a curated set at the crate root behind the same features. TheSpecErrorofwire::tls_specand the one ofwire::h2_specare re-exported asTlsSpecErrorandH2SpecError, so no two exported types share a name. The stealth layer is experimental and not a stable API.
Consequences#
- Positive: A concern is found by name, and its pure code is separate from the code that does I/O, so the pure part can be tested without a browser or a socket.
- Positive: The purity rule is checked by a test, not by convention.
- Positive: A wire value cannot drift from what a browser sent, because it is read from the capture and the capture’s evidence is kept beside it.
- Positive: The default build and the default dependency graph are unchanged.
- Negative: The measured data covers one browser on one system. Every other pair is refused until someone captures it, and a Chrome update leaves the profile behind until the capture tools are run again.
- Negative: The
impersonatefeature needs cmake and a C/C++ toolchain, and pinned release-candidate dependencies that can change API. - Negative: The purity check is syntactic and can be defeated by an alias. Review is still needed.
- Negative: The plan’s older paths (
scrape/domain/...,scrape/adapters/...) still appear in sections 2.1 and 5 of the plan until it is rewritten; the map at the top of its section 2 is the authority.
Not decided here#
The public API of HttpSession, MitmProxy and StealthSession is not recorded
in this ADR; see sections 3.11 to 3.13 of the plan and the tutorial. Measuring
more browsers and systems is open.
Verified against commit 4184e74 and the re-exports added after it (layout: src/lib.rs, src/wire/mod.rs,
src/scrape/mod.rs, src/impersonate/mod.rs; purity rules:
tests/scrape_domain_purity.rs; features and pins: Cargo.toml; measured data:
src/wire/emulation.rs, src/wire/measured/chrome-155-macos.json).