@mochi.js/challenges
v0.2.1
Published
Convenience layer for Cloudflare Turnstile auto-click — opt-in, behavioral-synth-backed, no new fingerprint surface.
Maintainers
Readme
@mochi.js/challenges
Convenience layer for common bot-defense challenge widgets in mochi.
v0.2 scope: Cloudflare Turnstile visible-checkbox auto-click only.
Out of scope (deferred):
- hCaptcha — same shape, separate task (v0.3)
- reCAPTCHA v2 / v3 — different mechanism (audio / visual challenges)
- 3rd-party solver API integrations (2captcha / anti-captcha) — v0.3+ via
onEscalation
What this is — and isn't
This is not a captcha solver. The visible Turnstile checkbox is a behavioral test: Cloudflare watches the cursor trajectory, the dwell, and a few hundred other signals around the click. The hard part is the behavioral profile, which mochi already does (@mochi.js/behavioral's Bezier+Fitts synth, the inject pipeline's matrix consistency, the wreq TLS fingerprint). The actual click is the easy part — this package exists so you don't have to write page.humanClick('iframe[src*="challenges.cloudflare.com"]') yourself in every flow.
For escalated variants (image / audio / managed-failed), this package fires onEscalation(reason) and bails. It will not click randomly into a challenge iframe.
Install
This package ships with @mochi.js/core v0.2+. You don't add it to your project directly.
Usage
Recommended: launch option
import { mochi } from "@mochi.js/core";
const session = await mochi.launch({
profile: "...",
seed: "...",
challenges: {
turnstile: {
autoClick: true,
timeout: 30_000,
onSolved: (token) => console.log("turnstile passed:", token.slice(0, 8) + "…"),
onEscalation: (reason) => console.warn("turnstile escalation:", reason),
},
},
});
// Every page from this session auto-clicks Turnstile.
const page = await session.newPage();
await page.goto("https://example.com");Manual: installTurnstileAutoClick
import { installTurnstileAutoClick } from "@mochi.js/challenges";
const session = await mochi.launch({ profile: "...", seed: "..." });
const page = await session.newPage();
const dispose = installTurnstileAutoClick(page, {
timeout: 30_000,
onSolved: () => console.log("turnstile passed"),
onEscalation: (reason) => console.warn("escalation:", reason),
});
await page.goto("https://example.com");
// ... do stuff ...
dispose();How it works
- Detection. A small inject script is mounted on the page's main world via
Page.addScriptToEvaluateOnNewDocument({ runImmediately: true, worldName: "" })(PLAN.md §8.4). The script installs aMutationObserverfiltered to iframe inserts only — it does not fire on every DOM mutation. - Channel. When a Turnstile iframe is detected, the inject emits a tagged
console.debug({__mochi_event:"turnstile-detected", …})event and exposes a Symbol-keyed snapshot reader ondocument(the only externally observable surface; non-enumerable + non-configurable so page script can't tamper). - Click. The mochi-side poller drains the snapshot, finds the iframe via
DOM.getBoxModel, and dispatches a click viapage.humanClick(...)— the same Bezier+Fitts synth the rest of the framework uses. We never reinvent the synth. - Token. After the click, the inject reader watches the parent page's hidden
cf-turnstile-responsefield. When a token appears,onSolved(token)fires. - Escalation. If the iframe
srcmatches/challenge.html(image/audio) or/managed.html(failed-bot variant), or the token doesn't appear withinopts.timeout,onEscalation(reason)fires and we bail on that widget.
Invariants
- Uses existing behavioral synth — Bezier path + Fitts's-Law dwell from
@mochi.js/behavioral. No new fingerprint surface. - No new globals — the inject script's only observable property is a Symbol-keyed function on
document. The Symbol is non-enumerable, writable:false, configurable:false. - No new postMessage handlers, no new event listeners on
window. - Iframe-only MutationObserver filter — perf invariant; the observer rejects mutations cheaply before doing any work.
- PLAN.md §8.2 — never sends
Runtime.enable. Detection is poll-based viaRuntime.callFunctionOnagainst the document objectId. - PLAN.md §8.4 — main world (
worldName: "") for the inject script. Any non-empty world name is detectable.
When to bring a 3rd-party solver
Roughly: the visible-checkbox flow covers the common case. If your target consistently escalates to image / audio challenges, that's a signal that mochi's stealth posture isn't passing the bot heuristics — fix the upstream signal first, then reach for a solver.
The onEscalation callback receives "image-challenge" | "managed" | "timeout" and lets you fire your solver of choice. v0.3 will ship a first-party solver hook surface.
Reference
- PLAN.md §8.4 — main-world inject pattern
- PLAN.md §11 — behavioral synth (Bezier + Fitts)
tasks/0220-turnstile-auto-click.md— the briefdocs/limits.md— what's deferred to v0.3
