@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.
- package/README.md +3 -3
- package/package.json +11 -4
- package/src/__tests__/behavioral.e2e.test.ts +200 -0
- package/src/__tests__/binary.test.ts +89 -0
- package/src/__tests__/forbidden.test.ts +80 -0
- package/src/__tests__/framer.test.ts +92 -0
- package/src/__tests__/inject.e2e.test.ts +253 -0
- package/src/__tests__/inject.test.ts +276 -0
- package/src/__tests__/integration.e2e.test.ts +60 -0
- package/src/__tests__/proxy-auth.test.ts +253 -0
- package/src/__tests__/router.test.ts +193 -0
- package/src/__tests__/smoke.test.ts +11 -5
- package/src/binary.ts +129 -0
- package/src/cdp/forbidden.ts +102 -0
- package/src/cdp/framer.ts +79 -0
- package/src/cdp/router.ts +240 -0
- package/src/cdp/transport.ts +167 -0
- package/src/cdp/types.ts +152 -0
- package/src/errors.ts +23 -0
- package/src/index.ts +46 -39
- package/src/launch.ts +282 -0
- package/src/page.ts +979 -0
- package/src/proc.ts +213 -0
- package/src/proxy-auth.ts +252 -0
- package/src/session.ts +638 -0
- package/src/version.ts +2 -0
package/src/cdp/types.ts
ADDED
|
@@ -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
|
-
*
|
|
2
|
+
* `@mochi.js/core` — the Bun-native browser automation framework.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
9
|
+
* @see https://github.com/0xchasercat/mochi
|
|
9
10
|
*/
|
|
10
11
|
|
|
11
|
-
export
|
|
12
|
-
|
|
13
|
-
export
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
+
}
|