Skip to content

0xchasercat/mochi

Repository files navigation

mochi.js

The library for faithful browser automation.

npm version license: MIT CI status GitHub stars bun >= 1.1


The 30-second pitch

Why the current stack fails. patchright, puppeteer-real-browser, nodriver, and undetected-chromedriver all randomize fingerprint surfaces independently — pick a UA, pick a hardwareConcurrency, pick a WebGL renderer, hope nothing cross-references. A single probe that compares two surfaces breaks the spoof. They also run HTTP fetches out-of-band through the runtime's stock TLS stack, so JA4 reveals the spoofed Chrome is not a Chrome. They synthesize at most a mouse helper, not a biomechanical model. And they're patches against a moving Chromium target, not a coherent design.

What mochi does differently. Every fingerprint surface derives from one (profile, seed) pair through a 48-rule deterministic DAG — a Mac UA never lands next to Linux WebGL. All network traffic — page.goto, in-page fetch, and session.fetch — routes through Chromium itself, so JA4/JA3/H2 are real Chrome by construction. There is no parallel HTTP layer. humanClick/humanType/humanScroll are full Bezier+Fitts+lognormal-digraph models, parameterized off the matrix's behavior block. One library owns the whole pipeline.

mochi patchright puppeteer-real-browser nodriver undetected-chromedriver
Relational (profile, seed) matrix yes no no no no
Chromium-native session.fetch yes (browser-routed) no no no no
Behavioral synthesis (Bezier+Fitts+jitter) yes no mouse-helper only mouse-only no
Single-runtime stack yes (Bun) yes (Node) yes (Node) yes (Python) yes (Python)
Probe-Manifest harness as CI gate yes no no no no

How to get started.

bun add @mochi.js/core @mochi.js/cli
bunx mochi browsers install
// hello-mochi.ts
import { mochi } from "@mochi.js/core";

const session = await mochi.launch({ profile: "linux-chrome-stable", seed: "user-12345" });
try {
  const page = await session.newPage();
  await page.goto("https://httpbin.org/headers");
  console.log(session.profile.userAgent);
} finally {
  await session.close();
}

Full walkthrough: mochijs.com/docs/getting-started/quickstart. One-page comparison deep-dive: mochijs.com/docs/reference/comparison.

Proof

mochi v0.4.0 on a Linux datacenter IP (Aixit GmbH, hosting ASN, Frankfurt) scored bot: not_detected, suspect_score: 8 against FingerprintJS Pro v4. Patched Chrome reports 12 in comparable conditions; CloakBrowser 18. The tampering ML detected something — tampering_ml_score: 0.9853 — but the bot classifier did not promote because the relational fingerprint was internally coherent across every axis.

Everyone told you to spoof Windows. They were wrong. Linux has 4% desktop market share but is massively overrepresented in high-LTV segments — developers, engineers, researchers. WAFs trained on real traffic don't flag Linux because Linux is real users. The signal was always HeadlessChrome, not Linux. mochi defaults to host-OS matching: a Linux server runs the linux profile.

Full evidence and architectural rationale: reference/comparison · concepts/stealth-philosophy.

What this is for

People use mochi to scrape, to QA-test against staging WAFs, to debug WAF rules they think over-block, to build data pipelines, to run cross-browser regression suites, to simulate users for performance work, to research how detection systems actually behave. The mechanics are identical in every case — you want a real Chrome session that doesn't get caught in measures designed for actual attackers. mochi gives you that by being consistent, not by being deceptive.

We don't sort our users by intent. If your threat model is "don't get traced," mochi is the wrong tool — it's open source and the fingerprint profiles ship in the package, which means a sophisticated attacker treats them as a known signature to avoid. mochi is sized for developers who want their automation to look like a real Chrome.

