-
Notifications
You must be signed in to change notification settings - Fork 1.1k
Architecture overview
Obscura is a workspace of eight crates.
obscura-cli CLI entry point. fetch, serve, scrape, mcp.
obscura-cdp Chrome DevTools Protocol server. WebSocket, dispatch, domain handlers.
obscura-browser Page type, navigation, lifecycle events.
obscura-js V8 runtime via deno_core. bootstrap.js + Rust ops.
obscura-dom DOM tree implementation.
obscura-net HTTP client, stealth client, cookie jar, robots cache, tracker blocklist.
obscura-mcp Model Context Protocol server.
obscura Embeddable Rust library API (Browser, Page, Element, CookieStore).
A Page.navigate from a CDP client:
CDP client (Puppeteer)
│ WebSocket frame
▼
obscura-cdp/server.rs accept, route by sessionId
│
▼
obscura-cdp/dispatch.rs method router, acquires v8_lock
│
▼
obscura-cdp/domains/page.rs Page.navigate handler
│
▼
obscura-browser/page.rs navigate_with_wait
│
├──► obscura-net/client.rs HTTP fetch
│
├──► obscura-dom/tree.rs parse HTML into the tree
│
└──► obscura-js/runtime.rs run inline scripts
│
└──► bootstrap.js + ops.rs DOM bindings
The dispatcher emits CDP events (Network.requestWillBeSent, Page.frameNavigated, Page.lifecycleEvent) back to the client through the same WebSocket.
All pages in a process share one V8 isolate. The isolate is single-threaded by design.
obscura_js::v8_lock::global() is a tokio::sync::Mutex that serializes V8 work. A handler that wants to run JS must acquire the lock first:
let _guard = obscura_js::v8_lock::global().lock().await;
page.evaluate(expr).awaitThe dispatcher routes long-running operations (navigation, eval) through process_with_interception in server.rs, which spawns the work onto the tokio LocalSet and releases the dispatcher to keep handling other CDP messages.
This is why Target.createTarget from many concurrent clients works: each newPage returns immediately while the actual navigation runs in a spawned task.
One page cannot hang or crash the process. obscura-js/runtime.rs provides a V8 termination watchdog (arm_watchdog, run_event_loop_bounded) that terminates the isolate from a separate thread when synchronous work overruns a budget, because tokio::time::timeout cannot preempt synchronous V8. It bounds the post-load settle, the navigation event-loop pumps, and --eval. obscura-js/cdp_watchdog.rs is a single shared watchdog the dispatcher arms around every CDP command, so a runaway page cannot hold the V8 lock and wedge other sessions (tunable via OBSCURA_CDP_COMMAND_TIMEOUT_MS). op_dom is wrapped in catch_unwind so a DOM-op panic degrades to a null result instead of aborting the process through V8's FFI frame, and obscura-dom/tree.rs rejects cyclic reparenting that would make tree walks loop forever. Scripted fetch()/XHR and module loads are timeout-bounded (OBSCURA_FETCH_TIMEOUT_MS), and the one-shot fetch CLI has a process-level hard deadline as a final backstop.
obscura-js/js/bootstrap.js provides the browser globals: document, window, navigator, location, observers, fetch, indexedDB, etc.
obscura-js/src/ops.rs registers Rust ops that the bootstrap calls into:
Deno.core.ops.op_dom('insert_before', parentNid, refNid, newNid);Adding a Web API usually means:
- JS shim in
bootstrap.jsthat exposes the API surface. - Rust op in
ops.rsthat performs the side effect (DOM mutation, fetch, crypto). - Register the op in
build_extension().
Worked example: Adding a CDP method or Web API.
Each CDP client connection gets attached to one or more targets.
Session IDs are "{targetId}-session". The dispatcher routes by sessionId in the incoming frame to the right Page.
Targets are created by Target.createTarget. Closing the WebSocket detaches all sessions but leaves the pages running.
Lifecycle events are emitted by obscura-browser/lifecycle.rs as the page transitions:
init → commit → domcontentloaded → load → networkidle2 → networkidle0
waitUntil on Page.navigate blocks until the requested level is reached. The Puppeteer / Playwright goto resolves on the matching Page.lifecycleEvent client-side.
--storage-dir persists cookies (cookies.json) and localStorage (localStorage/<origin>.json). Reads on process start, writes on every navigation and on graceful shutdown.
--stealth swaps the default reqwest client for obscura-net/wreq_client.rs, which randomizes TLS ClientHello and cipher order to match a real browser, and applies the bundled tracker blocklist before any request leaves the process.
- One crate per layer. Cross-crate calls go through the layer above, not sideways.
- All async is
tokiowith aLocalSetbecause V8 is!Send. - All DOM ops go through
op_domto keep the JS/Rust boundary narrow.