@mochi.js/core 0.8.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.
Files changed (2) hide show
  1. package/package.json +5 -4
  2. package/src/launch.ts +84 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@mochi.js/core",
3
- "version": "0.8.0",
3
+ "version": "0.8.1",
4
4
  "description": "The library for faithful browser automation. Bun-native; relational fingerprint matrix, biomechanical input, stock Chromium-for-Testing.",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -49,10 +49,11 @@
49
49
  "build": "echo 'no build step yet — Bun consumes src/ directly'"
50
50
  },
51
51
  "dependencies": {
52
- "@mochi.js/behavioral": "^0.1.4",
52
+ "@mochi.js/behavioral": "^0.1.5",
53
53
  "@mochi.js/challenges": "^0.2.1",
54
- "@mochi.js/consistency": "^0.1.3",
55
- "@mochi.js/inject": "^0.3.0"
54
+ "@mochi.js/consistency": "^0.1.4",
55
+ "@mochi.js/inject": "^0.3.1",
56
+ "@mochi.js/profiles": "^0.2.0"
56
57
  },
57
58
  "publishConfig": {
58
59
  "access": "public"
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";
@@ -262,15 +263,17 @@ export async function launch(opts: LaunchOptions): Promise<Session> {
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
@@ -509,27 +512,33 @@ function normalizeProxy(p: LaunchOptions["proxy"]):
509
512
  *
510
513
  * 1. Explicit `ProfileV1` object — flows through unchanged. `autoPicked`
511
514
  * false; `id` taken from the inline object.
512
- * 2. Explicit `ProfileId` string — same placeholder synthesis as before.
513
- * `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.
514
521
  * 3. `undefined` — task 0272: call `defaultProfileForHost()`. Throw with
515
522
  * the unsupported-host diagnostic when the resolver returns `null`.
516
- * `autoPicked` true.
523
+ * `autoPicked` true; same captured-vs-placeholder fallback as branch
524
+ * 2.
517
525
  *
518
- * Pure function does not log. The launcher emits the INFO line itself
519
- * after observing `autoPicked === true` so test fixtures can assert the
520
- * 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`.
521
530
  */
522
- function resolveProfileSource(profile: ProfileId | ProfileV1 | undefined): {
531
+ async function resolveProfileSource(profile: ProfileId | ProfileV1 | undefined): Promise<{
523
532
  profile: ProfileV1;
524
533
  id: ProfileId;
525
534
  autoPicked: boolean;
526
- } {
535
+ }> {
527
536
  if (typeof profile === "object") {
528
537
  return { profile, id: profile.id, autoPicked: false };
529
538
  }
530
539
  if (typeof profile === "string") {
531
540
  return {
532
- profile: synthesizePlaceholderProfile(profile),
541
+ profile: await loadProfileWithFallback(profile),
533
542
  id: profile,
534
543
  autoPicked: false,
535
544
  };
@@ -540,24 +549,75 @@ function resolveProfileSource(profile: ProfileId | ProfileV1 | undefined): {
540
549
  throw new Error(unsupportedHostMessage(process.platform, process.arch));
541
550
  }
542
551
  return {
543
- profile: synthesizePlaceholderProfile(picked),
552
+ profile: await loadProfileWithFallback(picked),
544
553
  id: picked,
545
554
  autoPicked: true,
546
555
  };
547
556
  }
548
557
 
549
558
  /**
550
- * Synthesize a generic placeholder `ProfileV1` from a profile id. Until
551
- * `@mochi.js/profiles.getProfile` lands (phase 0.4), the consistency engine
552
- * still produces a real, relationally-locked Matrix from this skeleton
553
- * 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.
554
614
  */
555
615
  function synthesizePlaceholderProfile(profile: ProfileId): ProfileV1 {
556
616
  return {
557
617
  id: profile,
558
618
  version: "0.0.0-placeholder",
559
619
  engine: "chromium",
560
- browser: { name: "chrome", channel: "stable", minVersion: "131", maxVersion: "133" },
620
+ browser: { name: "chrome", channel: "stable", minVersion: "148", maxVersion: "148" },
561
621
  os: { name: "linux", version: "22", arch: "x64" },
562
622
  device: {
563
623
  vendor: "generic",
@@ -582,11 +642,13 @@ function synthesizePlaceholderProfile(profile: ProfileId): ProfileV1 {
582
642
  locale: "en-US",
583
643
  languages: ["en-US", "en"],
584
644
  behavior: { hand: "right", tremor: 0.18, wpm: 60, scrollStyle: "smooth" },
585
- // Deprecated kept for one release for migration; runtime no longer
586
- // reads the field. Drops in 0.8.
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.
587
649
  wreqPreset: "chrome_148_linux",
588
650
  userAgent:
589
- "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",
590
652
  uaCh: {},
591
653
  entropyBudget: { fixed: [], perSeed: [] },
592
654
  };