What you get

  • Relational locking, not randomization. Every fingerprint surface (canvas, WebGL, audio, fonts, timing, MediaDevices, WebGPU, …) derives from a single (profile, seed) pair through a 48-rule DAG. No Frankenstein fingerprints — a Mac UA never lands next to Linux WebGL.
  • Zero-jitter spoofing. A single ~50KB inject payload runs at top-of-frame. JIT-friendly Proxy traps, no async round-trips when a WAF micro-times performance.now().
  • Inject delivery without the source-attribution leak. Fetch.fulfillRequest body splice on Document responses (CSP rewriter included), with Page.addScriptToEvaluateOnNewDocument({ runImmediately: true, worldName: "" }) as the about:blank fallback. Source-byte-indistinguishable from a same-origin developer's own <script> tag.
  • Behavioral synthesis. humanClick / humanType / humanScroll derive from biomechanical models — Bezier paths with overshoot+correction, Fitts-law movement times, lognormal digraph delays, Gaussian jitter — all parameterized per profile (hand, tremor, wpm, scrollStyle).
  • No parallel HTTP layer. session.fetch(url) routes through Chromium itself via CDP — Network.loadNetworkResource for simple GETs, page.evaluate("fetch") for everything else. JA4 / JA3 / H2 are real Chrome by construction because Chromium is the client. Cookies inherit from the page's origin; proxy egress is shared with page.goto.
  • Probe-Manifest harness. bun run harness:smoke captures a Probe Manifest from the live session and diffs it against per-profile baselines. Zero-Diff is a CI gate; intentional divergences live in expected-divergences.json next to a rationale.
  • Stock Chromium. No forks, no patches, no proprietary infrastructure. Pinned Chromium-for-Testing, auto-downloaded by mochi browsers install. BYO via binary: <path>.

What works / what doesn't

Direct port from the Limits page — the architectural-honesty document. Every entry there has a root cause and a tracking link. mochi gives you the best possible JS-layer answer for stealth automation against Chromium-family WAFs; some things genuinely require a Chromium patch and we name them.

Surface v0.1 status Notes
CDP pipe transport (--remote-debugging-pipe) works No TCP port, no Runtime.enable.
Page.goto / content / evaluate works evaluate is Runtime.callFunctionOn-based — JSON-serializable returns only.
Page.goto({ waitUntil: "networkidle" }) partial Mapped to "load" until per-frame Network.enable lands.
Relational fingerprint Matrix (48 rules) works (profile, seed)MatrixV1, deterministic, JSON round-trippable.
JS-layer spoofing (UA / UA-CH, navigator, WebGL, WebGPU, MediaDevices, Permissions, screen, fonts, timezone, locale) works Inject payload, JIT-proxy traps, top-of-frame.
Audio (OfflineAudioContext) byte-accurate fingerprint works Per-(profile, sample-rate) captures consumed via R-047 → audio-fingerprint inject module. The spoof distributes the residual across the 489 samples in [4510..4999) (using Math.fround to model f32 readback) so the page-side digest equals the captured baseline byte-exactly on every host architecture, not just Mac M-series.
Canvas (toDataURL) byte-accurate fingerprint works Per-profile data URL synthesis via R-048 → canvas-fingerprint inject module. Intercepts probe-sized canvases (300×150) with the captured baseline; probe-side hashString(url) + length + first-50-char prefix match byte-exactly. Non-probe sizes fall through to native rendering.
Behavioral synthesis (humanClick / humanType / humanScroll) works Bezier+Fitts+jitter; profile-parameterized (hand, tremor, wpm, scrollStyle).
Profile catalog (mac-m4-chrome-stable, mac-chrome-stable, mac-chrome-beta, windows-chrome-stable, linux-chrome-stable, mac-brave-stable) works Six real-device baselines captured against real Chrome on real devices, each filtered by FingerprintJS Pro suspectScore <= 20 and validated by the harness round-trip. Other catalog ids (mac-m2-…, mac-intel-…, win11-edge-…) still resolve to the generic placeholder.
Trace recording / replay (mochi recordhumanClick(sel, { trace })) deferred API surface forward-compatible; recorder lands in v1.x.
Browser-routed session.fetch (JA4/JA3/H2-coherent) works Routes through CDP — Network.loadNetworkResource for GETs, page.evaluate("fetch") for non-GET. Real Chrome JA4 by construction; cookies inherit from the page's origin; CORS applies for non-GET cross-origin calls.
session.fetch on FreeBSD / Alpine musl / Windows arm64 works Routes through the running Chromium process, so any host that runs CfT runs session.fetch.
Page.screenshot works PNG/JPEG/WebP via CDP Page.captureScreenshot; fullPage, clip, omitBackground, quality, encoding opts. Element-bounded capture ({ element: handle }) is a separate brief.
Proxy auth (HTTP/HTTPS/SOCKS5) works Inline URL or ProxyConfig shape; CDP Fetch.authRequired, no extension.
Cookie persistence (Session.cookies.{save,load}) works JSON file with version header + regex domain filter. Round-trips losslessly.
Page.localStorage.{get,set} / Page.sessionStorage works DOMStorage CDP, frame-scoped (defaults to current main-frame origin).
Page.grantAllPermissions() works Wraps Browser.grantPermissions with the full descriptor list. Pairs with R-036.
Proxy-PAC scripts not yet Use system network policy until the flag lands.
Turnstile auto-click works @mochi.js/challenges — opt-in via challenges: { turnstile: { autoClick: true } }. Visible-checkbox variants only; image / audio / managed escalations fire onEscalation instead of clicking blindly.
Init-script delivery without Page.createIsolatedWorld works Dual-mechanism: Fetch.fulfillRequest body-splice on Document responses (CSP-rewritten) plus Page.addScriptToEvaluateOnNewDocument({ runImmediately: true, worldName: "" }) fallback for about:blank and other non-HTTP nav targets. Idempotency guard via __mochi_inject_marker.
bot.incolumitas.com anti-debugger trap known limit C++-only fix path. Every CDP-driven tool trips it identically.
deviceandbrowserinfo.com/are_you_a_bot worker-injection trap known limit Same anti-debugger family as incolumitas.
fingerprint.com/web-scraping (datacenter IP, cold session) known limit Server-side IP-class scoring; route through residential.
Cross-engine FPU / JIT divergence (Safari-from-Chromium) out of v1 scope v1 is Chromium-family only.
Mobile / touch profiles out of v1 scope v2 roadmap.

