@mochi.js/core 0.3.0 → 0.8.0

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,110 @@
1
+ /**
2
+ * Auto-pick the host-OS-matching profile when `LaunchOptions.profile` is
3
+ * omitted. The function below is the pure decision table the
4
+ * launcher consults; tests stub `(platform, arch)` and assert the mapping
5
+ * without spinning a Chromium.
6
+ *
7
+ * ## Why
8
+ *
9
+ * Task 0271 documents the strategic thesis: spoofing Windows from a Linux
10
+ * server is the wrong default. Linux is a real-user signal, not a bot
11
+ * signal. WAFs trained on real traffic do not penalize Linux UAs because
12
+ * Linux desktops are massively overrepresented in high-LTV segments
13
+ * (developers, engineers, researchers). The signal was always
14
+ * `HeadlessChrome`, never Linux.
15
+ *
16
+ * Lifting host-OS-matching from "user types `profile: 'linux-chrome-stable'`
17
+ * by hand" into a default removes the entire class of "user accidentally
18
+ * spoofed Windows from a Linux DC and looked weird to the WAF" failures —
19
+ * the same argument that drove `detectLinuxServerEnv` for headless mode in
20
+ *
21
+ *
22
+ * ## Mapping
23
+ *
24
+ * The host pairs `(process.platform, process.arch)` we currently support:
25
+ *
26
+ * - `linux/x64` → `linux-chrome-stable`
27
+ * - `darwin/arm64` → `mac-m4-chrome-stable`
28
+ * - `darwin/x64` → `mac-chrome-stable`
29
+ * - `win32/x64` → `windows-chrome-stable`
30
+ *
31
+ * Everything else (linux/arm64, freebsd, alpine-musl detection, win32/arm64,
32
+ * etc.) returns `null` — the launcher then throws with a precise diagnostic
33
+ * listing the six explicit profile IDs and a pointer to the
34
+ * choose-your-profile guide. We never silently fall back to a placeholder.
35
+ *
36
+ * ## Caveat — darwin/x64
37
+ *
38
+ * The current profile catalog (`packages/profiles/data/`) ships
39
+ * `mac-chrome-stable` as a darwin/arm64 capture (its `os.arch === "arm64"`
40
+ * in `profile.json`). The mapping above still routes darwin/x64 to
41
+ * `mac-chrome-stable`; users on Intel Macs
42
+ * who want a strict arch match should pass `profile` explicitly until an
43
+ * `mac-intel-chrome-stable` capture lands.
44
+ *
45
+ */
46
+
47
+ import type { ProfileId } from "./launch";
48
+
49
+ /**
50
+ * Pure decision table: given the current host's `(process.platform,
51
+ * process.arch)` pair, return the profile id that best matches the host
52
+ * OS axis. Returns `null` for unsupported hosts so the launcher can throw
53
+ * with a precise diagnostic.
54
+ *
55
+ * No I/O, no logging — call sites can introspect the value cheaply (e.g.
56
+ * `console.log(mochi.defaultProfileForHost())`).
57
+ */
58
+ export function defaultProfileForHost(): ProfileId | null {
59
+ return resolveDefaultProfileForHost(process.platform, process.arch);
60
+ }
61
+
62
+ /**
63
+ * Internal pure resolver, exposed so the unit tests can drive the table
64
+ * without stubbing global `process`. Mirrors the precedence-table style of
65
+ * `resolveHeadlessMode`.
66
+ *
67
+ * @internal
68
+ */
69
+ export function resolveDefaultProfileForHost(
70
+ platform: NodeJS.Platform,
71
+ arch: string,
72
+ ): ProfileId | null {
73
+ if (platform === "linux" && arch === "x64") return "linux-chrome-stable";
74
+ if (platform === "darwin" && arch === "arm64") return "mac-m4-chrome-stable";
75
+ if (platform === "darwin" && arch === "x64") return "mac-chrome-stable";
76
+ if (platform === "win32" && arch === "x64") return "windows-chrome-stable";
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * The six real-device profile IDs that `defaultProfileForHost` can return,
82
+ * surfaced by the launcher's failure-mode diagnostic. Order matches the
83
+ * brief verbatim so the user-facing message is stable.
84
+ *
85
+ * @internal
86
+ */
87
+ export const EXPLICIT_PROFILE_IDS = [
88
+ "mac-m4-chrome-stable",
89
+ "mac-chrome-stable",
90
+ "mac-chrome-beta",
91
+ "windows-chrome-stable",
92
+ "linux-chrome-stable",
93
+ "mac-brave-stable",
94
+ ] as const satisfies readonly ProfileId[];
95
+
96
+ /**
97
+ * Build the precise diagnostic emitted when `profile` is omitted on an
98
+ * unsupported host. Format pinned — keep stable so docs +
99
+ * LLM-context blocks stay correct.
100
+ *
101
+ * @internal
102
+ */
103
+ export function unsupportedHostMessage(platform: NodeJS.Platform, arch: string): string {
104
+ const list = EXPLICIT_PROFILE_IDS.map((id) => ` - ${id}`).join("\n");
105
+ return (
106
+ `[mochi] launch: no profile supplied and no host-matching default for ` +
107
+ `platform=${platform} arch=${arch}. Pick one explicitly:\n${list}\n` +
108
+ `See https://mochijs.com/docs/guides/choose-your-profile for the decision aid.`
109
+ );
110
+ }
@@ -11,7 +11,6 @@
11
11
  * + en-US looks like every Tor user.
12
12
  *
13
13
  * @see PLAN.md §9 — relational consistency, IP/TZ/Locale axis
14
- * @see tasks/0262-ip-tz-locale-exit-consistency.md
15
14
  */
16
15
 
17
16
  import type { MatrixV1 } from "@mochi.js/consistency";
package/src/geo-probe.ts CHANGED
@@ -9,11 +9,13 @@
9
9
  * first half of the fix (the second half is {@link reconcileGeoConsistency}
10
10
  * in `launch.ts`).
11
11
  *
12
- * The probe issues a single GET through the same `wreq` preset the session
13
- * would use for user traffic, so the geolocation service sees the **same
14
- * JA4 / headers** as the actual page the probe doesn't itself become
15
- * detectable. The probe respects the `proxy` option; if unset, the probe
16
- * goes direct (which is fine the user's exit IP is the host's IP).
12
+ * Post-0.7 the probe rides Chromium itself same network stack as
13
+ * `page.goto`, same TLS / H2 / JA4 by definition. The default `ProbeFetch`
14
+ * adapter delegates to `globalThis.fetch` so {@link probeExitGeo} can run
15
+ * in a test runner without requiring a live Session; production callers
16
+ * inject a `Session.fetch`-backed adapter via `launch.ts`. The probe
17
+ * respects the `proxy` option as a diagnostic-only field — Chromium picks
18
+ * up `--proxy-server` from the launch flags directly.
17
19
  *
18
20
  * ### Endpoint registry (verified working 2026-05-09)
19
21
  *
@@ -34,14 +36,12 @@
34
36
  * mode (default `privacy-fallback`).
35
37
  *
36
38
  * **No cross-session caching** — proxy IPs rotate; stale cache is worse
37
- * than no cache. (`docs/limits.md` — task 0262.)
39
+ * than no cache. (`docs/limits.md` —)
38
40
  *
39
41
  * @see PLAN.md §9 (relational consistency — IP/TZ/Locale axis)
40
- * @see tasks/0262-ip-tz-locale-exit-consistency.md
41
42
  */
42
43
 
43
44
  import type { MatrixV1 } from "@mochi.js/consistency";
44
- import { fetch as netFetch } from "@mochi.js/net";
45
45
 
46
46
  /**
47
47
  * Normalised geolocation derived from one of the probe endpoints. The
@@ -429,22 +429,30 @@ const DEFAULT_PER_ENDPOINT_TIMEOUT_MS = 2000;
429
429
 
430
430
  /**
431
431
  * Injection seam for the underlying HTTP transport. Production uses
432
- * `@mochi.js/net`'s `fetch` (so the probe carries the same JA4/headers as
433
- * user traffic). Tests inject a stub.
432
+ * `Session.fetch` (so the probe carries the same JA4/headers as user
433
+ * traffic — it IS the browser). Tests inject a stub.
434
+ *
435
+ * The `proxy` field is forwarded for diagnostic purposes only; the
436
+ * actual proxy egress is wired at the Chromium `--proxy-server` flag.
434
437
  *
435
438
  * @internal
436
439
  */
437
440
  export type ProbeFetch = (
438
441
  url: string,
439
- init: { preset: string; proxy?: string; timeoutMs: number },
442
+ init: { proxy?: string; timeoutMs: number },
440
443
  ) => Promise<Response>;
441
444
 
442
445
  /** Options for {@link probeExitGeo}. */
443
446
  export interface ProbeOptions {
444
- /** Optional outbound proxy URL — `user:pass@host:port` form is fine. */
447
+ /** Optional outbound proxy URL — diagnostic-only post-0.7. */
445
448
  readonly proxy?: string;
446
- /** The matrix whose `wreqPreset` drives the TLS fingerprint of the probe. */
447
- readonly matrix: Pick<MatrixV1, "wreqPreset">;
449
+ /**
450
+ * The matrix is retained on the API surface for forward-compat (so any
451
+ * future per-locale endpoint shuffle can read from it) and to keep
452
+ * call sites stable across the 0.6 → 0.7 transition. The probe itself
453
+ * no longer reads any field — Chromium owns the TLS fingerprint.
454
+ */
455
+ readonly matrix?: Partial<MatrixV1>;
448
456
  /**
449
457
  * Override the default 4-attempt cap. Tests use 2 to keep wall-time low;
450
458
  * production sticks with 4.
@@ -454,7 +462,9 @@ export interface ProbeOptions {
454
462
  readonly perEndpointTimeoutMs?: number;
455
463
  /**
456
464
  * Inject a custom `fetch` transport (for tests). Defaults to
457
- * `@mochi.js/net`'s `fetch` so the probe shares the session's TLS preset.
465
+ * `globalThis.fetch` (test/standalone use) production calls override
466
+ * this with a `Session.fetch`-backed adapter so the probe rides the
467
+ * same Chromium network stack as user traffic.
458
468
  * @internal
459
469
  */
460
470
  readonly fetch?: ProbeFetch;
@@ -467,27 +477,23 @@ export interface ProbeOptions {
467
477
  }
468
478
 
469
479
  /**
470
- * Default `ProbeFetch` — issues the request through `@mochi.js/net`'s
471
- * one-shot `fetch` so the geo service sees the same JA4 as user traffic.
480
+ * Default `ProbeFetch` — falls back to `globalThis.fetch` so the probe
481
+ * works standalone in tests without a live Session. Production launch
482
+ * paths inject a `Session.fetch`-backed adapter so the probe shares
483
+ * Chromium's network stack (and therefore JA4) with user traffic.
472
484
  *
473
- * The per-call timeout is mapped onto the wreq `timeoutMs` field; we do
474
- * NOT also wrap with `AbortController` because Bun's `Response` from the
475
- * FFI path is synchronous and we want the wreq layer to own the timeout
476
- * (a layered `AbortController` would race with the Rust handle drop).
485
+ * The per-call timeout is enforced via `AbortController`.
477
486
  */
478
487
  const defaultFetch: ProbeFetch = (url, init) => {
479
- return Promise.resolve(
480
- netFetch(url, {
481
- preset: init.preset,
482
- ...(init.proxy !== undefined ? { proxy: init.proxy } : {}),
488
+ const ac = new AbortController();
489
+ const timer = setTimeout(() => ac.abort(), init.timeoutMs);
490
+ return globalThis
491
+ .fetch(url, {
483
492
  method: "GET",
484
493
  headers: { Accept: "application/json" },
485
- timeoutMs: init.timeoutMs,
486
- // Connect timeout matches the per-endpoint cap — a stuck SYN is the
487
- // dominant failure mode against rate-limited endpoints.
488
- connectTimeoutMs: init.timeoutMs,
489
- }),
490
- );
494
+ signal: ac.signal,
495
+ })
496
+ .finally(() => clearTimeout(timer));
491
497
  };
492
498
 
493
499
  /** Fisher-Yates shuffle — non-deterministic, Math.random-backed. */
@@ -570,7 +576,6 @@ export async function probeExitGeo(opts: ProbeOptions): Promise<ExitGeo | null>
570
576
  new Promise<Response>((resolve, reject) => {
571
577
  try {
572
578
  fetchFn(adapter.url, {
573
- preset: opts.matrix.wreqPreset,
574
579
  ...(opts.proxy !== undefined ? { proxy: opts.proxy } : {}),
575
580
  timeoutMs: perEndpointTimeoutMs,
576
581
  }).then(resolve, reject);
package/src/index.ts CHANGED
@@ -19,6 +19,13 @@ export {
19
19
  type SendOptions,
20
20
  type Unsubscribe,
21
21
  } from "./cdp/router";
22
+ // Auto-pick host-OS-matching profile when `LaunchOptions.profile` is omitted
23
+ // (— paired with the strategic thesis in task 0271).
24
+ export {
25
+ defaultProfileForHost,
26
+ EXPLICIT_PROFILE_IDS,
27
+ resolveDefaultProfileForHost,
28
+ } from "./default-profile";
22
29
  // Error surface.
23
30
  export { NotImplementedError } from "./errors";
24
31
  // Exit-IP / TZ / locale reconciliation (task 0262, PLAN.md §9).
@@ -40,16 +47,33 @@ export {
40
47
  mochi,
41
48
  type ProfileId,
42
49
  type ProxyConfig,
50
+ resolveHeadlessMode,
43
51
  } from "./launch";
52
+ // Linux-server environment detection. Pure helpers for users who want to
53
+ // introspect what mochi inferred (and override `headlessMode` from there).
54
+ // Task 0258 — `mochi.detectLinuxServerEnv()` calls `probeLinuxServerEnv`.
55
+ export {
56
+ detectLinuxServerEnv,
57
+ type LinuxServerEnv,
58
+ type LinuxServerProbes,
59
+ probeLinuxServerEnv,
60
+ snapshotProbes,
61
+ } from "./linux-server";
44
62
  export {
63
+ ALL_BROWSER_PERMISSIONS,
64
+ type BrowserPermission,
45
65
  type Cookie,
66
+ type DomStorage,
67
+ type DomStorageOptions,
46
68
  type GotoOptions,
69
+ type GrantAllPermissionsOptions,
47
70
  type HumanClickOptions,
48
71
  type HumanMoveOptions,
49
72
  type HumanScrollOptions,
50
73
  type HumanTypeOptions,
51
74
  Page,
52
75
  type PageInit,
76
+ type ScreenshotOptions,
53
77
  type WaitForOptions,
54
78
  type WaitState,
55
79
  type WaitUntil,
@@ -58,5 +82,13 @@ export { ElementHandle, type ElementHandleInit } from "./page/element-handle";
58
82
  // Proxy URL parsing — exported so tests + downstream tools can normalize
59
83
  // proxy strings without going through `launch()`.
60
84
  export { type ParsedProxy, parseProxyUrl } from "./proxy-auth";
61
- export { Session, type SessionInit, type StorageSnapshot } from "./session";
85
+ export {
86
+ COOKIE_JAR_FORMAT_VERSION,
87
+ type CookieJar,
88
+ type CookieJarFile,
89
+ type CookieJarOptions,
90
+ Session,
91
+ type SessionInit,
92
+ type StorageSnapshot,
93
+ } from "./session";
62
94
  export { VERSION } from "./version";