@mochi.js/core 0.6.0 → 0.8.1
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 +2 -2
- package/package.json +6 -6
- package/src/__tests__/cookies-jar.test.ts +2 -3
- package/src/__tests__/default-profile.test.ts +6 -8
- package/src/__tests__/dx-cluster.e2e.test.ts +1 -2
- package/src/__tests__/geo-consistency.test.ts +0 -1
- package/src/__tests__/geo-probe.test.ts +13 -13
- package/src/__tests__/init-injector.e2e.test.ts +0 -1
- package/src/__tests__/init-injector.test.ts +1 -2
- package/src/__tests__/inject.test.ts +1 -2
- package/src/__tests__/page-dx-cluster.test.ts +3 -4
- package/src/__tests__/piercing.test.ts +1 -1
- package/src/__tests__/proc-linux-server.test.ts +4 -4
- package/src/__tests__/proc.test.ts +3 -3
- package/src/__tests__/proxy-auth.test.ts +1 -2
- package/src/__tests__/screenshot.e2e.test.ts +1 -1
- package/src/__tests__/screenshot.test.ts +1 -1
- package/src/__tests__/window-size.e2e.test.ts +0 -1
- package/src/cdp/types.ts +0 -1
- package/src/default-profile.ts +6 -8
- package/src/geo-consistency.ts +0 -1
- package/src/geo-probe.ts +37 -32
- package/src/index.ts +1 -1
- package/src/launch.ts +120 -72
- package/src/page/element-handle.ts +0 -1
- package/src/page/piercing.ts +0 -1
- package/src/page/selector.ts +0 -1
- package/src/page.ts +31 -14
- package/src/proc.ts +5 -6
- package/src/proxy-auth.ts +1 -3
- package/src/session.ts +489 -124
- package/src/version.ts +1 -1
package/src/launch.ts
CHANGED
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import { deriveMatrix, type ProfileV1 } from "@mochi.js/consistency";
|
|
14
|
+
import { getProfile, ProfileBaselineMissingError, UnknownProfileIdError } from "@mochi.js/profiles";
|
|
14
15
|
import { resolveBinary } from "./binary";
|
|
15
16
|
import { defaultProfileForHost, unsupportedHostMessage } from "./default-profile";
|
|
16
17
|
import { type GeoConsistencyMode, reconcileGeoConsistency } from "./geo-consistency";
|
|
@@ -92,7 +93,7 @@ export interface LaunchOptions {
|
|
|
92
93
|
* string (looked up against `KNOWN_PROFILE_IDS`) or an inline `ProfileV1`
|
|
93
94
|
* object.
|
|
94
95
|
*
|
|
95
|
-
* **Optional
|
|
96
|
+
* **Optional** — when omitted, mochi auto-picks the
|
|
96
97
|
* profile whose declared OS matches the host's `process.platform` /
|
|
97
98
|
* `process.arch` pair via {@link defaultProfileForHost}:
|
|
98
99
|
*
|
|
@@ -120,7 +121,7 @@ export interface LaunchOptions {
|
|
|
120
121
|
* `false` (default in v0.1) runs headful. New code should prefer
|
|
121
122
|
* {@link headlessMode}, which is more expressive AND env-aware.
|
|
122
123
|
*
|
|
123
|
-
* Resolution priority
|
|
124
|
+
* Resolution priority:
|
|
124
125
|
*
|
|
125
126
|
* 1. `headlessMode` if set.
|
|
126
127
|
* 2. Else `headless: true → "new"`, `headless: false → "off"`.
|
|
@@ -130,7 +131,7 @@ export interface LaunchOptions {
|
|
|
130
131
|
*/
|
|
131
132
|
headless?: boolean;
|
|
132
133
|
/**
|
|
133
|
-
* Headless dispatch mode
|
|
134
|
+
* Headless dispatch mode. One of:
|
|
134
135
|
*
|
|
135
136
|
* - `"new"` — modern Chromium headless (`--headless=new`). Full
|
|
136
137
|
* rendering, near-byte-identical to headful for
|
|
@@ -178,7 +179,7 @@ export interface LaunchOptions {
|
|
|
178
179
|
* trivially fingerprinted as Chromium-for-Testing.
|
|
179
180
|
*
|
|
180
181
|
* Defaults to `false`. PLAN.md §12.1 (capture must run against bare
|
|
181
|
-
* Chromium);
|
|
182
|
+
* Chromium);
|
|
182
183
|
*/
|
|
183
184
|
bypassInject?: boolean;
|
|
184
185
|
/**
|
|
@@ -199,7 +200,7 @@ export interface LaunchOptions {
|
|
|
199
200
|
*
|
|
200
201
|
* Pairs with — but is independent of — {@link bypassInject}. Capture
|
|
201
202
|
* flows set both `true`; harness conformance runs set `hermetic: true`
|
|
202
|
-
* with full inject pipeline active. PLAN.md §8.6 +
|
|
203
|
+
* with full inject pipeline active. PLAN.md §8.6 +
|
|
203
204
|
*/
|
|
204
205
|
hermetic?: boolean;
|
|
205
206
|
/**
|
|
@@ -235,13 +236,13 @@ export interface LaunchOptions {
|
|
|
235
236
|
* - `"off"` — skip the probe entirely. Use in offline tests / when
|
|
236
237
|
* the probe service is rate-limited.
|
|
237
238
|
*
|
|
238
|
-
* The probe is a single GET through
|
|
239
|
-
* `
|
|
240
|
-
* traffic
|
|
241
|
-
* cached across sessions — proxy IPs
|
|
239
|
+
* The probe is a single GET through Chromium itself (Session.fetch via
|
|
240
|
+
* CDP `Network.loadNetworkResource`), so the geo service sees the same
|
|
241
|
+
* JA4 / headers as user traffic by definition. 4-attempt cap, 2s per
|
|
242
|
+
* endpoint. Probe results are NOT cached across sessions — proxy IPs
|
|
243
|
+
* rotate.
|
|
242
244
|
*
|
|
243
245
|
* @see PLAN.md §9 (relational consistency, IP/TZ/Locale axis)
|
|
244
|
-
* @see tasks/0262-ip-tz-locale-exit-consistency.md
|
|
245
246
|
*/
|
|
246
247
|
geoConsistency?: GeoConsistencyMode;
|
|
247
248
|
}
|
|
@@ -255,27 +256,29 @@ export async function launch(opts: LaunchOptions): Promise<Session> {
|
|
|
255
256
|
const normalized = normalizeProxy(opts.proxy);
|
|
256
257
|
|
|
257
258
|
// Resolve the `MatrixV1` BEFORE spawning so matrix-derived values flow
|
|
258
|
-
// into both the `--lang` flag
|
|
259
|
-
|
|
259
|
+
// into both the `--lang` flag and `--window-size` flag
|
|
260
|
+
//. The matrix is otherwise read post-spawn for inject;
|
|
260
261
|
// deriving early is cheap (~µs, pure function) and lets us close the
|
|
261
262
|
// I-5 leaks between Chromium's native network/OS-window state and the
|
|
262
263
|
// JS-layer spoof.
|
|
263
264
|
//
|
|
264
265
|
// Inline `ProfileV1` objects flow straight through; string profile ids
|
|
265
|
-
//
|
|
266
|
-
//
|
|
267
|
-
//
|
|
266
|
+
// resolve to the captured `data/<id>/profile.json` baseline shipped by
|
|
267
|
+
// `@mochi.js/profiles`. When the catalog declares an id but no captured
|
|
268
|
+
// baseline ships yet (e.g. `mac-m2-chrome-stable`), we fall back to a
|
|
269
|
+
// synthesized placeholder so the launch still succeeds. The matrix is
|
|
270
|
+
// bit-stable per `(profile, seed)` excluding the `derivedAt` timestamp.
|
|
268
271
|
//
|
|
269
272
|
// Task 0272 — when `profile` is omitted, auto-pick the host-OS-matching
|
|
270
273
|
// profile id. Throws with a precise diagnostic if the host is one of the
|
|
271
274
|
// unsupported ones (FreeBSD, Linux arm64 today, Windows arm64, Alpine
|
|
272
275
|
// musl). Explicit `profile:` always wins; the auto-pick never overrides.
|
|
273
|
-
const profileSource = resolveProfileSource(opts.profile);
|
|
276
|
+
const profileSource = await resolveProfileSource(opts.profile);
|
|
274
277
|
const matrix = deriveMatrix(profileSource.profile, opts.seed);
|
|
275
278
|
if (profileSource.autoPicked) {
|
|
276
279
|
// One info-level log line so users can see what mochi inferred without
|
|
277
280
|
// calling `defaultProfileForHost()` themselves. Wording is pinned by
|
|
278
|
-
//
|
|
281
|
+
// — keep stable so docs + LLM-context blocks stay correct.
|
|
279
282
|
// (Routed through `console.warn` to match the existing diagnostic
|
|
280
283
|
// channel for `geoConsistency` / Linux-server inference; `console.info`
|
|
281
284
|
// is gated by the workspace lint config — `noConsole` only allows
|
|
@@ -289,9 +292,10 @@ export async function launch(opts: LaunchOptions): Promise<Session> {
|
|
|
289
292
|
|
|
290
293
|
// Task 0262 — exit-IP / TZ / locale reconciliation.
|
|
291
294
|
//
|
|
292
|
-
// Probe the apparent exit IP through the configured proxy
|
|
293
|
-
//
|
|
294
|
-
//
|
|
295
|
+
// Probe the apparent exit IP through the configured proxy. Post-0.7
|
|
296
|
+
// the probe runs through Chromium itself (Session.fetch via CDP
|
|
297
|
+
// `Network.loadNetworkResource`), so the geo service sees the same
|
|
298
|
+
// JA4 / headers as user traffic by definition. Cross-reference against
|
|
295
299
|
// `(matrix.timezone, matrix.locale)` and apply `geoConsistency`. The
|
|
296
300
|
// adjusted matrix flows into BOTH `spawnChromium` (so `--lang` reflects
|
|
297
301
|
// any override) AND `Session` (so inject + the CDP `Emulation.set
|
|
@@ -303,7 +307,7 @@ export async function launch(opts: LaunchOptions): Promise<Session> {
|
|
|
303
307
|
let adjustedMatrix = matrix;
|
|
304
308
|
if (geoMode !== "off") {
|
|
305
309
|
const geo = await probeExitGeo({
|
|
306
|
-
...(normalized?.
|
|
310
|
+
...(normalized?.proxy !== undefined ? { proxy: normalized.proxy } : {}),
|
|
307
311
|
matrix,
|
|
308
312
|
});
|
|
309
313
|
// Strict mode throws GeoMismatchError on real mismatch; let it
|
|
@@ -359,13 +363,13 @@ export async function launch(opts: LaunchOptions): Promise<Session> {
|
|
|
359
363
|
// multi-locale list still flows through `matrix.languages` to the
|
|
360
364
|
// inject layer's `navigator.languages` spoof; Chromium derives the
|
|
361
365
|
// q-weighted `Accept-Language` value from the single `--lang` primary
|
|
362
|
-
// automatically.
|
|
366
|
+
// automatically.
|
|
363
367
|
locale: adjustedMatrix.locale,
|
|
364
368
|
// Pin OS-level outer window from the matrix's display geometry so
|
|
365
369
|
// `window.outerWidth/outerHeight` (which reads from the OS window,
|
|
366
370
|
// NOT the JS-spoofed `screen.*`) matches the spoof. Closes the
|
|
367
371
|
// `fingerprint-scan.com` 800×600 leak under `--headless=new`.
|
|
368
|
-
// UDC fixes the same issue at `__init__.py:410-411`.
|
|
372
|
+
// UDC fixes the same issue at `__init__.py:410-411`.
|
|
369
373
|
//
|
|
370
374
|
// (`adjustedMatrix.display` === `matrix.display` since geo reconcile
|
|
371
375
|
// only touches timezone/locale/languages — but we use the adjusted
|
|
@@ -395,10 +399,11 @@ export async function launch(opts: LaunchOptions): Promise<Session> {
|
|
|
395
399
|
seed: opts.seed,
|
|
396
400
|
...(opts.timeout !== undefined ? { defaultTimeoutMs: opts.timeout } : {}),
|
|
397
401
|
...(opts.bypassInject === true ? { bypassInject: true } : {}),
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
|
|
402
|
+
// Proxy auth is the only piece that needs explicit Session-side
|
|
403
|
+
// wiring (the `--proxy-server` flag is already on Chromium's command
|
|
404
|
+
// line above). Out-of-band `Session.fetch` traffic rides Chromium's
|
|
405
|
+
// network stack post-0.7, so it inherits the `--proxy-server` egress
|
|
406
|
+
// automatically — no per-call proxy URL needed.
|
|
402
407
|
...(normalized?.auth !== undefined ? { proxyAuth: normalized.auth } : {}),
|
|
403
408
|
...(opts.challenges !== undefined ? { challenges: opts.challenges } : {}),
|
|
404
409
|
});
|
|
@@ -417,16 +422,15 @@ export const mochi = {
|
|
|
417
422
|
* Inspect what mochi would infer about the current process environment for
|
|
418
423
|
* Linux-server detection (drives `headlessMode` defaulting). Pure read of
|
|
419
424
|
* `process.platform`, `process.env.DISPLAY`, `process.env.WAYLAND_DISPLAY`,
|
|
420
|
-
* `process.getuid?.()`, and the container probe paths.
|
|
425
|
+
* `process.getuid?.()`, and the container probe paths.
|
|
421
426
|
*/
|
|
422
427
|
detectLinuxServerEnv: probeLinuxServerEnv,
|
|
423
428
|
/**
|
|
424
429
|
* Inspect which profile id `mochi.launch` would auto-pick on the current
|
|
425
430
|
* host when `profile` is omitted. Pure read of `process.platform` /
|
|
426
431
|
* `process.arch`. Returns `null` on unsupported hosts — the launcher
|
|
427
|
-
* throws on that path with a list of explicit profile IDs.
|
|
432
|
+
* throws on that path with a list of explicit profile IDs.
|
|
428
433
|
*
|
|
429
|
-
* @see tasks/0271-the-linux-os-thesis.md — the strategic thesis
|
|
430
434
|
* @see https://mochijs.com/docs/concepts/stealth-philosophy
|
|
431
435
|
*/
|
|
432
436
|
defaultProfileForHost,
|
|
@@ -463,9 +467,10 @@ export function resolveHeadlessMode(
|
|
|
463
467
|
* Reconcile the two `LaunchOptions.proxy` shapes (URL string and
|
|
464
468
|
* `ProxyConfig` record) into a single normalized record carrying:
|
|
465
469
|
* - `server`: auth-stripped URL safe to feed `--proxy-server=`.
|
|
466
|
-
* - `
|
|
467
|
-
*
|
|
468
|
-
*
|
|
470
|
+
* - `proxy`: the auth-stripped URL forwarded to the geo-probe so it
|
|
471
|
+
* can record the egress on diagnostics. (Kept for API parity even
|
|
472
|
+
* though the probe now rides Session.fetch + Chromium's network
|
|
473
|
+
* stack — i.e. picks up `--proxy-server` automatically.)
|
|
469
474
|
* - `auth`: parsed credentials for the CDP auth handler. Undefined when
|
|
470
475
|
* no creds were supplied.
|
|
471
476
|
*
|
|
@@ -474,7 +479,7 @@ export function resolveHeadlessMode(
|
|
|
474
479
|
function normalizeProxy(p: LaunchOptions["proxy"]):
|
|
475
480
|
| {
|
|
476
481
|
server: string;
|
|
477
|
-
|
|
482
|
+
proxy: string;
|
|
478
483
|
auth?: { username: string; password: string };
|
|
479
484
|
}
|
|
480
485
|
| undefined {
|
|
@@ -484,7 +489,7 @@ function normalizeProxy(p: LaunchOptions["proxy"]):
|
|
|
484
489
|
const parsed = parseProxyUrl(p);
|
|
485
490
|
return {
|
|
486
491
|
server: parsed.server,
|
|
487
|
-
|
|
492
|
+
proxy: parsed.server,
|
|
488
493
|
...(parsed.auth !== undefined ? { auth: parsed.auth } : {}),
|
|
489
494
|
};
|
|
490
495
|
}
|
|
@@ -493,31 +498,13 @@ function normalizeProxy(p: LaunchOptions["proxy"]):
|
|
|
493
498
|
const parsed = parseProxyUrl(p.server);
|
|
494
499
|
const auth =
|
|
495
500
|
p.username !== undefined ? { username: p.username, password: p.password ?? "" } : parsed.auth;
|
|
496
|
-
// Reconstruct the netProxy URL preserving any explicit auth (wreq path).
|
|
497
|
-
const netProxy = auth !== undefined ? injectAuth(parsed.server, auth) : parsed.server;
|
|
498
501
|
return {
|
|
499
502
|
server: parsed.server,
|
|
500
|
-
|
|
503
|
+
proxy: parsed.server,
|
|
501
504
|
...(auth !== undefined ? { auth } : {}),
|
|
502
505
|
};
|
|
503
506
|
}
|
|
504
507
|
|
|
505
|
-
/**
|
|
506
|
-
* Inject `username:password@` into a server URL, percent-encoding both
|
|
507
|
-
* components so reserved characters round-trip cleanly through wreq's URL
|
|
508
|
-
* parser.
|
|
509
|
-
*/
|
|
510
|
-
function injectAuth(server: string, auth: { username: string; password: string }): string {
|
|
511
|
-
const u = encodeURIComponent(auth.username);
|
|
512
|
-
const p = encodeURIComponent(auth.password);
|
|
513
|
-
// server is `<protocol>://<host>:<port>` (per parseProxyUrl).
|
|
514
|
-
const idx = server.indexOf("://");
|
|
515
|
-
if (idx < 0) return server;
|
|
516
|
-
const head = server.slice(0, idx + 3);
|
|
517
|
-
const tail = server.slice(idx + 3);
|
|
518
|
-
return `${head}${u}:${p}@${tail}`;
|
|
519
|
-
}
|
|
520
|
-
|
|
521
508
|
/**
|
|
522
509
|
* Resolve `LaunchOptions.profile` into a concrete `ProfileV1` plus the
|
|
523
510
|
* meta-flag the launcher needs to decide whether to log the auto-pick
|
|
@@ -525,55 +512,112 @@ function injectAuth(server: string, auth: { username: string; password: string }
|
|
|
525
512
|
*
|
|
526
513
|
* 1. Explicit `ProfileV1` object — flows through unchanged. `autoPicked`
|
|
527
514
|
* false; `id` taken from the inline object.
|
|
528
|
-
* 2. Explicit `ProfileId` string —
|
|
529
|
-
*
|
|
515
|
+
* 2. Explicit `ProfileId` string — load the captured baseline from
|
|
516
|
+
* `@mochi.js/profiles`. If the id is known to the catalog but no
|
|
517
|
+
* captured baseline ships, fall back to a placeholder synthesis so
|
|
518
|
+
* the launch still succeeds (and the consistency engine still locks
|
|
519
|
+
* a relationally-consistent Matrix from the skeleton). Unknown ids
|
|
520
|
+
* propagate as a hard error. `autoPicked` false.
|
|
530
521
|
* 3. `undefined` — task 0272: call `defaultProfileForHost()`. Throw with
|
|
531
522
|
* the unsupported-host diagnostic when the resolver returns `null`.
|
|
532
|
-
* `autoPicked` true
|
|
523
|
+
* `autoPicked` true; same captured-vs-placeholder fallback as branch
|
|
524
|
+
* 2.
|
|
533
525
|
*
|
|
534
|
-
*
|
|
535
|
-
*
|
|
536
|
-
*
|
|
526
|
+
* Async because `getProfile` reads `data/<id>/profile.json` from disk via
|
|
527
|
+
* `Bun.file().json()`. The launcher does not log here — the INFO line for
|
|
528
|
+
* `autoPicked === true` is emitted at the call-site so test fixtures can
|
|
529
|
+
* assert the resolution without intercepting `console`.
|
|
537
530
|
*/
|
|
538
|
-
function resolveProfileSource(profile: ProfileId | ProfileV1 | undefined): {
|
|
531
|
+
async function resolveProfileSource(profile: ProfileId | ProfileV1 | undefined): Promise<{
|
|
539
532
|
profile: ProfileV1;
|
|
540
533
|
id: ProfileId;
|
|
541
534
|
autoPicked: boolean;
|
|
542
|
-
} {
|
|
535
|
+
}> {
|
|
543
536
|
if (typeof profile === "object") {
|
|
544
537
|
return { profile, id: profile.id, autoPicked: false };
|
|
545
538
|
}
|
|
546
539
|
if (typeof profile === "string") {
|
|
547
540
|
return {
|
|
548
|
-
profile:
|
|
541
|
+
profile: await loadProfileWithFallback(profile),
|
|
549
542
|
id: profile,
|
|
550
543
|
autoPicked: false,
|
|
551
544
|
};
|
|
552
545
|
}
|
|
553
|
-
// Auto-pick branch —
|
|
546
|
+
// Auto-pick branch —
|
|
554
547
|
const picked = defaultProfileForHost();
|
|
555
548
|
if (picked === null) {
|
|
556
549
|
throw new Error(unsupportedHostMessage(process.platform, process.arch));
|
|
557
550
|
}
|
|
558
551
|
return {
|
|
559
|
-
profile:
|
|
552
|
+
profile: await loadProfileWithFallback(picked),
|
|
560
553
|
id: picked,
|
|
561
554
|
autoPicked: true,
|
|
562
555
|
};
|
|
563
556
|
}
|
|
564
557
|
|
|
565
558
|
/**
|
|
566
|
-
*
|
|
567
|
-
*
|
|
568
|
-
*
|
|
569
|
-
*
|
|
559
|
+
* Load a `ProfileV1` for `id` from `@mochi.js/profiles` if a captured
|
|
560
|
+
* baseline ships, otherwise synthesize a placeholder. Unknown ids also fall
|
|
561
|
+
* back to the placeholder (with a console.warn) — preserving the
|
|
562
|
+
* pre-getProfile() contract that any string id produces a working session.
|
|
563
|
+
* E2E test fixtures rely on synthetic ids like "test-humanize".
|
|
564
|
+
*
|
|
565
|
+
* Critical correctness path: the captured baselines pin tip-of-stable Chrome
|
|
566
|
+
* majors (147+ as of 2026-05). The pre-fix code path called
|
|
567
|
+
* `synthesizePlaceholderProfile` for every string id, which hardcoded
|
|
568
|
+
* Chrome 131 and produced a UA mismatch with the actual Chromium-for-Testing
|
|
569
|
+
* binary.
|
|
570
|
+
*/
|
|
571
|
+
async function loadProfileWithFallback(id: ProfileId): Promise<ProfileV1> {
|
|
572
|
+
try {
|
|
573
|
+
// `ProfileId` here is the loose `string` alias the launcher accepts
|
|
574
|
+
// (see comment near the type definition). `getProfile` narrows it
|
|
575
|
+
// back to the catalog union at runtime and throws
|
|
576
|
+
// `UnknownProfileIdError` for ids outside the catalog.
|
|
577
|
+
return await getProfile(id as Parameters<typeof getProfile>[0]);
|
|
578
|
+
} catch (err) {
|
|
579
|
+
if (err instanceof ProfileBaselineMissingError) {
|
|
580
|
+
// Known catalog entry, no baseline shipped yet — fall back to the
|
|
581
|
+
// synthesized placeholder so the launch still succeeds.
|
|
582
|
+
return synthesizePlaceholderProfile(id);
|
|
583
|
+
}
|
|
584
|
+
if (err instanceof UnknownProfileIdError) {
|
|
585
|
+
// Caller passed an id that isn't in `KNOWN_PROFILE_IDS`. Surface a
|
|
586
|
+
// warning so typos are visible, but fall back to the placeholder so
|
|
587
|
+
// synthetic test-fixture ids (e.g. "test-humanize") keep working.
|
|
588
|
+
// biome-ignore lint/suspicious/noConsole: dev-facing diagnostic
|
|
589
|
+
console.warn(
|
|
590
|
+
`[mochi] profile id "${id}" is not in @mochi.js/profiles.KNOWN_PROFILE_IDS; ` +
|
|
591
|
+
"falling back to a synthesized placeholder. Pass a ProfileV1 object directly " +
|
|
592
|
+
"or use one of the catalog ids to silence this warning.",
|
|
593
|
+
);
|
|
594
|
+
return synthesizePlaceholderProfile(id);
|
|
595
|
+
}
|
|
596
|
+
throw err;
|
|
597
|
+
}
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
/**
|
|
601
|
+
* Synthesize a generic placeholder `ProfileV1` from a profile id, used as
|
|
602
|
+
* a fallback when the catalog declares an id but no captured baseline
|
|
603
|
+
* ships in `@mochi.js/profiles` yet. The consistency engine still produces
|
|
604
|
+
* a real, relationally-locked Matrix from this skeleton — the id is what
|
|
605
|
+
* flows into `sha256(profile.id + seed)`.
|
|
606
|
+
*
|
|
607
|
+
* The major version pinned here MUST track the live Chromium-for-Testing
|
|
608
|
+
* pin (`packages/cli/src/browsers/manifest.ts:PINNED_FALLBACK_VERSION`)
|
|
609
|
+
* and the tip entry in
|
|
610
|
+
* `packages/consistency/src/rules/lookups/browser.ts:BROWSER_TIP_FULL_VERSION`.
|
|
611
|
+
* A drift between these surfaces ships a UA whose major doesn't match the
|
|
612
|
+
* installed binary — the canonical fingerprint-mismatch bug R-004 was
|
|
613
|
+
* meant to prevent. Bump all three together.
|
|
570
614
|
*/
|
|
571
615
|
function synthesizePlaceholderProfile(profile: ProfileId): ProfileV1 {
|
|
572
616
|
return {
|
|
573
617
|
id: profile,
|
|
574
618
|
version: "0.0.0-placeholder",
|
|
575
619
|
engine: "chromium",
|
|
576
|
-
browser: { name: "chrome", channel: "stable", minVersion: "
|
|
620
|
+
browser: { name: "chrome", channel: "stable", minVersion: "148", maxVersion: "148" },
|
|
577
621
|
os: { name: "linux", version: "22", arch: "x64" },
|
|
578
622
|
device: {
|
|
579
623
|
vendor: "generic",
|
|
@@ -598,9 +642,13 @@ function synthesizePlaceholderProfile(profile: ProfileId): ProfileV1 {
|
|
|
598
642
|
locale: "en-US",
|
|
599
643
|
languages: ["en-US", "en"],
|
|
600
644
|
behavior: { hand: "right", tremor: 0.18, wpm: 60, scrollStyle: "smooth" },
|
|
601
|
-
wreqPreset
|
|
645
|
+
// `wreqPreset` is required by the ProfileV1 schema for one release of
|
|
646
|
+
// back-compat (see `schemas/profile.schema.json`). The runtime no
|
|
647
|
+
// longer reads it — `Session.fetch` rides Chromium's network stack via
|
|
648
|
+
// CDP, so JA4 is real Chrome by definition. Drops in 0.8.
|
|
649
|
+
wreqPreset: "chrome_148_linux",
|
|
602
650
|
userAgent:
|
|
603
|
-
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
651
|
+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/148.0.0.0 Safari/537.36",
|
|
604
652
|
uaCh: {},
|
|
605
653
|
entropyBudget: { fixed: [], perSeed: [] },
|
|
606
654
|
};
|
package/src/page/piercing.ts
CHANGED
package/src/page/selector.ts
CHANGED
|
@@ -31,7 +31,6 @@
|
|
|
31
31
|
* Throws `SelectorParseError` on syntactically invalid input. The matcher
|
|
32
32
|
* itself never throws — unsupported nodes just don't match.
|
|
33
33
|
*
|
|
34
|
-
* @see tasks/0253-closed-shadow-piercing-locator.md
|
|
35
34
|
* @see PLAN.md §8.2 (forbidden CDP — neither `DOM.getDocument` nor
|
|
36
35
|
* `DOM.resolveNode` is forbidden; both fine).
|
|
37
36
|
*/
|
package/src/page.ts
CHANGED
|
@@ -202,7 +202,7 @@ export interface ScreenshotOptions {
|
|
|
202
202
|
encoding?: "binary" | "base64";
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
// ---- DX cluster: DOM storage + permissions
|
|
205
|
+
// ---- DX cluster: DOM storage + permissions ---------------------
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
208
|
* Options for {@link Page.localStorage} / {@link Page.sessionStorage}
|
|
@@ -252,43 +252,61 @@ export interface DomStorage {
|
|
|
252
252
|
/**
|
|
253
253
|
* Every browser-level permission descriptor `Browser.grantPermissions` accepts.
|
|
254
254
|
*
|
|
255
|
-
* Pinned to the CDP `Browser.PermissionType` enum on Chromium
|
|
256
|
-
*
|
|
257
|
-
* The list is verbose-on-purpose: we want a contract test
|
|
258
|
-
* Chromium adds a new permission so we can decide
|
|
255
|
+
* Pinned to the CDP `Browser.PermissionType` enum on Chromium 148 (the
|
|
256
|
+
* post-0.7 profile floor — verified `2026-05-09` against the live CDP
|
|
257
|
+
* reference). The list is verbose-on-purpose: we want a contract test
|
|
258
|
+
* to catch the day Chromium adds a new permission so we can decide
|
|
259
|
+
* whether to forward it.
|
|
260
|
+
*
|
|
261
|
+
* Drift history:
|
|
262
|
+
* - 0.7: removed `accessibilityEvents`, `captureHandle`, `flash`,
|
|
263
|
+
* `videoCapturePanTiltZoom` (gone or renamed in 148). Added the XR
|
|
264
|
+
* cluster (`ar`, `vr`, `handTracking`), `automaticFullscreen`,
|
|
265
|
+
* `cameraPanTiltZoom`, `capturedSurfaceControl`, `keyboardLock`,
|
|
266
|
+
* `pointerLock`, `localNetwork`, `localNetworkAccess`,
|
|
267
|
+
* `loopbackNetwork`, `smartCard`, `webPrinting`.
|
|
259
268
|
*
|
|
260
269
|
* @see https://chromedevtools.github.io/devtools-protocol/tot/Browser/#type-PermissionType
|
|
261
270
|
*/
|
|
262
271
|
export const ALL_BROWSER_PERMISSIONS = [
|
|
263
|
-
"
|
|
272
|
+
"ar",
|
|
264
273
|
"audioCapture",
|
|
265
|
-
"
|
|
274
|
+
"automaticFullscreen",
|
|
266
275
|
"backgroundFetch",
|
|
267
|
-
"
|
|
276
|
+
"backgroundSync",
|
|
277
|
+
"cameraPanTiltZoom",
|
|
278
|
+
"capturedSurfaceControl",
|
|
268
279
|
"clipboardReadWrite",
|
|
269
280
|
"clipboardSanitizedWrite",
|
|
270
281
|
"displayCapture",
|
|
271
282
|
"durableStorage",
|
|
272
|
-
"flash",
|
|
273
283
|
"geolocation",
|
|
284
|
+
"handTracking",
|
|
274
285
|
"idleDetection",
|
|
286
|
+
"keyboardLock",
|
|
275
287
|
"localFonts",
|
|
288
|
+
"localNetwork",
|
|
289
|
+
"localNetworkAccess",
|
|
290
|
+
"loopbackNetwork",
|
|
276
291
|
"midi",
|
|
277
292
|
"midiSysex",
|
|
278
293
|
"nfc",
|
|
279
294
|
"notifications",
|
|
280
295
|
"paymentHandler",
|
|
281
296
|
"periodicBackgroundSync",
|
|
297
|
+
"pointerLock",
|
|
282
298
|
"protectedMediaIdentifier",
|
|
283
299
|
"sensors",
|
|
284
|
-
"
|
|
300
|
+
"smartCard",
|
|
285
301
|
"speakerSelection",
|
|
302
|
+
"storageAccess",
|
|
286
303
|
"topLevelStorageAccess",
|
|
287
304
|
"videoCapture",
|
|
288
|
-
"
|
|
305
|
+
"vr",
|
|
289
306
|
"wakeLockScreen",
|
|
290
307
|
"wakeLockSystem",
|
|
291
308
|
"webAppInstallation",
|
|
309
|
+
"webPrinting",
|
|
292
310
|
"windowManagement",
|
|
293
311
|
] as const;
|
|
294
312
|
|
|
@@ -1030,13 +1048,12 @@ export class Page {
|
|
|
1030
1048
|
* Supported selectors (see `selector.ts`): tag / id / class / attribute /
|
|
1031
1049
|
* descendant combinator / comma-separated lists. **Not** supported:
|
|
1032
1050
|
* `>`/`+`/`~` combinators, `:pseudo-classes`, `::pseudo-elements`, XPath.
|
|
1033
|
-
* XPath is a stretch goal
|
|
1051
|
+
* XPath is a stretch goal — TODO if a future surface
|
|
1034
1052
|
* needs it (Turnstile detection only needs CSS).
|
|
1035
1053
|
*
|
|
1036
1054
|
* Performance: O(N) in DOM size per call. Acceptable for v0.2; a per-page
|
|
1037
1055
|
* cache layer is a v0.3+ concern (also called out in 0253).
|
|
1038
1056
|
*
|
|
1039
|
-
* @see tasks/0253-closed-shadow-piercing-locator.md
|
|
1040
1057
|
* @see PLAN.md §8.2 (`DOM.getDocument` and `DOM.resolveNode` are not on the
|
|
1041
1058
|
* forbidden list — both fine to use here).
|
|
1042
1059
|
*/
|
|
@@ -1480,7 +1497,7 @@ function hash01(s: string): number {
|
|
|
1480
1497
|
return (h >>> 0) / 0x1_0000_0000;
|
|
1481
1498
|
}
|
|
1482
1499
|
|
|
1483
|
-
// ---- DOM storage factory
|
|
1500
|
+
// ---- DOM storage factory ----------------------------------------
|
|
1484
1501
|
|
|
1485
1502
|
/**
|
|
1486
1503
|
* Build the {@link DomStorage} returned by `Page.localStorage` /
|
package/src/proc.ts
CHANGED
|
@@ -15,7 +15,7 @@ import type { PipeReader, PipeWriter } from "./cdp/transport";
|
|
|
15
15
|
/**
|
|
16
16
|
* The chromium flags PLAN.md §8.6 mandates we always pass in PRODUCTION
|
|
17
17
|
* (non-hermetic) mode. Trimmed against patchright's
|
|
18
|
-
* `chromiumSwitchesPatch.ts:20-34` removal list
|
|
18
|
+
* `chromiumSwitchesPatch.ts:20-34` removal list: every flag
|
|
19
19
|
* here passes two tests — (a) it isn't a passive command-line bot-tell that
|
|
20
20
|
* patchright explicitly drops, AND (b) we have a concrete production reason
|
|
21
21
|
* to keep it (CDP transport, UI suppression that matters in headed mode,
|
|
@@ -181,7 +181,7 @@ export interface SpawnConfig {
|
|
|
181
181
|
* integers; otherwise the flag is omitted. Sourced from
|
|
182
182
|
* `matrix.display.{width,height}` by `launch.ts` — the matrix is canonical.
|
|
183
183
|
*
|
|
184
|
-
* @see UDC `__init__.py:410-411`, UDC issue #2242,
|
|
184
|
+
* @see UDC `__init__.py:410-411`, UDC issue #2242,
|
|
185
185
|
*/
|
|
186
186
|
windowSize?: { width: number; height: number };
|
|
187
187
|
/**
|
|
@@ -418,7 +418,7 @@ export function buildChromiumArgs(
|
|
|
418
418
|
}
|
|
419
419
|
// Headless dispatch.
|
|
420
420
|
//
|
|
421
|
-
// `headlessMode`
|
|
421
|
+
// `headlessMode` supersedes the legacy `headless: boolean` knob
|
|
422
422
|
// when both are set. When `headlessMode` is unset, fall back to the v0.1
|
|
423
423
|
// mapping (`headless: true → "new"`, `false → "off"`). The launcher in
|
|
424
424
|
// `launch.ts` is responsible for the env-aware default ("new" on Linux
|
|
@@ -445,7 +445,7 @@ export function buildChromiumArgs(
|
|
|
445
445
|
// header so the network surface matches the JS-layer `navigator.language`
|
|
446
446
|
// spoof (PLAN.md I-5). Pushed BEFORE `extraArgs` so a user-supplied
|
|
447
447
|
// override in `args` can win on the command line if absolutely needed —
|
|
448
|
-
// Chromium honors the last-occurrence on the line for `--lang`.
|
|
448
|
+
// Chromium honors the last-occurrence on the line for `--lang`.
|
|
449
449
|
if (cfg.locale !== undefined && cfg.locale.length > 0) {
|
|
450
450
|
args.push(`--lang=${cfg.locale}`);
|
|
451
451
|
}
|
|
@@ -454,7 +454,7 @@ export function buildChromiumArgs(
|
|
|
454
454
|
// Chromium's headless 800×600 default. The matrix is canonical: when
|
|
455
455
|
// `display.{width,height}` is missing or non-finite we omit the flag
|
|
456
456
|
// rather than fall back to a hardcoded value (a hardcoded value would
|
|
457
|
-
// mismatch a profile that legitimately uses different dimensions).
|
|
457
|
+
// mismatch a profile that legitimately uses different dimensions).
|
|
458
458
|
if (cfg.windowSize !== undefined) {
|
|
459
459
|
const { width, height } = cfg.windowSize;
|
|
460
460
|
if (
|
|
@@ -501,7 +501,6 @@ export function buildChromiumArgs(
|
|
|
501
501
|
* surfaces only the raw stderr tail. Exported for unit tests so we can lock
|
|
502
502
|
* the regexes against regressions without spawning Chromium.
|
|
503
503
|
*
|
|
504
|
-
* @see tasks/0259-linux-first-run-experience.md
|
|
505
504
|
*/
|
|
506
505
|
export function diagnoseEarlyExitTail(tail: string): string {
|
|
507
506
|
if (/running.*root.*without.*--no-sandbox|--no-sandbox.*required/i.test(tail)) {
|
package/src/proxy-auth.ts
CHANGED
|
@@ -39,8 +39,6 @@
|
|
|
39
39
|
* handler covers both.
|
|
40
40
|
*
|
|
41
41
|
* @see PLAN.md §8.2 / §10
|
|
42
|
-
* @see tasks/0160-proxy-auth-and-ci-fix.md
|
|
43
|
-
* @see tasks/0266-fetch-fulfill-init-script.md
|
|
44
42
|
*/
|
|
45
43
|
|
|
46
44
|
import { installInitInjector } from "./cdp/init-injector";
|
|
@@ -148,7 +146,7 @@ export interface ProxyAuthHandle {
|
|
|
148
146
|
* shim — delegates to {@link installInitInjector} with `payloadCode: null`
|
|
149
147
|
* so the proxy-auth-only call path still works for any out-of-tree caller.
|
|
150
148
|
*
|
|
151
|
-
* The Session no longer uses this directly
|
|
149
|
+
* The Session no longer uses this directly; proxy auth and
|
|
152
150
|
* init-script delivery share a single `Fetch.enable` owner.
|
|
153
151
|
*
|
|
154
152
|
* Behavior (unchanged contract):
|