The full limits document has the per-vector root-cause analysis. Read it before opening an issue saying "X site detects mochi" — half the answers are already there.

Comparison

mochi's peer group is the JS-layer stealth-automation tools that drive stock or near-stock Chromium. Each row below is a structural axis, not a marketing axis. The Comparison page has the deep version: each claim cites a specific upstream-source line range you can follow.

mochi patchright puppeteer-real-browser (archived) nodriver undetected-chromedriver
Runtime Bun ≥ 1.1 Node Node Python Python
Browser stock CfT stock Chromium stock Chrome + helpers stock Chrome stock Chrome (patched binary)
Runtime.enable avoided yes (asserted) yes no partial n/a (WebDriver)
Page.createIsolatedWorld avoided yes yes no yes n/a
Relational (profile, seed) Matrix yes no no no no
JS-layer fingerprint coverage (48-rule DAG) yes partial (~12 patches) partial (fingerprint-injector add-on) partial partial (flag-level)
Probe-Manifest harness as CI gate yes no no no no
Behavioral synthesis (humanClick/humanType) yes (Bezier+Fitts+jitter) no mouse-helper only mouse-only no
JA4/JA3/H2-coherent session.fetch yes (browser-routed) no no no no
Single-runtime stack (no pip install next to npm install) yes yes yes yes (Python only) yes (Python only)
Turnstile auto-click yes (@mochi.js/challenges) yes yes partial partial
Stable-Chrome quirks accumulated over 4+ years no partial partial yes yes
Ecosystem maturity (issues / PRs / community) new mid mid mid high

Where mochi wins today: relational consistency, no parallel HTTP layer, behavioral synthesis depth, harness-as-gate, single-runtime stack.

Where mochi loses today: ecosystem age, Turnstile auto-click polish, accumulated quirks-fixes from years of production deployment.

Deep version, with per-library audit reports: mochijs.com/docs/reference/comparison.

How it fits together

