@mochi.js/core 0.0.1 → 0.1.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,152 @@
1
+ /**
2
+ * Minimal hand-curated CDP type surface. Mochi deliberately does NOT depend on
3
+ * `chrome-devtools-protocol` — the generated full surface is multi-megabyte and
4
+ * only a small slice is referenced. We grow this file as needed; every type
5
+ * here corresponds to a method or event Mochi actually issues.
6
+ *
7
+ * Reference: https://chromedevtools.github.io/devtools-protocol/
8
+ *
9
+ * @see PLAN.md §8 — CDP engine design notes
10
+ */
11
+
12
+ /** A monotonic CDP request id. */
13
+ export type CdpRequestId = number;
14
+
15
+ /** A logical CDP session (string id assigned by the browser). */
16
+ export type CdpSessionId = string;
17
+
18
+ /**
19
+ * The on-the-wire shape of an outbound CDP command. Optional `sessionId` routes
20
+ * to a sub-target; absent = root browser target.
21
+ */
22
+ export interface CdpRequest {
23
+ id: CdpRequestId;
24
+ method: string;
25
+ params?: unknown;
26
+ sessionId?: CdpSessionId;
27
+ }
28
+
29
+ /** Inbound CDP message — either a response to an `id`, or an event. */
30
+ export interface CdpResponse {
31
+ id?: CdpRequestId;
32
+ result?: unknown;
33
+ error?: { code: number; message: string; data?: unknown };
34
+ method?: string;
35
+ params?: unknown;
36
+ sessionId?: CdpSessionId;
37
+ }
38
+
39
+ /**
40
+ * A successful CDP response payload. The transport returns this when `error` is
41
+ * absent; the caller is responsible for typing `result` to a method-specific
42
+ * shape.
43
+ */
44
+ export type CdpResultEnvelope<T = unknown> = { result: T };
45
+
46
+ /**
47
+ * Subset of `Runtime.RemoteObject`. We only care about `objectId` (for
48
+ * callFunctionOn round-trips) and `value`/`type` for primitive returns.
49
+ *
50
+ * @see https://chromedevtools.github.io/devtools-protocol/tot/Runtime/#type-RemoteObject
51
+ */
52
+ export interface RemoteObject {
53
+ type: "object" | "function" | "undefined" | "string" | "number" | "boolean" | "symbol" | "bigint";
54
+ subtype?: string;
55
+ className?: string;
56
+ /** Present when the value is JSON-serializable. */
57
+ value?: unknown;
58
+ /** Present when the object lives only by reference; required for callFunctionOn. */
59
+ objectId?: string;
60
+ description?: string;
61
+ }
62
+
63
+ /** Subset of `DOM.Node` we consult. */
64
+ export interface DomNode {
65
+ nodeId: number;
66
+ backendNodeId: number;
67
+ nodeType: number;
68
+ nodeName: string;
69
+ }
70
+
71
+ /** Subset of `Page.Frame`. */
72
+ export interface PageFrame {
73
+ id: string;
74
+ parentId?: string;
75
+ url: string;
76
+ loaderId?: string;
77
+ }
78
+
79
+ /** `Page.frameAttached` event params. */
80
+ export interface FrameAttachedEvent {
81
+ frameId: string;
82
+ parentFrameId?: string;
83
+ }
84
+
85
+ /** `Page.frameNavigated` event params. */
86
+ export interface FrameNavigatedEvent {
87
+ frame: PageFrame;
88
+ }
89
+
90
+ /** `Target.attachedToTarget` event params. */
91
+ export interface AttachedToTargetEvent {
92
+ sessionId: CdpSessionId;
93
+ targetInfo: {
94
+ targetId: string;
95
+ type: string;
96
+ title: string;
97
+ url: string;
98
+ attached: boolean;
99
+ };
100
+ waitingForDebugger: boolean;
101
+ }
102
+
103
+ /**
104
+ * Subset of `DOM.BoxModel` we consume in `Page.humanClick`. CDP returns the
105
+ * border-box (and content-box, padding-box, etc.) as flat 8-number quads:
106
+ * `[x0, y0, x1, y1, x2, y2, x3, y3]` walking the corners CCW.
107
+ *
108
+ * @see https://chromedevtools.github.io/devtools-protocol/tot/DOM/#type-BoxModel
109
+ */
110
+ export interface BoxModel {
111
+ content: readonly number[];
112
+ border: readonly number[];
113
+ padding: readonly number[];
114
+ margin: readonly number[];
115
+ width: number;
116
+ height: number;
117
+ }
118
+
119
+ /**
120
+ * Subset of `Input.dispatchMouseEvent` parameters we send. The full CDP type
121
+ * has many optional fields; we only construct the ones the behavioral path
122
+ * actually needs.
123
+ *
124
+ * @see https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchMouseEvent
125
+ */
126
+ export interface DispatchMouseEventParams {
127
+ type: "mousePressed" | "mouseReleased" | "mouseMoved" | "mouseWheel";
128
+ x: number;
129
+ y: number;
130
+ button?: "none" | "left" | "middle" | "right";
131
+ buttons?: number;
132
+ clickCount?: number;
133
+ modifiers?: number;
134
+ deltaX?: number;
135
+ deltaY?: number;
136
+ }
137
+
138
+ /**
139
+ * Subset of `Input.dispatchKeyEvent` parameters we send.
140
+ *
141
+ * @see https://chromedevtools.github.io/devtools-protocol/tot/Input/#method-dispatchKeyEvent
142
+ */
143
+ export interface DispatchKeyEventParams {
144
+ type: "keyDown" | "keyUp" | "rawKeyDown" | "char";
145
+ key?: string;
146
+ code?: string;
147
+ text?: string;
148
+ unmodifiedText?: string;
149
+ modifiers?: number;
150
+ windowsVirtualKeyCode?: number;
151
+ nativeVirtualKeyCode?: number;
152
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,23 @@
1
+ /**
2
+ * Shared error types for `@mochi.js/core`. Centralized so internal modules can
3
+ * import without going through the public index barrel.
4
+ */
5
+
6
+ import { VERSION } from "./version";
7
+
8
+ /**
9
+ * Thrown when a public API surface is declared by the v1 contract but not yet
10
+ * implemented in the current phase. Always names the target API so callers can
11
+ * grep for the milestone in PLAN.md.
12
+ */
13
+ export class NotImplementedError extends Error {
14
+ readonly api: string;
15
+ constructor(api: string) {
16
+ super(
17
+ `${api} is not yet implemented. mochi is at v${VERSION}. ` +
18
+ "See PLAN.md §14 (Implementation phases) for the roadmap.",
19
+ );
20
+ this.name = "NotImplementedError";
21
+ this.api = api;
22
+ }
23
+ }
package/src/index.ts CHANGED
@@ -1,44 +1,51 @@
1
1
  /**
2
- * @mochi.js/core — the Bun-native browser automation framework.
2
+ * `@mochi.js/core` — the Bun-native browser automation framework.
3
3
  *
4
- * This is a v0.0.1 claim release. The full surface `mochi.launch()`, `Session`,
5
- * `Page`, `humanClick`, `humanType`, `humanScroll` lands incrementally per
6
- * the implementation phases in PLAN.md (phase 0.1 1.0).
4
+ * Phase 0.1: pipe-mode CDP transport + minimal Session/Page lands here. The
5
+ * spoofing pipeline (consistency + inject) wires in phases 0.2 → 0.3; the
6
+ * behavioral surface (humanClick/Type/Scroll) lands in phase 0.8. See PLAN.md
7
+ * §14 for the full roadmap.
7
8
  *
8
- * @see https://github.com/0xchasercat/mochi.js
9
+ * @see https://github.com/0xchasercat/mochi
9
10
  */
10
11
 
11
- export const VERSION = "0.0.1" as const;
12
-
13
- export class NotImplementedError extends Error {
14
- constructor(public readonly api: string) {
15
- super(
16
- `${api} is not yet implemented. mochi is at v${VERSION} (claim release). ` +
17
- `See https://github.com/0xchasercat/mochi.js for the implementation roadmap.`,
18
- );
19
- this.name = "NotImplementedError";
20
- }
21
- }
22
-
23
- /**
24
- * The mochi namespace. v0.0.1 placeholder; real surface lands in phase 0.1.
25
- */
26
- export const mochi = {
27
- /** Framework version. */
28
- version: VERSION,
29
-
30
- /**
31
- * Launch a browser session. Lands in phase 0.1.
32
- *
33
- * @example
34
- * ```ts
35
- * import { mochi } from "@mochi.js/core";
36
- * const session = await mochi.launch({ profile: "mac-m2-chrome-stable", seed: "user-1" });
37
- * ```
38
- */
39
- launch(_opts?: unknown): never {
40
- throw new NotImplementedError("mochi.launch");
41
- },
42
- } as const;
43
-
44
- export type Mochi = typeof mochi;
12
+ export { ChromiumNotFoundError } from "./binary";
13
+ export { ForbiddenCdpMethodError } from "./cdp/forbidden";
14
+ export {
15
+ BrowserCrashedError,
16
+ type CdpEventHandler,
17
+ CdpRemoteError,
18
+ CdpTimeoutError,
19
+ type SendOptions,
20
+ type Unsubscribe,
21
+ } from "./cdp/router";
22
+ // Error surface.
23
+ export { NotImplementedError } from "./errors";
24
+ // Public surface — exported here so users only need `@mochi.js/core`.
25
+ export {
26
+ type ChallengeLaunchOptions,
27
+ type LaunchOptions,
28
+ launch,
29
+ type Mochi,
30
+ mochi,
31
+ type ProfileId,
32
+ type ProxyConfig,
33
+ } from "./launch";
34
+ export {
35
+ type Cookie,
36
+ type GotoOptions,
37
+ type HumanClickOptions,
38
+ type HumanMoveOptions,
39
+ type HumanScrollOptions,
40
+ type HumanTypeOptions,
41
+ Page,
42
+ type PageInit,
43
+ type WaitForOptions,
44
+ type WaitState,
45
+ type WaitUntil,
46
+ } from "./page";
47
+ // Proxy URL parsing — exported so tests + downstream tools can normalize
48
+ // proxy strings without going through `launch()`.
49
+ export { type ParsedProxy, parseProxyUrl } from "./proxy-auth";
50
+ export { Session, type SessionInit, type StorageSnapshot } from "./session";
51
+ export { VERSION } from "./version";
package/src/launch.ts ADDED
@@ -0,0 +1,282 @@
1
+ /**
2
+ * `mochi.launch()` — entry point for opening a Session.
3
+ *
4
+ * v0.2 wires `@mochi.js/consistency`'s `deriveMatrix` into the launch path:
5
+ * the input `(profile, seed)` flows through the rule DAG and the resolved
6
+ * `MatrixV1` is stamped on the Session. The Matrix is **not** yet injected
7
+ * into the page — that's phase 0.3 (`@mochi.js/inject`). The browser still
8
+ * sees its native fingerprints; only `Session.profile` carries the spoof.
9
+ *
10
+ * @see PLAN.md §5.1 / §7 / §14
11
+ */
12
+
13
+ import { deriveMatrix, type ProfileV1 } from "@mochi.js/consistency";
14
+ import { resolveBinary } from "./binary";
15
+ import { spawnChromium } from "./proc";
16
+ import { parseProxyUrl } from "./proxy-auth";
17
+ import { Session } from "./session";
18
+ import { VERSION } from "./version";
19
+
20
+ /** Profile reference accepted by `mochi.launch`. */
21
+ export type ProfileId = string;
22
+
23
+ /** Proxy spec accepted by `mochi.launch`. */
24
+ export interface ProxyConfig {
25
+ server: string;
26
+ username?: string;
27
+ password?: string;
28
+ }
29
+
30
+ /**
31
+ * Per-challenge convenience options surfaced via `LaunchOptions.challenges`.
32
+ *
33
+ * v0.2 implements `turnstile.autoClick` only. Other entries (hCaptcha,
34
+ * reCAPTCHA, etc.) are reserved for v0.3+ — see `@mochi.js/challenges`
35
+ * README.
36
+ *
37
+ * When `turnstile.autoClick: true`, the `Session` calls
38
+ * `installTurnstileAutoClick(page)` on every page returned by `newPage`.
39
+ * The handle is disposed automatically on page close.
40
+ *
41
+ * The full `TurnstileOptions` (timeout / humanize / onSolved / onEscalation)
42
+ * are passed through unchanged. See
43
+ * `@mochi.js/challenges#TurnstileOptions`.
44
+ */
45
+ export interface ChallengeLaunchOptions {
46
+ turnstile?: {
47
+ /** When `true`, auto-install Turnstile detection + click on every newPage. */
48
+ autoClick?: boolean;
49
+ /** Override the per-widget post-click timeout (ms). Default 30_000. */
50
+ timeout?: number;
51
+ /** When `false`, use a fast non-humanized click path. Default `true`. */
52
+ humanize?: boolean;
53
+ /** Fired when a widget reports a token. */
54
+ onSolved?: (token: string) => void;
55
+ /** Fired on image-challenge / managed-variant / timeout. */
56
+ onEscalation?: (reason: "image-challenge" | "managed" | "timeout") => void;
57
+ /** Override the DOM-poll cadence (ms). Default 500. */
58
+ pollIntervalMs?: number;
59
+ };
60
+ }
61
+
62
+ /**
63
+ * Options accepted by `mochi.launch`.
64
+ *
65
+ * v0.2 behavior of fields:
66
+ * - `profile`, `seed`: drive `@mochi.js/consistency.deriveMatrix` to
67
+ * produce a relationally-locked `MatrixV1`. The Matrix is exposed via
68
+ * `Session.profile` but **not yet injected** into the page (phase 0.3).
69
+ * - `binary`: explicit override. Highest-priority resolution path.
70
+ * - `headless`: passes `--headless=new` to Chromium.
71
+ * - `proxy`: passes `--proxy-server=<server>` to Chromium with credentials
72
+ * stripped (Chromium rejects inline auth on that flag). When credentials
73
+ * are present (either in the URL form `http://user:pass@host:port` or
74
+ * via `ProxyConfig.username`/`password`) the Session installs a CDP
75
+ * `Fetch.authRequired` handler so HTTP / HTTPS / SOCKS5 / SOCKS4 proxy
76
+ * auth challenges are answered transparently. See
77
+ * `packages/core/src/proxy-auth.ts` for the invariant rationale.
78
+ * - `args`: appended after the default flag set.
79
+ * - `out.traceDir`: not yet honored at v0.1.
80
+ * - `timeout`: per-CDP-request default; defaults to 30000ms.
81
+ * - `bypassInject`: short-circuits the inject payload entirely (see field
82
+ * JSDoc). Intended for `mochi capture` and similar baseline-collection
83
+ * flows — never enable in production.
84
+ */
85
+ export interface LaunchOptions {
86
+ profile: ProfileId | ProfileV1;
87
+ seed: string;
88
+ proxy?: string | ProxyConfig;
89
+ headless?: boolean;
90
+ binary?: string;
91
+ args?: string[];
92
+ out?: { traceDir?: string };
93
+ timeout?: number;
94
+ /**
95
+ * When `true`, the {@link Session} skips both `buildPayload` (no payload
96
+ * is compiled) and `Page.addScriptToEvaluateOnNewDocument` on every new
97
+ * page. Auto-attached worker / service-worker / audio-worklet targets
98
+ * are likewise NOT injected — the browser reports its bare, un-spoofed
99
+ * fingerprints.
100
+ *
101
+ * Intended for `mochi capture` and similar baseline-collection flows;
102
+ * **do not enable in production**. The whole point of mochi is the
103
+ * inject pipeline; bypassing it produces a session that will be
104
+ * trivially fingerprinted as Chromium-for-Testing.
105
+ *
106
+ * Defaults to `false`. PLAN.md §12.1 (capture must run against bare
107
+ * Chromium); task 0040.
108
+ */
109
+ bypassInject?: boolean;
110
+ /**
111
+ * Convenience layer toggles for common bot-defense widgets. When
112
+ * `challenges.turnstile.autoClick` is `true`, every page returned by
113
+ * `Session.newPage` has `installTurnstileAutoClick(page, opts)` wired
114
+ * automatically — the Bezier+Fitts behavioral synth handles the click,
115
+ * an optional `onSolved` callback fires when the response token appears,
116
+ * and `onEscalation` fires on image-challenge / managed-variant / timeout.
117
+ *
118
+ * See `@mochi.js/challenges` for the full surface and the limits page
119
+ * for the v0.2 scope (visible-checkbox variants only — image/audio
120
+ * solving is v0.3+).
121
+ */
122
+ challenges?: ChallengeLaunchOptions;
123
+ }
124
+
125
+ /**
126
+ * Launch a Session: spawn Chromium with `--remote-debugging-pipe`, attach the
127
+ * CDP transport, and return a configured `Session`.
128
+ */
129
+ export async function launch(opts: LaunchOptions): Promise<Session> {
130
+ const binary = await resolveBinary(opts.binary);
131
+ const normalized = normalizeProxy(opts.proxy);
132
+ const proc = await spawnChromium({
133
+ binary,
134
+ extraArgs: opts.args,
135
+ headless: opts.headless ?? false,
136
+ // Chromium rejects inline auth on `--proxy-server`; pass the
137
+ // auth-stripped server URL.
138
+ ...(normalized !== undefined ? { proxy: normalized.server } : {}),
139
+ });
140
+
141
+ // Resolve the `MatrixV1` for this session via the consistency engine.
142
+ // Inline `ProfileV1` objects flow straight through; string profile ids
143
+ // are resolved against a placeholder profile until `@mochi.js/profiles`
144
+ // ships its first capture (phase 0.4). The matrix is bit-stable per
145
+ // `(profile, seed)` excluding the `derivedAt` timestamp.
146
+ const profile = resolveProfile(opts.profile);
147
+ const matrix = deriveMatrix(profile, opts.seed);
148
+
149
+ const session = new Session({
150
+ proc,
151
+ matrix,
152
+ seed: opts.seed,
153
+ ...(opts.timeout !== undefined ? { defaultTimeoutMs: opts.timeout } : {}),
154
+ ...(opts.bypassInject === true ? { bypassInject: true } : {}),
155
+ // Forward the same proxy (with auth, if any) to the net FFI so
156
+ // out-of-band Session.fetch traffic shares the apparent egress.
157
+ // `@mochi.js/net` (wreq) accepts the full `user:pass@host` URL form.
158
+ ...(normalized !== undefined ? { netProxy: normalized.netProxy } : {}),
159
+ ...(normalized?.auth !== undefined ? { proxyAuth: normalized.auth } : {}),
160
+ ...(opts.challenges !== undefined ? { challenges: opts.challenges } : {}),
161
+ });
162
+ return session;
163
+ }
164
+
165
+ /**
166
+ * The public namespace exposed via `import { mochi } from "@mochi.js/core"`.
167
+ */
168
+ export const mochi = {
169
+ /** Framework version. */
170
+ version: VERSION,
171
+ /** Launch a browser session. */
172
+ launch,
173
+ } as const;
174
+
175
+ export type Mochi = typeof mochi;
176
+
177
+ // ---- helpers ----------------------------------------------------------------
178
+
179
+ /**
180
+ * Reconcile the two `LaunchOptions.proxy` shapes (URL string and
181
+ * `ProxyConfig` record) into a single normalized record carrying:
182
+ * - `server`: auth-stripped URL safe to feed `--proxy-server=`.
183
+ * - `netProxy`: the URL handed to the network FFI. Preserves credentials
184
+ * (wreq accepts `user:pass@host` inline) so out-of-band fetches
185
+ * authenticate against the same proxy.
186
+ * - `auth`: parsed credentials for the CDP auth handler. Undefined when
187
+ * no creds were supplied.
188
+ *
189
+ * Returns `undefined` only when no proxy was configured at all.
190
+ */
191
+ function normalizeProxy(p: LaunchOptions["proxy"]):
192
+ | {
193
+ server: string;
194
+ netProxy: string;
195
+ auth?: { username: string; password: string };
196
+ }
197
+ | undefined {
198
+ if (p === undefined) return undefined;
199
+ if (typeof p === "string") {
200
+ if (p.length === 0) return undefined;
201
+ const parsed = parseProxyUrl(p);
202
+ return {
203
+ server: parsed.server,
204
+ netProxy: p,
205
+ ...(parsed.auth !== undefined ? { auth: parsed.auth } : {}),
206
+ };
207
+ }
208
+ // ProxyConfig form. `server` may itself include credentials; if so we
209
+ // strip them. Explicit username/password fields take precedence.
210
+ const parsed = parseProxyUrl(p.server);
211
+ const auth =
212
+ p.username !== undefined ? { username: p.username, password: p.password ?? "" } : parsed.auth;
213
+ // Reconstruct the netProxy URL preserving any explicit auth (wreq path).
214
+ const netProxy = auth !== undefined ? injectAuth(parsed.server, auth) : parsed.server;
215
+ return {
216
+ server: parsed.server,
217
+ netProxy,
218
+ ...(auth !== undefined ? { auth } : {}),
219
+ };
220
+ }
221
+
222
+ /**
223
+ * Inject `username:password@` into a server URL, percent-encoding both
224
+ * components so reserved characters round-trip cleanly through wreq's URL
225
+ * parser.
226
+ */
227
+ function injectAuth(server: string, auth: { username: string; password: string }): string {
228
+ const u = encodeURIComponent(auth.username);
229
+ const p = encodeURIComponent(auth.password);
230
+ // server is `<protocol>://<host>:<port>` (per parseProxyUrl).
231
+ const idx = server.indexOf("://");
232
+ if (idx < 0) return server;
233
+ const head = server.slice(0, idx + 3);
234
+ const tail = server.slice(idx + 3);
235
+ return `${head}${u}:${p}@${tail}`;
236
+ }
237
+
238
+ /**
239
+ * Resolve `LaunchOptions.profile` into a concrete `ProfileV1`. Inline
240
+ * profiles flow through unchanged. String profile ids — until
241
+ * `@mochi.js/profiles` ships (phase 0.4) — resolve to a generic placeholder
242
+ * stamped with the id; the consistency engine still produces a real,
243
+ * relationally-locked Matrix from it.
244
+ */
245
+ function resolveProfile(profile: ProfileId | ProfileV1): ProfileV1 {
246
+ if (typeof profile === "object") return profile;
247
+ return {
248
+ id: profile,
249
+ version: "0.0.0-placeholder",
250
+ engine: "chromium",
251
+ browser: { name: "chrome", channel: "stable", minVersion: "131", maxVersion: "133" },
252
+ os: { name: "linux", version: "22", arch: "x64" },
253
+ device: {
254
+ vendor: "generic",
255
+ model: "generic-x64",
256
+ cpuFamily: "intel-core-i7",
257
+ cores: 8,
258
+ memoryGB: 16,
259
+ },
260
+ display: { width: 1920, height: 1080, dpr: 1, colorDepth: 24, pixelDepth: 24 },
261
+ gpu: {
262
+ vendor: "Intel Inc.",
263
+ renderer: "Intel Iris Xe Graphics",
264
+ webglUnmaskedVendor: "Google Inc. (Intel Inc.)",
265
+ webglUnmaskedRenderer: "ANGLE (Intel Inc., Intel Iris Xe Graphics, OpenGL 4.1)",
266
+ webglMaxTextureSize: 16384,
267
+ webglMaxColorAttachments: 8,
268
+ webglExtensions: [],
269
+ },
270
+ audio: { contextSampleRate: 48000, audioWorkletLatency: 0.005, destinationMaxChannelCount: 2 },
271
+ fonts: { family: "linux-baseline", list: ["DejaVu Sans"] },
272
+ timezone: "UTC",
273
+ locale: "en-US",
274
+ languages: ["en-US", "en"],
275
+ behavior: { hand: "right", tremor: 0.18, wpm: 60, scrollStyle: "smooth" },
276
+ wreqPreset: "chrome_131_linux",
277
+ userAgent:
278
+ "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36",
279
+ uaCh: {},
280
+ entropyBudget: { fixed: [], perSeed: [] },
281
+ };
282
+ }