@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/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 since task 0272** — when omitted, mochi auto-picks the
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 (task 0258):
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 (task 0258). One of:
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); task 0040.
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 + task 0256.
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 wreq (using the matrix's
239
- * `wreqPreset`, so the geo service sees the same JA4/headers as user
240
- * traffic). 4-attempt cap, 2s per endpoint. Probe results are NOT
241
- * cached across sessions — proxy IPs rotate.
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 (task 0251) and `--window-size` flag
259
- // (task 0252). The matrix is otherwise read post-spawn for inject;
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
- // are resolved against a placeholder profile until `@mochi.js/profiles`
266
- // ships its first capture (phase 0.4). The matrix is bit-stable per
267
- // `(profile, seed)` excluding the `derivedAt` timestamp.
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
- // task 0272 — keep stable so docs + LLM-context blocks stay correct.
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 (using wreq
293
- // with the matrix's `wreqPreset` so the geo service sees the same JA4
294
- // / headers as user traffic). Then cross-reference against
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?.netProxy !== undefined ? { proxy: normalized.netProxy } : {}),
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. Task 0251.
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`. Task 0252.
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
- // Forward the same proxy (with auth, if any) to the net FFI so
399
- // out-of-band Session.fetch traffic shares the apparent egress.
400
- // `@mochi.js/net` (wreq) accepts the full `user:pass@host` URL form.
401
- ...(normalized !== undefined ? { netProxy: normalized.netProxy } : {}),
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. Task 0258.
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. Task 0272.
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
- * - `netProxy`: the URL handed to the network FFI. Preserves credentials
467
- * (wreq accepts `user:pass@host` inline) so out-of-band fetches
468
- * authenticate against the same proxy.
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
- netProxy: string;
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
- netProxy: p,
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
- netProxy,
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 — same placeholder synthesis as before.
529
- * `autoPicked` false.
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
- * Pure function does not log. The launcher emits the INFO line itself
535
- * after observing `autoPicked === true` so test fixtures can assert the
536
- * resolution without intercepting `console`.
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: synthesizePlaceholderProfile(profile),
541
+ profile: await loadProfileWithFallback(profile),
549
542
  id: profile,
550
543
  autoPicked: false,
551
544
  };
552
545
  }
553
- // Auto-pick branch — task 0272.
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: synthesizePlaceholderProfile(picked),
552
+ profile: await loadProfileWithFallback(picked),
560
553
  id: picked,
561
554
  autoPicked: true,
562
555
  };
563
556
  }
564
557
 
565
558
  /**
566
- * Synthesize a generic placeholder `ProfileV1` from a profile id. Until
567
- * `@mochi.js/profiles.getProfile` lands (phase 0.4), the consistency engine
568
- * still produces a real, relationally-locked Matrix from this skeleton
569
- * the id is what flows into `sha256(profile.id + seed)`.
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: "131", maxVersion: "133" },
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: "chrome_131_linux",
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/131.0.0.0 Safari/537.36",
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
  };
@@ -17,7 +17,6 @@
17
17
  * which is fine for a v0.2 surface.
18
18
  *
19
19
  * @see PLAN.md §8.2 / §8.3
20
- * @see tasks/0253-closed-shadow-piercing-locator.md
21
20
  */
22
21
 
23
22
  import type { MessageRouter } from "../cdp/router";
@@ -33,7 +33,6 @@
33
33
  * brief — a per-page cache layer is a v0.3+ concern).
34
34
  *
35
35
  * @see PLAN.md §8.2 — `DOM.getDocument` / `DOM.resolveNode` are not forbidden
36
- * @see tasks/0253-closed-shadow-piercing-locator.md
37
36
  */
38
37
 
39
38
  import type { PierceDomNode } from "../cdp/types";
@@ -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 (task 0257) ---------------------
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 131 (the
256
- * mochi profile floor — same baseline the worker idOnly bootstrap relies on).
257
- * The list is verbose-on-purpose: we want a contract test to catch the day
258
- * Chromium adds a new permission so we can decide whether to forward it.
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
- "accessibilityEvents",
272
+ "ar",
264
273
  "audioCapture",
265
- "backgroundSync",
274
+ "automaticFullscreen",
266
275
  "backgroundFetch",
267
- "captureHandle",
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
- "storageAccess",
300
+ "smartCard",
285
301
  "speakerSelection",
302
+ "storageAccess",
286
303
  "topLevelStorageAccess",
287
304
  "videoCapture",
288
- "videoCapturePanTiltZoom",
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 per task 0253 brief — TODO if a future surface
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 (task 0257) ----------------------------------------
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 (task 0256): every flag
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, task 0252.
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` (task 0258) supersedes the legacy `headless: boolean` knob
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`. Task 0251.
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). Task 0252.
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 (task 0266); proxy auth and
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):