# eoka
[](https://crates.io/crates/eoka)
[](https://docs.rs/eoka)
[](https://github.com/cbxss/eoka/actions/workflows/ci.yml)
Stealth browser automation in Rust. Passes bot detection without the bloat.
## Requirements
Chrome or Chromium installed. eoka launches and controls it via CDP.
## Install
```toml
[dependencies]
eoka = "0.3"
tokio = { version = "1", features = ["rt-multi-thread", "macros"] }
```
## Quick Start
```rust
use eoka::{Browser, Result};
#[tokio::main]
async fn main() -> Result<()> {
let browser = Browser::launch().await?;
let page = browser.new_page("https://example.com").await?;
page.human_click("#button").await?;
page.human_type("#input", "hello").await?;
let png = page.screenshot().await?;
std::fs::write("screenshot.png", png)?;
browser.close().await?;
Ok(())
}
```
## Login Flow Example
```rust
use eoka::{Browser, Result, StealthConfig};
#[tokio::main]
async fn main() -> Result<()> {
let browser = Browser::launch_with_config(StealthConfig::visible()).await?;
let page = browser.new_page("https://example.com/login").await?;
page.try_click_by_text("Accept Cookies").await?;
page.human_click_by_text("Sign In").await?;
page.wait_for_visible("#email", 10_000).await?;
page.human_fill("#email", "user@example.com").await?;
page.human_fill("#password", "secret123").await?;
page.human_click_by_text("Log In").await?;
page.wait_for_text("Welcome back", 15_000).await?;
browser.close().await?;
Ok(())
}
```
## API
### Browser
```rust
let browser = Browser::launch().await?;
let browser = Browser::launch_with_config(config).await?;
let page = browser.new_page("https://example.com").await?;
let tabs = browser.tabs().await?;
browser.activate_tab(id).await?;
browser.close_tab(id).await?;
browser.close().await?;
```
### Finding Elements
```rust
page.find("#button").await?; // CSS selector
page.find_all(".item").await?; // all matches
page.find_by_text("Sign In").await?; // by visible text
page.find_any(&["#email", "[name='email']"]).await?; // first match
page.exists("#popup").await; // bool
page.text_exists("Error").await; // bool
```
### Clicking
```rust
page.click("#button").await?; // instant
page.human_click("#button").await?; // with mouse movement
page.click_by_text("Submit").await?;
page.human_click_by_text("Submit").await?;
page.try_click("#optional").await?; // Ok(false) if missing
page.try_click_by_text("Accept").await?;
```
### Typing
```rust
page.fill("#email", "user@example.com").await?; // clear + type
page.human_fill("#email", "user@example.com").await?; // human-like
page.type_into("#search", "query").await?; // append (no clear)
page.human_type("#search", "query").await?;
```
### Waiting
```rust
page.wait_for("#results", 10_000).await?; // in DOM
page.wait_for_visible("#email", 10_000).await?; // visible + clickable
page.wait_for_hidden(".loading", 5_000).await?;
page.wait_for_any(&["#ok", ".error"], 10_000).await?;
page.wait_for_text("Success", 10_000).await?;
page.wait_for_url_contains("dashboard", 10_000).await?;
page.wait_for_url_change(10_000).await?;
page.wait_for_network_idle(500, 30_000).await?; // XHR/fetch idle
```
### Elements
```rust
let elem = page.find("#btn").await?;
elem.click().await?;
elem.is_visible().await?; // Result<bool>
elem.bounding_box().await; // Option<BoundingBox>
elem.get_attribute("href").await?; // Option<String>
elem.tag_name().await?;
elem.value().await?;
elem.text().await?;
elem.is_enabled().await?;
elem.is_checked().await?;
elem.css("color").await?;
elem.scroll_into_view().await?;
```
### Keyboard, Hover, Select, Upload
```rust
page.press_key("Enter").await?;
page.press_key("Ctrl+A").await?;
page.select_all().await?;
page.copy().await?;
page.paste().await?;
page.hover("#menu").await?;
page.human_hover("#menu").await?;
page.select("#country", "US").await?;
page.select_by_text("#country", "United States").await?;
page.upload_file("input[type='file']", "/path/to/file.pdf").await?;
page.upload_files("input[type='file']", &["/a.pdf", "/b.pdf"]).await?;
```
### JavaScript & Frames
```rust
let count: i32 = page.evaluate("document.querySelectorAll('li').length").await?;
page.execute("window.scrollTo(0, 1000)").await?;
let title: String = page.evaluate_in_frame("iframe#widget", "document.title").await?;
```
### Page Info & Debug
```rust
page.url().await?;
page.title().await?;
page.content().await?; // full HTML
page.text().await?; // visible text
page.screenshot().await?; // PNG bytes
page.screenshot_jpeg(80).await?; // JPEG at quality 80
page.debug_state().await?; // PageState with element counts
page.debug_screenshot("step1").await?; // timestamped screenshot
```
### Multi-Tab
```rust
let page1 = browser.new_page("https://a.com").await?;
let page2 = browser.new_page("https://b.com").await?;
browser.activate_tab(page1.target_id()).await?;
browser.close_tab(page2.target_id()).await?;
```
### Navigation
```rust
page.goto("https://example.com").await?;
page.goto_with_referrer("https://example.com", "https://google.com").await?;
page.goto_with_headers("https://example.com", headers).await?;
page.reload().await?;
page.back().await?;
page.forward().await?;
```
### Network & Cookies
```rust
let cookies = page.cookies().await?;
page.set_cookie("name", "value", Some("example.com"), None).await?;
page.delete_cookie("name", None).await?;
page.clear_all_cookies().await?;
page.enable_request_capture().await?; // start capturing XHR/fetch
let body = page.get_response_body(id).await?;
page.disable_request_capture().await?;
```
### Configuration & Dialogs
```rust
page.set_bypass_csp(true).await?; // disable CSP
page.set_user_agent("custom UA").await?;
page.ignore_cert_errors(true).await?;
page.accept_dialog(None).await?; // accept alert/confirm
page.dismiss_dialog().await?; // dismiss dialog
```
### Retry
```rust
page.with_retry(3, 500, || async {
page.human_click("#flaky").await
}).await?;
```
## Config
```rust
let config = StealthConfig {
headless: false,
patch_binary: true,
human_mouse: true,
human_typing: true,
debug: true,
..Default::default()
};
// Presets
StealthConfig::visible() // headless: false
StealthConfig::debug() // headless: false, debug: true
```
## Detection Results
Patches Chrome binary, injects 15 evasion scripts, blocks detectable CDP commands at the transport layer, simulates human input with Bezier curves.
- **Passes**: sannysoft, rebrowser bot detector (6/6), areyouheadless, browserleaks
- **Partial**: creepjs (33% trust score)
## How it Works
~5K lines of Rust. No chromiumoxide, no puppeteer-extra. Hand-written CDP types for the ~30 commands actually needed. 9 crate dependencies.
```
src/
├── cdp/ # raw websocket transport, command filtering
├── stealth/ # evasions, binary patcher, human simulation
├── browser.rs # chrome launcher
├── page.rs # page api
└── session.rs # cookie export
```
The key insight: most detection comes from CDP commands leaking (`Runtime.enable` fires `consoleAPICalled` events that pages can detect). eoka blocks those at the transport layer and defines navigator properties on the prototype instead of the instance.
## Ecosystem
| [eoka-agent](https://crates.io/crates/eoka-agent) | AI agent layer with MCP server |
| [eoka-runner](https://crates.io/crates/eoka-runner) | Config-based automation (YAML flows) |
## License
MIT