┌──────────────────────── User code (TypeScript) ────────────────────────┐
│   import { mochi } from "@mochi.js/core";                              │
└────────────────────────────────┬───────────────────────────────────────┘
                                 │
            ┌────────────────────▼────────────────────┐
            │  @mochi.js/core   — launch, CDP pipe,   │
            │                     Page, Session,      │
            │                     Session.fetch (CDP) │
            └────┬─────────────────┬──────────────────┘
                 │                 │
        ┌────────▼──┐  ┌───────────▼────────┐
        │ inject    │  │ behavioral         │
        │ (payload) │  │ Bezier+Fitts       │
        └────┬──────┘  └────────────────────┘
             │
        ┌────▼──────────┐
        │ consistency   │
        │ (Matrix DAG)  │
        └────┬──────────┘
             │
        ┌────▼──────────┐
        │ profiles      │   ◄──── @mochi.js/harness
        │ (data)        │         (Probe Manifest diff,
        └───────────────┘          PR gate / nightly)

Documentation

Convenience layers

For common bot-defense widgets, mochi ships opt-in convenience layers under @mochi.js/challenges. These are thin wrappers around the existing inject + behavioral pipelines — no new fingerprint surface.

const session = await mochi.launch({
  profile: "linux-chrome-stable",
  seed: "user-12345",
  challenges: { turnstile: { autoClick: true } },
});
// Every page from this session auto-clicks visible Turnstile checkboxes.

v0.2 covers: Cloudflare Turnstile (visible-checkbox variants only). Image/audio/managed escalations fire an onEscalation callback rather than clicking blindly. See packages/challenges/README.md and the Limits page.

Why Bun-only?

mochi is invariant I-3 in PLAN.md: Bun ≥ 1.1 only, no Node, no Deno. The reasons are concrete and load-bearing:

  • Pipe-mode CDPBun.spawn exposes file descriptors 3 + 4 directly to user code, which is what --remote-debugging-pipe needs. Node's child_process doesn't, so every Node-based stealth tool falls back to TCP — and a listening CDP port is a fingerprintable surface.
  • Bun:SQL — the offline-first profile lookup and bun work orchestrator both use Bun.SQL (libSQL-backed). No better-sqlite3 native dep, no migration story, no platform-specific build issues.
  • Bun.spawn + Bun.serve ergonomics — the harness fixture server, mochi capture Probe Manifest collector, and the conformance-suite proxy chain all rely on Bun-native primitives that have no zero-cost equivalent in Node land.

If you need a Node-runtime stealth tool today, patchright and puppeteer-real-browser are the live options.

Status

Foundations in main; first npm release 2026-05-08. @mochi.js/core 0.8.0 drops the Rust wreq cdylib entirely — Session.fetch now routes through Chromium itself via CDP (real Chrome JA4 by construction; cookies inherit from the page's origin; proxy egress shared with page.goto). No parallel HTTP layer remains. 0.8 also retunes ALL_BROWSER_PERMISSIONS for Chromium 148. Earlier 0.5/0.6/0.7 lines shipped Page.screenshot, the cookies / localStorage / sessionStorage / grantAllPermissions DX cluster, the Fetch.fulfillRequest dual-mechanism inject, byte-exact audio + canvas fingerprint blobs, and the host-OS-matching profile auto-pick (mochi.defaultProfileForHost()). Public API is stable; new surfaces are additive. The harness Zero-Diff gate runs on every PR. See CHANGELOG.md for what shipped where.

If you found this from somewhere and you're wondering whether to depend on it for production traffic: not yet. The "what works / what doesn't" matrix above is the honest cut. v1.0 will say so plainly.

Contributing

CONTRIBUTING.md covers the workflow: clone → branch → PR, conventional commits with package scopes, the gates CI runs (typecheck, lint, test, contract, harness Zero-Diff), and the documentation-discipline rules (every limit landed in reference/limits.md in the same PR as the gap it documents).

Acknowledgements

Stands on the shoulders of:

License

MIT.

About

The library for faithful browser automation. High-fidelity fingerprinting for Bun, engineered for consistency and transparency.

Topics

Resources

License

Contributing

Stars

Watchers

Forks

Packages

 
 
 

Contributors

Languages