🚀 Socket Launch Week Day 5:Introducing Repository Access Permissions and Custom Roles.Learn more
Sign In

@mochi.js/inject

Package Overview
Dependencies
Maintainers
1
Versions
10
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@mochi.js/inject - npm Package Compare versions

Comparing version
0.0.1
to
0.1.0
+86
src/__tests__/fixtures.ts
/**
* Shared test fixtures for the inject unit tests.
*
* The canonical matrix is a deterministically-derived `MatrixV1` for
* `(profile = mac-m2-chrome-stable-fixture, seed = "fixture-seed")`. We
* pin a hand-built fixture rather than calling `deriveMatrix` so the
* inject tests stay independent of the consistency engine's exact
* behaviour — what we want is "given THIS matrix, the payload spoofs
* THIS surface".
*
* The contract test in `tests/contract/inject-payload.contract.test.ts`
* uses the consistency engine's real `deriveMatrix` for the sha256 pin.
*/
import type { MatrixV1 } from "@mochi.js/consistency";
export const FIXTURE_MATRIX: MatrixV1 = {
id: "mac-m2-chrome-stable-fixture",
version: "0.0.0-fixture",
engine: "chromium",
browser: { name: "chrome", channel: "stable", minVersion: "131", maxVersion: "133" },
os: { name: "macos", version: "14", arch: "arm64" },
device: {
vendor: "Apple",
model: "Mac14,2",
cpuFamily: "apple-silicon-m2",
cores: 8,
memoryGB: 16,
},
display: { width: 1728, height: 1117, dpr: 2, colorDepth: 30, pixelDepth: 30 },
gpu: {
vendor: "Apple Inc.",
renderer: "Apple M2",
webglUnmaskedVendor: "Google Inc. (Apple)",
webglUnmaskedRenderer: "ANGLE (Apple, ANGLE Metal Renderer: Apple M2, Unspecified Version)",
webglMaxTextureSize: 16384,
webglMaxColorAttachments: 8,
webglExtensions: ["ANGLE_instanced_arrays", "EXT_blend_minmax", "EXT_color_buffer_half_float"],
},
audio: { contextSampleRate: 48000, audioWorkletLatency: 0.005, destinationMaxChannelCount: 2 },
fonts: {
family: "macos-baseline",
list: ["Helvetica", "Helvetica Neue", "Arial", "Times", "Courier"],
},
timezone: "America/Los_Angeles",
locale: "en-US",
languages: ["en-US", "en"],
behavior: { hand: "right", tremor: 0.18, wpm: 60, scrollStyle: "smooth" },
wreqPreset: "chrome_131_macos",
userAgent:
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36",
uaCh: {
"sec-ch-ua": '"Google Chrome";v="131", "Not.A/Brand";v="8", "Chromium";v="131"',
"sec-ch-ua-platform": '"macOS"',
"sec-ch-ua-platform-version": '"14.0.0"',
"sec-ch-ua-arch": '"arm"',
"sec-ch-ua-bitness": '"64"',
"sec-ch-ua-mobile": "?0",
"navigator-platform": "MacIntel",
"navigator-vendor": "Google Inc.",
"navigator-appCodeName": "Mozilla",
"navigator-product": "Gecko",
"navigator-cookieEnabled": "true",
"navigator-maxTouchPoints": "0",
"navigator-webdriver": "false",
"navigator-appVersion":
"5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.6778.86 Safari/537.36",
"screen-availSize": JSON.stringify({ availWidth: 1728, availHeight: 1092 }),
"screen-dimensions": JSON.stringify({
width: 1728,
height: 1117,
availWidth: 1728,
availHeight: 1092,
}),
"window-viewport": JSON.stringify({
innerWidth: 1728,
innerHeight: 1005,
outerWidth: 1728,
outerHeight: 1092,
}),
},
entropyBudget: { fixed: [], perSeed: [] },
seed: "fixture-seed",
derivedAt: "2026-01-01T00:00:00.000Z",
consistencyEngineVersion: "0.2.0-fixture",
};
/**
* Unit: payload shape and determinism.
*
* - `buildPayload` returns a non-empty IIFE string.
* - The string parses as valid JS (`new Function` accepts it).
* - Same matrix → byte-identical code → identical sha256.
* - Differs across matrices.
* - sha256 is 64 hex chars.
* - `code` is wrapped in a single top-level IIFE.
*
* @see tasks/0030-inject-engine-v0.md §"payload-shape.test.ts"
*/
import { describe, expect, it } from "bun:test";
import { buildPayload } from "../build";
import { FIXTURE_MATRIX } from "./fixtures";
describe("buildPayload — payload shape and determinism", () => {
it("returns code + sha256", () => {
const out = buildPayload(FIXTURE_MATRIX);
expect(typeof out.code).toBe("string");
expect(out.code.length).toBeGreaterThan(0);
expect(typeof out.sha256).toBe("string");
expect(out.sha256).toMatch(/^[0-9a-f]{64}$/);
});
it("wraps code in a single top-level IIFE", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
// Allow leading whitespace; first non-whitespace token must be `(`.
expect(code.trimStart().startsWith("(function")).toBe(true);
// Tail must close the IIFE and invoke it.
expect(code.trimEnd().endsWith("})();")).toBe(true);
});
it("parses as valid JS (new Function accepts it)", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
// We don't EXECUTE it here (no DOM); we only confirm it parses.
expect(() => new Function(code)).not.toThrow();
});
it("is deterministic across calls (same matrix → identical code)", () => {
const a = buildPayload(FIXTURE_MATRIX);
const b = buildPayload(FIXTURE_MATRIX);
expect(a.code).toBe(b.code);
expect(a.sha256).toBe(b.sha256);
});
it("changes when the matrix's userAgent changes", () => {
const a = buildPayload(FIXTURE_MATRIX);
const b = buildPayload({ ...FIXTURE_MATRIX, userAgent: "Mozilla/5.0 (different)" });
expect(a.sha256).not.toBe(b.sha256);
expect(a.code).not.toBe(b.code);
});
it("ignores derivedAt for byte stability", () => {
// Per the build doc: only fields used by spoof modules affect output.
// The header banner does NOT include derivedAt, so changing it must
// not change the bytes.
const a = buildPayload(FIXTURE_MATRIX);
const b = buildPayload({ ...FIXTURE_MATRIX, derivedAt: "2099-12-31T23:59:59.999Z" });
expect(a.sha256).toBe(b.sha256);
});
it("respects the soft size budget (≤ 80 KB)", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
expect(new TextEncoder().encode(code).length).toBeLessThanOrEqual(80 * 1024);
});
});
/**
* Unit: phase-0.7 spoof modules.
*
* The new modules (webgpu, media-devices, permissions, network-info,
* screen-orientation) read JSON-encoded uaCh keys produced by R-031..R-040.
* The sandbox in `sandbox.ts` doesn't yet stand up navigator.gpu /
* mediaDevices / permissions / connection / matchMedia, so these tests
* assert the SHAPE of the emitted JS rather than its runtime semantics —
* matching the pattern used by `payload-shape.test.ts`. Runtime semantics
* are exercised by the harness E2E gate against real Chromium.
*
* @see tasks/0070-consistency-rules-full.md
*/
import { describe, expect, it } from "bun:test";
import { buildPayload } from "../build";
import { emitMediaDevicesModule } from "../modules/media-devices";
import { emitNetworkInfoModule } from "../modules/network-info";
import { emitPermissionsModule } from "../modules/permissions";
import { emitScreenOrientationModule } from "../modules/screen-orientation";
import { emitWebgpuModule } from "../modules/webgpu";
import { FIXTURE_MATRIX } from "./fixtures";
/**
* Fixture extended with the phase-0.7 uaCh keys. Mirrors the JSON shapes
* produced by R-031..R-040 in `@mochi.js/consistency`.
*/
const PHASE07_MATRIX = {
...FIXTURE_MATRIX,
uaCh: {
...FIXTURE_MATRIX.uaCh,
"ua-full-version-list": JSON.stringify([
{ brand: "Google Chrome", version: "131.0.6778.110" },
{ brand: "Not.A/Brand", version: "8.0.0.0" },
{ brand: "Chromium", version: "131.0.6778.110" },
]),
"webgpu-features": JSON.stringify(["depth32float-stencil8", "shader-f16"]),
"webgpu-info": JSON.stringify({
vendor: "apple",
architecture: "metal-3",
device: "",
description: "",
}),
"media-devices": JSON.stringify([
{ kind: "audioinput", label: "" },
{ kind: "videoinput", label: "" },
]),
"media-supported-constraints": JSON.stringify({ deviceId: true, groupId: true }),
"permissions-defaults": JSON.stringify({ geolocation: "prompt", accelerometer: "granted" }),
connection: JSON.stringify({ effectiveType: "4g", downlink: 10, rtt: 50, saveData: false }),
"screen-orientation": JSON.stringify({ type: "landscape-primary", angle: 0 }),
"media-queries": JSON.stringify({ "prefers-color-scheme": "light", monochrome: false }),
"storage-estimate": JSON.stringify({ quota: 1_000_000_000, usage: 0 }),
},
};
describe("phase-0.7 modules — webgpu", () => {
it("emits a non-empty module when uaCh.webgpu-features is present", () => {
const code = emitWebgpuModule(PHASE07_MATRIX);
expect(code).toContain("WebGPU spoof");
expect(code).toContain("requestAdapter");
expect(code).toContain("metal-3");
expect(code).toContain("shader-f16");
});
it("skips when matrix has no webgpu data", () => {
const code = emitWebgpuModule(FIXTURE_MATRIX);
expect(code).toContain("WebGPU spoof (skipped");
});
});
describe("phase-0.7 modules — media-devices", () => {
it("emits enumerateDevices override + seeded deterministic IDs", () => {
const code = emitMediaDevicesModule(PHASE07_MATRIX);
expect(code).toContain("enumerateDevices");
expect(code).toContain("getSupportedConstraints");
// Deterministic IDs: the deviceId hex appears twice in the source
// (once per device) — we verify the substring shape, not the exact
// bytes (those depend on profile.id + seed).
expect(code).toMatch(/"deviceId":"[a-f0-9]{64}"/);
expect(code).toMatch(/"groupId":"[a-f0-9]{64}"/);
});
it("derives stable IDs per (profile, seed)", () => {
const a = emitMediaDevicesModule(PHASE07_MATRIX);
const b = emitMediaDevicesModule(PHASE07_MATRIX);
expect(a).toBe(b);
});
it("derives different IDs per seed", () => {
const a = emitMediaDevicesModule(PHASE07_MATRIX);
const b = emitMediaDevicesModule({ ...PHASE07_MATRIX, seed: "fixture-seed-other" });
expect(a).not.toBe(b);
});
});
describe("phase-0.7 modules — permissions", () => {
it("emits Permissions.prototype.query override", () => {
const code = emitPermissionsModule(PHASE07_MATRIX);
expect(code).toContain("Permissions.prototype");
expect(code).toContain('"geolocation":"prompt"');
expect(code).toContain('"accelerometer":"granted"');
});
it("skips when defaults are missing", () => {
const code = emitPermissionsModule(FIXTURE_MATRIX);
expect(code).toContain("permissions spoof (skipped");
});
});
describe("phase-0.7 modules — network-info", () => {
it("emits effectiveType + downlink + rtt + saveData defines", () => {
const code = emitNetworkInfoModule(PHASE07_MATRIX);
expect(code).toContain("effectiveType");
expect(code).toContain('"4g"');
expect(code).toContain("10");
expect(code).toContain("50");
});
});
describe("phase-0.7 modules — screen-orientation + matchMedia", () => {
it("emits orientation + matchMedia + storage.estimate spoofs", () => {
const code = emitScreenOrientationModule(PHASE07_MATRIX);
expect(code).toContain("landscape-primary");
expect(code).toContain("matchMedia");
expect(code).toContain("estimate");
});
});
describe("phase-0.7 — full payload integration", () => {
it("buildPayload includes all phase-0.7 module markers", () => {
const { code } = buildPayload(PHASE07_MATRIX);
expect(code).toContain("mochi:webgpu");
expect(code).toContain("mochi:media-devices");
expect(code).toContain("mochi:permissions");
expect(code).toContain("mochi:network-info");
expect(code).toContain("mochi:screen-orientation");
});
it("buildPayload deterministic for identical phase-0.7 fixture", () => {
const a = buildPayload(PHASE07_MATRIX);
const b = buildPayload(PHASE07_MATRIX);
expect(a.sha256).toBe(b.sha256);
});
it("buildPayload differs when phase-0.7 fixture changes", () => {
const a = buildPayload(PHASE07_MATRIX);
const b = buildPayload({
...PHASE07_MATRIX,
uaCh: {
...PHASE07_MATRIX.uaCh,
"screen-orientation": JSON.stringify({ type: "portrait-primary", angle: 90 }),
},
});
expect(a.sha256).not.toBe(b.sha256);
});
});
/**
* Unit: payload runtime overrides.
*
* Loads the payload string into a synthesized JS sandbox (no Bun.spawn,
* no real Chromium) and asserts each spoofed property reads back the
* matrix value.
*
* The sandbox is a minimal stand-in — the real proof is the E2E test in
* `packages/core/src/__tests__/inject.e2e.test.ts`. These unit tests
* verify the payload's *intent* and exercise the structure on the fast
* feedback loop.
*/
import { describe, expect, it } from "bun:test";
import { buildPayload } from "../build";
import { FIXTURE_MATRIX } from "./fixtures";
import { runPayloadInSandbox } from "./sandbox";
describe("inject runtime overrides — navigator", () => {
it("overrides navigator.userAgent", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect(sb.navigator.userAgent).toBe(FIXTURE_MATRIX.userAgent);
});
it("overrides navigator.platform from uaCh", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect(sb.navigator.platform).toBe(FIXTURE_MATRIX.uaCh["navigator-platform"]);
});
it("overrides navigator.vendor from uaCh", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect(sb.navigator.vendor).toBe(FIXTURE_MATRIX.uaCh["navigator-vendor"]);
});
it("overrides navigator.appVersion from uaCh", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect(sb.navigator.appVersion).toBe(FIXTURE_MATRIX.uaCh["navigator-appVersion"]);
});
it("overrides navigator.{appCodeName,product}", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect(sb.navigator.appCodeName).toBe("Mozilla");
expect(sb.navigator.product).toBe("Gecko");
});
it("overrides navigator.cookieEnabled to boolean true", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect(sb.navigator.cookieEnabled).toBe(true);
});
it("overrides navigator.maxTouchPoints to number 0", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect(sb.navigator.maxTouchPoints).toBe(0);
});
it("overrides navigator.webdriver to boolean false", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect(sb.navigator.webdriver).toBe(false);
});
it("overrides navigator.hardwareConcurrency from device.cores", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect(sb.navigator.hardwareConcurrency).toBe(FIXTURE_MATRIX.device.cores);
});
it("overrides navigator.deviceMemory from device.memoryGB", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect(sb.navigator.deviceMemory).toBe(FIXTURE_MATRIX.device.memoryGB);
});
it("overrides navigator.language and navigator.languages", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect(sb.navigator.language).toBe(FIXTURE_MATRIX.locale);
expect(sb.navigator.languages).toEqual(FIXTURE_MATRIX.languages);
});
it("makes navigator.languages a frozen array", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const arr = sb.navigator.languages as unknown[];
expect(Object.isFrozen(arr)).toBe(true);
});
});
describe("inject runtime overrides — screen + viewport", () => {
it("overrides screen.{width,height,colorDepth,pixelDepth}", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect(sb.screen.width).toBe(FIXTURE_MATRIX.display.width);
expect(sb.screen.height).toBe(FIXTURE_MATRIX.display.height);
expect(sb.screen.colorDepth).toBe(FIXTURE_MATRIX.display.colorDepth);
expect(sb.screen.pixelDepth).toBe(FIXTURE_MATRIX.display.pixelDepth);
});
it("overrides screen.{availWidth,availHeight} from uaCh", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const expected = JSON.parse(FIXTURE_MATRIX.uaCh["screen-availSize"] as string) as {
availWidth: number;
availHeight: number;
};
expect(sb.screen.availWidth).toBe(expected.availWidth);
expect(sb.screen.availHeight).toBe(expected.availHeight);
});
it("overrides window.devicePixelRatio from display.dpr", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
expect((sb.window as unknown as { devicePixelRatio: unknown }).devicePixelRatio).toBe(
FIXTURE_MATRIX.display.dpr,
);
});
it("overrides window.{innerWidth,innerHeight,outerWidth,outerHeight} from uaCh", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const expected = JSON.parse(FIXTURE_MATRIX.uaCh["window-viewport"] as string) as Record<
string,
number
>;
const win = sb.window as Record<string, unknown>;
expect(win.innerWidth).toBe(expected.innerWidth);
expect(win.innerHeight).toBe(expected.innerHeight);
expect(win.outerWidth).toBe(expected.outerWidth);
expect(win.outerHeight).toBe(expected.outerHeight);
});
});
describe("inject runtime overrides — webgl", () => {
it("returns spoofed UNMASKED_VENDOR_WEBGL", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const proto = sb.WebGLRenderingContext.prototype as { getParameter: (p: number) => unknown };
expect(proto.getParameter(0x9245)).toBe(FIXTURE_MATRIX.gpu.webglUnmaskedVendor);
});
it("returns spoofed UNMASKED_RENDERER_WEBGL", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const proto = sb.WebGLRenderingContext.prototype as { getParameter: (p: number) => unknown };
expect(proto.getParameter(0x9246)).toBe(FIXTURE_MATRIX.gpu.webglUnmaskedRenderer);
});
it("returns spoofed MAX_TEXTURE_SIZE", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const proto = sb.WebGLRenderingContext.prototype as { getParameter: (p: number) => unknown };
expect(proto.getParameter(0x0d33)).toBe(FIXTURE_MATRIX.gpu.webglMaxTextureSize);
});
it("falls through to native for non-spoofed pname", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const proto = sb.WebGLRenderingContext.prototype as { getParameter: (p: number) => unknown };
// sandbox's native returns "BARE-<pname>"
expect(proto.getParameter(7937)).toBe("BARE-7937");
});
it("WebGL2 returns spoofed MAX_COLOR_ATTACHMENTS", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const proto = sb.WebGL2RenderingContext.prototype as { getParameter: (p: number) => unknown };
expect(proto.getParameter(0x8cdf)).toBe(FIXTURE_MATRIX.gpu.webglMaxColorAttachments);
});
});
describe("inject runtime overrides — client-hints (userAgentData)", () => {
it("exposes brands, mobile, platform via the userAgentData getter", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const uad = sb.navigator.userAgentData as {
brands: Array<{ brand: string; version: string }>;
mobile: boolean;
platform: string;
};
expect(uad).toBeDefined();
expect(uad.platform).toBe("macOS");
expect(uad.mobile).toBe(false);
expect(uad.brands.length).toBe(3);
const brandSet = new Set(uad.brands.map((b) => b.brand));
expect(brandSet.has("Chromium")).toBe(true);
expect(brandSet.has("Google Chrome")).toBe(true);
});
it("toJSON() returns brands+mobile+platform", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const uad = sb.navigator.userAgentData as { toJSON(): Record<string, unknown> };
const j = uad.toJSON();
expect(j.platform).toBe("macOS");
expect(j.mobile).toBe(false);
expect(Array.isArray(j.brands)).toBe(true);
});
it("getHighEntropyValues returns the requested hints", async () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const uad = sb.navigator.userAgentData as {
getHighEntropyValues(hints: string[]): Promise<Record<string, unknown>>;
};
const hev = await uad.getHighEntropyValues(["architecture", "bitness", "platformVersion"]);
expect(hev.architecture).toBe("arm");
expect(hev.bitness).toBe("64");
expect(hev.platformVersion).toBe("14.0.0");
});
});
describe("inject runtime overrides — timing (Intl.DateTimeFormat)", () => {
it("resolvedOptions().timeZone returns matrix.timezone", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const dtf = new sb.Intl.DateTimeFormat();
expect(dtf.resolvedOptions().timeZone).toBe(FIXTURE_MATRIX.timezone);
});
});
describe("inject runtime overrides — fonts", () => {
it("document.fonts.size matches matrix.fonts.list.length", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const fonts = sb.document.fonts as { size: number };
expect(fonts.size).toBe(FIXTURE_MATRIX.fonts.list.length);
});
it("for…of iterates the matrix font list", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const fonts = sb.document.fonts as unknown as Iterable<{ family: string }>;
const families: string[] = [];
for (const f of fonts) {
families.push(f.family);
}
expect(families).toEqual([...FIXTURE_MATRIX.fonts.list]);
});
it("check(spec) returns true for matrix-listed fonts", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const fonts = sb.document.fonts as { check(spec: string): boolean };
expect(fonts.check("12px Helvetica")).toBe(true);
expect(fonts.check("16px 'Helvetica Neue'")).toBe(true);
});
it("check(spec) returns false for unknown fonts", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const fonts = sb.document.fonts as { check(spec: string): boolean };
expect(fonts.check("12px DefinitelyNotInTheList")).toBe(false);
});
});
describe("inject runtime overrides — bot-globals cleanup", () => {
it("deletes automation sentinel keys if present", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
// Plant a sentinel pre-payload.
(sb.window as Record<string, unknown>).cdc_adoQpoasnfa76pfcZLmcfl_Array = "tainted";
// Re-run a fresh payload to clean. The sandbox runner above already
// ran once; for this we run the payload again on the same sandbox.
// Simpler: plant the key inside the sandbox before run; but sandbox
// is built fresh by runPayloadInSandbox. Test the cleanup tail by
// running a new sandbox + planting.
const sb2 = runPayloadInSandbox(
`(function(){window.cdc_adoQpoasnfa76pfcZLmcfl_Array='tainted';})();${code}`,
);
expect(
(sb2.window as Record<string, unknown>).cdc_adoQpoasnfa76pfcZLmcfl_Array,
).toBeUndefined();
// Also confirm in sb2 that the sentinel is gone.
void sb;
});
});
/**
* Synthesized JS sandbox for inject unit tests.
*
* Bun has no `node:vm` and we don't want to spin up real Chromium for
* unit tests. We build a fake `window` / `navigator` / `screen` /
* `WebGLRenderingContext` / etc. that is *enumerable-shape* close to a
* real Chromium globalThis, then run the payload via the `Function`
* constructor with the fakes in scope.
*
* The sandbox is intentionally minimal — it gives the payload enough of a
* surface to install its overrides without throwing. It does NOT
* faithfully emulate Chromium semantics; the E2E test (`packages/core/src/
* __tests__/inject.e2e.test.ts`) is the real proof against the real
* browser. These unit tests verify the payload's STRUCTURE and INTENT —
* not full Chrome compatibility.
*/
/**
* The shape of the fake globals exposed inside the sandbox. All accessors
* and methods relevant to the v0.3 spoof modules are present and
* read-writable until the payload installs its overrides.
*/
export interface SandboxGlobals {
window: SandboxGlobals & Record<string, unknown>;
globalThis: SandboxGlobals & Record<string, unknown>;
navigator: Record<string, unknown>;
screen: Record<string, unknown>;
document: { fonts: Record<string, unknown> };
WebGLRenderingContext: { prototype: Record<string, unknown> };
WebGL2RenderingContext: { prototype: Record<string, unknown> };
Intl: typeof Intl;
FontFace: typeof FontFace | undefined;
Symbol: typeof Symbol;
Object: typeof Object;
Array: typeof Array;
Promise: typeof Promise;
Function: typeof Function;
Number: typeof Number;
String: typeof String;
TypeError: typeof TypeError;
Error: typeof Error;
WeakMap: typeof WeakMap;
}
/**
* Build a fresh sandbox. The returned object's `window === globalThis ===
* sandbox` so the payload's `typeof window !== "undefined"` checks succeed.
*
* Each call returns a brand-new sandbox; tests should NOT share state.
*/
export function makeSandbox(): SandboxGlobals {
// Build the navigator with proto-style accessor descriptors. Every
// property is configurable so the payload's `defineProperty` calls
// succeed; once the payload installs its overrides they become
// configurable:false.
type Navish = Record<string, unknown>;
const navProto: Navish = Object.create(Object.prototype);
// Default fields the page would normally see — placeholders so we can
// assert "before" vs "after" the payload runs.
Object.defineProperty(navProto, "userAgent", {
configurable: true,
enumerable: true,
get() {
return "BARE";
},
});
Object.defineProperty(navProto, "platform", {
configurable: true,
enumerable: true,
get() {
return "BARE-PLATFORM";
},
});
Object.defineProperty(navProto, "vendor", {
configurable: true,
enumerable: true,
get() {
return "BARE-VENDOR";
},
});
Object.defineProperty(navProto, "appVersion", {
configurable: true,
enumerable: true,
get() {
return "BARE-APPVER";
},
});
Object.defineProperty(navProto, "appCodeName", {
configurable: true,
enumerable: true,
get() {
return "BARE-APPCODE";
},
});
Object.defineProperty(navProto, "product", {
configurable: true,
enumerable: true,
get() {
return "BARE-PRODUCT";
},
});
Object.defineProperty(navProto, "cookieEnabled", {
configurable: true,
enumerable: true,
get() {
return false;
},
});
Object.defineProperty(navProto, "maxTouchPoints", {
configurable: true,
enumerable: true,
get() {
return 99;
},
});
Object.defineProperty(navProto, "webdriver", {
configurable: true,
enumerable: true,
get() {
return true;
},
});
Object.defineProperty(navProto, "hardwareConcurrency", {
configurable: true,
enumerable: true,
get() {
return 0;
},
});
Object.defineProperty(navProto, "deviceMemory", {
configurable: true,
enumerable: true,
get() {
return 0;
},
});
Object.defineProperty(navProto, "language", {
configurable: true,
enumerable: true,
get() {
return "BARE-LANG";
},
});
Object.defineProperty(navProto, "languages", {
configurable: true,
enumerable: true,
get() {
return [];
},
});
Object.defineProperty(navProto, "userAgentData", {
configurable: true,
enumerable: true,
get() {
return undefined;
},
});
const navigator = Object.create(navProto);
// Screen with prototype-defined accessors.
type Screenish = Record<string, unknown>;
const screenProto: Screenish = Object.create(Object.prototype);
for (const k of ["width", "height", "availWidth", "availHeight", "colorDepth", "pixelDepth"]) {
Object.defineProperty(screenProto, k, {
configurable: true,
enumerable: true,
get() {
return -1;
},
});
}
const screen = Object.create(screenProto);
// Stub WebGL contexts with a `getParameter` method on the prototype.
type Glish = { getParameter: (pname: number) => unknown };
const glProto: Glish & Record<string, unknown> = Object.create(Object.prototype);
glProto.getParameter = function getParameter(pname: number): unknown {
return `BARE-${String(pname)}`;
};
const gl2Proto: Glish & Record<string, unknown> = Object.create(Object.prototype);
gl2Proto.getParameter = function getParameter(pname: number): unknown {
return `BARE2-${String(pname)}`;
};
const WebGLRenderingContext = function (this: unknown) {} as unknown as {
prototype: Glish & Record<string, unknown>;
};
WebGLRenderingContext.prototype = glProto;
const WebGL2RenderingContext = function (this: unknown) {} as unknown as {
prototype: Glish & Record<string, unknown>;
};
WebGL2RenderingContext.prototype = gl2Proto;
// FontFaceSet stub.
const fonts: Record<string, unknown> = {
size: 0,
[Symbol.iterator](): IterableIterator<unknown> {
return [].values();
},
forEach(_cb: (...args: unknown[]) => void): void {},
check(_spec: string): boolean {
return false;
},
};
const sandbox = {
navigator,
screen,
document: { fonts },
WebGLRenderingContext,
WebGL2RenderingContext,
Intl,
FontFace: typeof FontFace !== "undefined" ? FontFace : undefined,
// The payload reaches for these globals; share them from the host.
Symbol,
Object,
Array,
Promise,
Function,
Number,
String,
TypeError,
Error,
WeakMap,
} as unknown as SandboxGlobals;
// window === globalThis === sandbox itself
(sandbox as unknown as { window: unknown }).window = sandbox;
(sandbox as unknown as { globalThis: unknown }).globalThis = sandbox;
return sandbox;
}
/**
* Run the payload code against a fresh sandbox. Returns the sandbox so
* tests can probe state.
*
* Errors bubble out — payload-internal try/catch swallows module errors,
* so a propagated error here means the IIFE itself failed (e.g. parse).
*/
export function runPayloadInSandbox(code: string): SandboxGlobals {
const sandbox = makeSandbox();
// Bind every sandbox property as a function arg so the payload's bare
// identifier references resolve against our fake. We omit `globalThis`
// because Function-constructor scripts bind it to the host globalThis;
// the payload uses `window` for its world checks.
const keys = [
"window",
"navigator",
"screen",
"document",
"WebGLRenderingContext",
"WebGL2RenderingContext",
"Intl",
"FontFace",
"Symbol",
"Object",
"Array",
"Promise",
"Function",
"Number",
"String",
"TypeError",
"Error",
"WeakMap",
];
// Body: shadow `globalThis` locally so the payload's `globalThis` reads
// see our sandbox. The Function constructor's scope is the global
// scope; nested `var globalThis` in the body shadows correctly.
const body = `var globalThis = window; ${code}`;
const fn = new Function(...keys, body);
fn(
sandbox.window,
sandbox.navigator,
sandbox.screen,
sandbox.document,
sandbox.WebGLRenderingContext,
sandbox.WebGL2RenderingContext,
sandbox.Intl,
sandbox.FontFace,
sandbox.Symbol,
sandbox.Object,
sandbox.Array,
sandbox.Promise,
sandbox.Function,
sandbox.Number,
sandbox.String,
sandbox.TypeError,
sandbox.Error,
sandbox.WeakMap,
);
return sandbox;
}
/**
* Unit: self-deletion of init globals.
*
* After the payload runs, no `__mochi*` keys may be visible on `window`.
* The IIFE captures all helpers as locals, but a tail-of-IIFE cleanup
* sweeps `window` for any stragglers.
*/
import { describe, expect, it } from "bun:test";
import { buildPayload } from "../build";
import { FIXTURE_MATRIX } from "./fixtures";
import { runPayloadInSandbox } from "./sandbox";
describe("self-delete — no __mochi* globals leak", () => {
it("window has no __mochi-prefixed own properties after run", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const keys = Object.getOwnPropertyNames(sb.window);
const leaked = keys.filter((k) => k.startsWith("__mochi"));
expect(leaked).toEqual([]);
});
it("removes any pre-planted __mochi globals", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
// Plant before the IIFE runs.
const planted = `(function(){ window.__mochi_planted__ = 'x'; window.__mochi_oops__ = 'y'; })();${code}`;
const sb = runPayloadInSandbox(planted);
expect((sb.window as Record<string, unknown>).__mochi_planted__).toBeUndefined();
expect((sb.window as Record<string, unknown>).__mochi_oops__).toBeUndefined();
});
});
/**
* Unit: toString cloaking — verifies that every spoofed function answers
* `.toString()` with the native shape `function ${name}() { [native code] }`.
*
* Tests run against the sandbox-loaded payload.
*/
import { describe, expect, it } from "bun:test";
import { buildPayload } from "../build";
import { FIXTURE_MATRIX } from "./fixtures";
import { runPayloadInSandbox } from "./sandbox";
describe("toString cloak — spoofed functions return native-shape toString()", () => {
it("Function.prototype.toString.toString() returns native shape", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const fnToString = sb.Function.prototype.toString;
expect(fnToString.toString()).toBe("function toString() { [native code] }");
});
it("WebGLRenderingContext.prototype.getParameter.toString() is cloaked", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const proto = sb.WebGLRenderingContext.prototype as {
getParameter: { toString(): string };
};
expect(proto.getParameter.toString()).toBe("function getParameter() { [native code] }");
});
it("WebGL2RenderingContext.prototype.getParameter.toString() is cloaked", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const proto = sb.WebGL2RenderingContext.prototype as {
getParameter: { toString(): string };
};
expect(proto.getParameter.toString()).toBe("function getParameter() { [native code] }");
});
it("Intl.DateTimeFormat.prototype.resolvedOptions.toString() is cloaked", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const ro = sb.Intl.DateTimeFormat.prototype.resolvedOptions;
expect(ro.toString()).toBe("function resolvedOptions() { [native code] }");
});
it("userAgentData.getHighEntropyValues.toString() is cloaked", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
const sb = runPayloadInSandbox(code);
const uad = sb.navigator.userAgentData as { getHighEntropyValues: { toString(): string } };
expect(uad.getHighEntropyValues.toString()).toBe(
"function getHighEntropyValues() { [native code] }",
);
});
it("normal user-defined functions still show real source", () => {
const { code } = buildPayload(FIXTURE_MATRIX);
runPayloadInSandbox(code);
// Defining a function in this test process (which already had its own
// Function.prototype.toString untouched, since the sandbox patched a
// copy only inside the sandboxed evaluation) — the host-side toString
// is untouched, so we can verify it still produces source.
function userFn(): number {
return 42;
}
expect(userFn.toString()).toContain("return 42");
});
});
/**
* Payload builder — composes per-API spoof modules into a single IIFE
* delivered to Chromium via `Page.addScriptToEvaluateOnNewDocument(
* runImmediately: true, worldName: "")`.
*
* Determinism contract:
* - Same `MatrixV1` (excluding `derivedAt`) → byte-identical `code` →
* identical sha256.
* - Module composition order is fixed.
* - No `Date.now()`, no `Math.random()`, no env reads in the build path.
*
* Size budget: ≤ 80 KB minified (target ~50 KB at v0.3).
* - We don't run a full JS minifier here — `esbuild` can be added later
* if budget pressure mounts. For v0.3 the source is already compact;
* measured below 30 KB with whitespace.
*
* Stealth invariants enforced at build time:
* - Wrapped in a single IIFE — no top-level identifiers escape.
* - Initialization globals `__mochi__*` declared inside the IIFE; any
* stragglers wiped via `delete window.__mochi__*` at end-of-IIFE.
* - `try { ... } catch (e) { ... }` around each module so a thrown
* override never reaches page script (PLAN.md §5.3).
*
* @see PLAN.md §5.3, §8.4
* @see tasks/0030-inject-engine-v0.md
*/
import type { MatrixV1 } from "@mochi.js/consistency";
import { emitBotGlobalsModule } from "./modules/bot-globals";
import { emitClientHintsModule } from "./modules/client-hints";
import { emitFontsModule } from "./modules/fonts";
import { emitMediaDevicesModule } from "./modules/media-devices";
import { emitNavigatorModule } from "./modules/navigator";
import { emitNetworkInfoModule } from "./modules/network-info";
import { emitPermissionsModule } from "./modules/permissions";
import { emitPluginsModule } from "./modules/plugins";
import { emitScreenModule } from "./modules/screen";
import { emitScreenOrientationModule } from "./modules/screen-orientation";
import { emitTimingModule } from "./modules/timing";
import { emitWebglModule } from "./modules/webgl";
import { emitWebgpuModule } from "./modules/webgpu";
import { emitWindowChromeModule } from "./modules/window-chrome";
import { emitDefinePropertyHelper } from "./runtime/defineproperty";
import { emitToStringCloak } from "./runtime/tostring-cloak";
/**
* The result returned by {@link buildPayload}.
*
* - `code` is the IIFE source. Send as-is to
* `Page.addScriptToEvaluateOnNewDocument` with `runImmediately: true,
* worldName: ""` and `Runtime.evaluate` against worker targets.
* - `sha256` is hex-encoded SHA-256 of `code`. Used for cache keys, the
* contract pin in `tests/contract/inject-payload.contract.test.ts`, and
* downstream change detection in the harness.
*/
export interface PayloadResult {
readonly code: string;
readonly sha256: string;
}
/** Soft budget — buildPayload `console.warn`s if exceeded but never throws. */
const SIZE_BUDGET_BYTES = 80 * 1024;
/**
* Compose the inject payload for a given matrix.
*
* The layout is fixed (changing it changes every downstream sha256 pin):
*
* 1. IIFE prologue — `(function(){`
* 2. Header banner (always-true comment block, useful in DevTools dumps)
* 3. Runtime helpers (defineProperty + toString cloak)
* 4. Spoof modules — navigator, screen, webgl, client-hints, timing,
* bot-globals, fonts (in that order)
* 5. Self-deletion of any `__mochi*` window globals
* 6. IIFE epilogue — `})();`
*
* Each spoof module is wrapped in a `try { … } catch (_) {}` at the top
* level of the IIFE so a single module's failure can't take out the rest.
*
* @throws Never. If a module throws *during build* it propagates; runtime
* errors are swallowed by the IIFE's per-module try/catch.
*/
export function buildPayload(matrix: MatrixV1): PayloadResult {
// PLAN.md I-5: never invent values — but we DO build defensively. If the
// matrix is missing a uaCh key, the corresponding module skips that line.
const parts: string[] = [];
parts.push("(function () {");
parts.push(banner(matrix));
parts.push("'use strict';");
parts.push(emitDefinePropertyHelper());
parts.push(emitToStringCloak());
// Each module is wrapped in a try/catch so a single failure can't take
// down the rest. The wrapper logs nothing (PLAN.md §5.3 — never let our
// injection produce console output that page script can observe).
parts.push(wrapTry("navigator", emitNavigatorModule(matrix)));
parts.push(wrapTry("screen", emitScreenModule(matrix)));
parts.push(wrapTry("webgl", emitWebglModule(matrix)));
parts.push(wrapTry("client-hints", emitClientHintsModule(matrix)));
parts.push(wrapTry("timing", emitTimingModule(matrix)));
parts.push(wrapTry("bot-globals", emitBotGlobalsModule()));
parts.push(wrapTry("fonts", emitFontsModule(matrix)));
// Phase 0.7 surface coverage. Order doesn't matter for correctness — each
// module is wrapped in its own try/catch and reads only matrix.uaCh.* — but
// we keep an alphabetical-ish grouping for human readability of the dump.
parts.push(wrapTry("media-devices", emitMediaDevicesModule(matrix)));
parts.push(wrapTry("network-info", emitNetworkInfoModule(matrix)));
parts.push(wrapTry("permissions", emitPermissionsModule(matrix)));
parts.push(wrapTry("screen-orientation", emitScreenOrientationModule(matrix)));
parts.push(wrapTry("webgpu", emitWebgpuModule(matrix)));
// CloakBrowser-surfaced modules — defensive shims that no-op on real
// Chrome.app (where the underlying browser already provides these
// surfaces) and install on Chromium-for-Testing where they're absent.
// See tasks/0140-stealth-conformance.md.
parts.push(wrapTry("window-chrome", emitWindowChromeModule(matrix)));
parts.push(wrapTry("plugins", emitPluginsModule(matrix)));
// Self-deletion of any stray __mochi__* properties on window/globalThis
// — none of our helpers leak there in v0.3 (they're all IIFE-locals),
// but the cleanup is the safety net described in PLAN.md §5.3 last bullet.
parts.push(emitSelfDelete());
parts.push("})();");
const code = parts.join("\n");
// Soft size budget warning.
const bytes = byteLength(code);
if (bytes > SIZE_BUDGET_BYTES) {
console.warn(
`[mochi/inject] payload size ${bytes}B exceeds ${SIZE_BUDGET_BYTES}B budget — consider trimming modules`,
);
}
const sha256 = hashHex(code);
return { code, sha256 };
}
// ---- helpers ----------------------------------------------------------------
/**
* The header banner — a comment block at the top of the IIFE. Useful for
* humans reading the payload in DevTools dumps, and harmless. Includes the
* `Runtime.enable`-resilience disclaimer per PLAN.md §8.2.
*/
function banner(matrix: MatrixV1): string {
// We deliberately do NOT include `derivedAt` so the payload bytes stay
// stable per (profile, seed). Engine version + matrix-deterministic fields
// are fine because they're determinism-stable.
return [
"// @mochi inject payload — see PLAN.md §5.3 / §8.4.",
"// Assumption: Runtime.enable is never sent (PLAN.md §8.2). This IIFE",
"// does NOT add anti-Runtime.enable hacks; the CDP layer",
"// enforces the invariant via packages/core/src/cdp/forbidden.ts.",
`// engine: ${matrix.consistencyEngineVersion}`,
`// profile: ${matrix.id}@${matrix.version}`,
`// seed: ${JSON.stringify(matrix.seed)}`,
].join("\n");
}
/**
* Wrap a module body in a top-level `try { ... } catch (_) {}`. The
* try/catch sits inside the IIFE but outside the module's own IIFEs.
*/
function wrapTry(name: string, body: string): string {
// Inline JS comments stay inside the IIFE source; the wrapper itself is
// a try/catch so a thrown spoof can't take out subsequent modules.
return `try { /* mochi:${name} */\n${body}\n} catch (_e) { /* swallowed per PLAN.md §5.3 */ }`;
}
/**
* Emit the self-delete tail. Walks `window` for keys starting with
* `__mochi__` or `__mochi_` and `delete`s them. Belt-and-braces — the
* module sources don't actually expose any of these to window today (all
* helpers are IIFE-locals).
*/
function emitSelfDelete(): string {
return `
// ---- self-delete init globals ---------------------------------------------
try {
if (typeof window !== "undefined") {
var __mochi_keys__ = Object.getOwnPropertyNames(window);
for (var __mochi_i__ = 0; __mochi_i__ < __mochi_keys__.length; __mochi_i__++) {
var __mochi_k__ = __mochi_keys__[__mochi_i__];
if (__mochi_k__.indexOf("__mochi") === 0) {
try { delete window[__mochi_k__]; } catch (_e) {}
}
}
}
} catch (_e) {}
`;
}
/** UTF-8 byte length of a JS string. */
function byteLength(s: string): number {
// Bun has Buffer; using the global TextEncoder for portability.
return new TextEncoder().encode(s).length;
}
/**
* Hex-encoded SHA-256. Uses Bun's native CryptoHasher when available
* (per task brief), falls back to a small inline JS implementation if
* not (so that the package can be consumed by tooling that runs the
* builder under Node — e.g. doc generators).
*/
function hashHex(input: string): string {
// Bun's CryptoHasher is fastest and matches the task brief's contract.
type CryptoHasherCtor = new (
algo: "sha256",
) => {
update(data: string): void;
digest(encoding: "hex"): string;
};
const maybeBun = (globalThis as { Bun?: { CryptoHasher?: CryptoHasherCtor } }).Bun;
if (maybeBun !== undefined && maybeBun.CryptoHasher !== undefined) {
const h = new maybeBun.CryptoHasher("sha256");
h.update(input);
return h.digest("hex");
}
// Fallback: WebCrypto (sync wouldn't be possible — but the inject build
// path is invoked from sync paths). Throw a helpful error.
throw new Error(
"[mochi/inject] buildPayload requires Bun's CryptoHasher — running outside Bun is unsupported",
);
}
/**
* Spoof module: bot-detection global cleanup.
*
* Defensive deletion of automation framework globals that should NOT exist
* on stock Chromium-for-Testing but show up if the user accidentally launched
* a CDC-tainted Selenium/Chromedriver build, or if a hostile extension
* injected them. The list mirrors the sentinel-key catalog in
* `chaser-recon/src/lib/fingerprint/bot-detection.ts:14-25`.
*
* v0.3 deletes them; phase 0.7 may add a per-key getter trap for
* "delete and observe" detection. The deletion is best-effort (silently
* ignores TypeErrors from non-configurable globals).
*
* The matrix isn't read here — these keys must always be absent regardless
* of profile.
*
* @see tasks/0030-inject-engine-v0.md §"bot-globals.ts"
*/
/**
* The full automation-key catalog. Order doesn't matter; deletion is
* symmetric.
*/
const AUTOMATION_KEYS: readonly string[] = [
// Chromedriver/Selenium CDC sentinel keys.
"cdc_adoQpoasnfa76pfcZLmcfl_Array",
"cdc_adoQpoasnfa76pfcZLmcfl_Promise",
"cdc_adoQpoasnfa76pfcZLmcfl_Symbol",
"$cdc_asdjflasutopfhvcZLmcfl_",
"$chrome_asyncScriptInfo",
"__$webdriverAsyncExecutor",
// PhantomJS / Nightmare / Selenium IDE sentinels.
"_phantom",
"__nightmare",
"_selenium",
"callPhantom",
"callSelenium",
"_Selenium_IDE_Recorder",
// Headless browser markers.
"domAutomation",
"domAutomationController",
"__webdriver_evaluate",
"__selenium_evaluate",
"__webdriver_script_function",
"__webdriver_script_func",
"__webdriver_script_fn",
"__fxdriver_evaluate",
"__driver_unwrapped",
"__webdriver_unwrapped",
"__driver_evaluate",
"__selenium_unwrapped",
"__fxdriver_unwrapped",
"__webdriverFunc",
] as const;
export function emitBotGlobalsModule(): string {
const keys = JSON.stringify(AUTOMATION_KEYS);
return `
// ---- bot-globals cleanup ---------------------------------------------------
(function() {
var KEYS = ${keys};
for (var i = 0; i < KEYS.length; i++) {
var k = KEYS[i];
try {
// Outer scope deletion — \`window\` and \`document\` both checked.
if (typeof window !== "undefined" && k in window) {
delete window[k];
}
if (typeof document !== "undefined" && k in document) {
delete document[k];
}
} catch (_e) {
// Non-configurable; nothing we can do at JS layer. Caller documents
// in docs/limits.md if the surface ever exists in stock Chromium-for-
// Testing.
}
}
})();
`;
}
/**
* Spoof module: `navigator.userAgentData`.
*
* Reads from the matrix:
* - `matrix.uaCh["sec-ch-ua"]` → brands list (R-005)
* - `matrix.uaCh["sec-ch-ua-platform"]` → platform (R-006)
* - `matrix.uaCh["sec-ch-ua-platform-version"]` → platformVersion (R-007)
* - `matrix.uaCh["sec-ch-ua-arch"]` → arch (optional)
* - `matrix.uaCh["sec-ch-ua-bitness"]` → bitness (optional)
* - `matrix.uaCh["sec-ch-ua-model"]` → model (optional)
* - `matrix.uaCh["sec-ch-ua-mobile"]` → mobile (optional, "?0"/"?1")
*
* Sec-CH-UA values arrive on the wire as quoted (e.g. `'"macOS"'`,
* `'"Google Chrome";v="131", "Not.A/Brand";v="8", "Chromium";v="131"'`).
* The spoofed `userAgentData` API exposes parsed shapes:
* - `brands`: array of `{ brand, version }`
* - `platform`: unquoted string
* - `mobile`: boolean
* - `getHighEntropyValues(hints)`: Promise resolving to the requested hints
*
* Missing keys → field omitted from the response (PLAN.md I-5).
*
* @see tasks/0030-inject-engine-v0.md §"client-hints.ts"
*/
import type { MatrixV1 } from "@mochi.js/consistency";
interface BrandEntry {
readonly brand: string;
readonly version: string;
}
/** Strip surrounding double-quotes if present. */
function unquote(s: string): string {
if (s.length >= 2 && s.startsWith('"') && s.endsWith('"')) {
return s.slice(1, -1);
}
return s;
}
/**
* Parse a Sec-CH-UA header value into brand entries. Format:
* `"Brand A";v="123", "Not.A/Brand";v="8", "Brand B";v="456"`
*/
function parseSecChUa(s: string): BrandEntry[] {
const out: BrandEntry[] = [];
// Split on `,` outside quoted segments. Simple state machine.
const parts: string[] = [];
let depth = 0;
let cur = "";
for (let i = 0; i < s.length; i++) {
const c = s[i] as string;
if (c === '"') {
depth = depth === 0 ? 1 : 0;
cur += c;
} else if (c === "," && depth === 0) {
parts.push(cur);
cur = "";
} else {
cur += c;
}
}
if (cur.length > 0) parts.push(cur);
for (const raw of parts) {
const piece = raw.trim();
if (piece.length === 0) continue;
// `"Brand";v="123"`
const semi = piece.indexOf(";");
if (semi === -1) {
out.push({ brand: unquote(piece), version: "" });
continue;
}
const brandPart = piece.slice(0, semi).trim();
const rest = piece.slice(semi + 1).trim();
let version = "";
if (rest.startsWith("v=")) {
version = unquote(rest.slice(2).trim());
}
out.push({ brand: unquote(brandPart), version });
}
return out;
}
export function emitClientHintsModule(matrix: MatrixV1): string {
const ua = matrix.uaCh;
// Parse the Sec-CH-UA bag. Required for the brands list — without it we
// skip the whole module.
const secChUa = ua["sec-ch-ua"];
if (typeof secChUa !== "string" || secChUa.length === 0) {
return `
// ---- client-hints spoof (skipped — no matrix.uaCh["sec-ch-ua"]) -----------
`;
}
const brands = parseSecChUa(secChUa);
const platformRaw = ua["sec-ch-ua-platform"];
const platform = typeof platformRaw === "string" ? unquote(platformRaw) : "";
const platformVersionRaw = ua["sec-ch-ua-platform-version"];
const platformVersion = typeof platformVersionRaw === "string" ? unquote(platformVersionRaw) : "";
// Optional high-entropy fields.
const arch = typeof ua["sec-ch-ua-arch"] === "string" ? unquote(ua["sec-ch-ua-arch"]) : "";
const bitness =
typeof ua["sec-ch-ua-bitness"] === "string" ? unquote(ua["sec-ch-ua-bitness"]) : "";
const model = typeof ua["sec-ch-ua-model"] === "string" ? unquote(ua["sec-ch-ua-model"]) : "";
const mobileRaw = ua["sec-ch-ua-mobile"];
// sec-ch-ua-mobile is "?0" or "?1" on the wire.
const mobile = mobileRaw === "?1";
// R-031 emits a tip-locked full brand list (e.g. `"147.0.7727.138"`) under
// `uaCh.ua-full-version-list` — match what
// `userAgentData.getHighEntropyValues(["fullVersionList"])` returns on
// captured-device baselines. Fall back to `brands` (brand-list majors)
// when the tip table doesn't carry the major.
const fullVersionListRaw = ua["ua-full-version-list"];
let fullVersionList: BrandEntry[] = brands;
if (typeof fullVersionListRaw === "string" && fullVersionListRaw.length > 0) {
try {
const parsed = JSON.parse(fullVersionListRaw) as unknown;
if (Array.isArray(parsed)) {
fullVersionList = parsed
.filter(
(e): e is BrandEntry =>
typeof e === "object" &&
e !== null &&
typeof (e as { brand?: unknown }).brand === "string" &&
typeof (e as { version?: unknown }).version === "string",
)
.map((e) => ({ brand: e.brand, version: e.version }));
}
} catch {
// Fall through to brands.
}
}
const brandsLiteral = JSON.stringify(brands);
const fullVersionListLiteral = JSON.stringify(fullVersionList);
return `
// ---- client-hints spoof ----------------------------------------------------
(function() {
if (typeof navigator === "undefined") return;
var SPOOF_BRANDS = ${brandsLiteral};
var SPOOF_FULL_VERSION_LIST = ${fullVersionListLiteral};
var SPOOF_PLATFORM = ${JSON.stringify(platform)};
var SPOOF_PLATFORM_VERSION = ${JSON.stringify(platformVersion)};
var SPOOF_ARCH = ${JSON.stringify(arch)};
var SPOOF_BITNESS = ${JSON.stringify(bitness)};
var SPOOF_MODEL = ${JSON.stringify(model)};
var SPOOF_MOBILE = ${mobile ? "true" : "false"};
var SPOOF_UA_FULL_VERSION = (SPOOF_FULL_VERSION_LIST[0] && SPOOF_FULL_VERSION_LIST[0].version) || "";
// Re-freeze brand entries on every read (Chrome returns frozen objects).
function freezeBrands(arr) {
var out = [];
for (var i = 0; i < arr.length; i++) {
out.push(Object.freeze({ brand: arr[i].brand, version: arr[i].version }));
}
return Object.freeze(out);
}
function toJSON() {
return { brands: freezeBrands(SPOOF_BRANDS), mobile: SPOOF_MOBILE, platform: SPOOF_PLATFORM };
}
__mochi_register_native__(toJSON, "toJSON");
function getHighEntropyValues(hints) {
return new Promise(function(resolve) {
var out = {
brands: freezeBrands(SPOOF_BRANDS),
mobile: SPOOF_MOBILE,
platform: SPOOF_PLATFORM,
};
if (Array.isArray(hints)) {
for (var i = 0; i < hints.length; i++) {
var h = hints[i];
if (h === "architecture" && SPOOF_ARCH) out.architecture = SPOOF_ARCH;
else if (h === "bitness" && SPOOF_BITNESS) out.bitness = SPOOF_BITNESS;
else if (h === "model") out.model = SPOOF_MODEL;
else if (h === "platformVersion") out.platformVersion = SPOOF_PLATFORM_VERSION;
else if (h === "uaFullVersion" && SPOOF_UA_FULL_VERSION) out.uaFullVersion = SPOOF_UA_FULL_VERSION;
else if (h === "fullVersionList") out.fullVersionList = freezeBrands(SPOOF_FULL_VERSION_LIST);
else if (h === "wow64") out.wow64 = false;
else if (h === "formFactor" && SPOOF_MOBILE) out.formFactor = ["Mobile"];
}
}
resolve(out);
});
}
__mochi_register_native__(getHighEntropyValues, "getHighEntropyValues");
// Build a userAgentData object. Match the live Chrome shape: brands,
// mobile, platform are direct accessors; toJSON + getHighEntropyValues
// are methods.
var spoof = Object.create(null);
__mochi_defineProperty__(spoof, "brands", {
configurable: true,
enumerable: true,
get: function() { return freezeBrands(SPOOF_BRANDS); },
});
__mochi_defineProperty__(spoof, "mobile", {
configurable: true,
enumerable: true,
get: function() { return SPOOF_MOBILE; },
});
__mochi_defineProperty__(spoof, "platform", {
configurable: true,
enumerable: true,
get: function() { return SPOOF_PLATFORM; },
});
__mochi_defineProperty__(spoof, "toJSON", {
configurable: true, enumerable: false, writable: true, value: toJSON,
});
__mochi_defineProperty__(spoof, "getHighEntropyValues", {
configurable: true, enumerable: false, writable: true, value: getHighEntropyValues,
});
// Install on Navigator.prototype so .userAgentData reads return our spoof.
var navProto = __mochi_getPrototypeOf__(navigator);
__mochi_define__(navProto, "userAgentData", spoof);
})();
`;
}
/**
* Spoof module: font enumeration.
*
* Reads from the matrix:
* - `matrix.fonts.list` (R-013) — curated baseline list per OS.
*
* v0.3 surface:
* - Override `document.fonts` iteration so that `Array.from(document.fonts)`
* and `for…of (document.fonts)` enumerate exactly the matrix list.
* - Override `document.fonts.size` to match.
* - Override `document.fonts.check(spec)` to return true iff the family
* parsed from `spec` is in the matrix list.
*
* Limitations (documented in docs/limits.md):
* - We don't actually load any FontFace; CSS rendering with these
* families still uses whatever Chromium has on disk. Probes that
* measure rendered glyph bbox / canvas-rendered fonts will see the
* real font outlines, not the spoofed list. Phase 0.7 lands canvas
* spoofing which closes the loop.
* - We don't override the experimental `queryLocalFonts()` API.
*
* @see tasks/0030-inject-engine-v0.md §"fonts.ts"
* @see docs/limits.md §"v0.3 inject limits"
*/
import type { MatrixV1 } from "@mochi.js/consistency";
export function emitFontsModule(matrix: MatrixV1): string {
// Build a literal of synthetic FontFace-shape entries. Each carries the
// family + weight/style defaults the FontFaceSet APIs query.
const entries = matrix.fonts.list.map((family) => ({
family,
style: "normal",
weight: "400",
stretch: "100%",
unicodeRange: "U+0-10FFFF",
variant: "normal",
featureSettings: "normal",
display: "auto",
status: "loaded",
}));
const entriesLiteral = JSON.stringify(entries);
const familySetLiteral = JSON.stringify(matrix.fonts.list);
return `
// ---- fonts spoof -----------------------------------------------------------
(function() {
if (typeof document === "undefined") return;
var fonts = document.fonts;
if (fonts === undefined || fonts === null) return;
var SPOOF_ENTRIES = ${entriesLiteral};
var SPOOF_FAMILIES = ${familySetLiteral};
// Lowercase set for case-insensitive family matching.
var SPOOF_FAMILIES_LC = {};
for (var i = 0; i < SPOOF_FAMILIES.length; i++) {
SPOOF_FAMILIES_LC[String(SPOOF_FAMILIES[i]).toLowerCase()] = true;
}
// Build FontFace-shape stand-ins. We don't construct real FontFace
// instances (the constructor takes a font URL we don't have); a plain
// object with the right keys is enough for fingerprint enumeration.
function buildFakeFonts() {
var out = [];
for (var i = 0; i < SPOOF_ENTRIES.length; i++) {
var e = SPOOF_ENTRIES[i];
// Prefer the real FontFace prototype if available, but with our data.
var f = Object.create(typeof FontFace !== "undefined" ? FontFace.prototype : Object.prototype);
for (var k in e) {
if (Object.prototype.hasOwnProperty.call(e, k)) {
try {
__mochi_defineProperty__(f, k, {
configurable: true, enumerable: true, get: (function(v) { return function() { return v; }; })(e[k]),
});
} catch (_err) {}
}
}
out.push(f);
}
return out;
}
var fakeList = buildFakeFonts();
// Replace [Symbol.iterator] on the FontFaceSet so for…of enumerates ours.
function fontIterator() {
var idx = 0;
return {
next: function() {
if (idx < fakeList.length) {
return { value: fakeList[idx++], done: false };
}
return { value: undefined, done: true };
},
// Iterators in JS must themselves be iterable.
// (Function.prototype[Symbol.iterator] won't help; we add one.)
};
}
// FontFaceSet has [Symbol.iterator], values(), keys(), entries(), forEach,
// size, check(), load(). v0.3 covers the enumeration surface plus check;
// load() falls through to native (it's about loading remote fonts, not
// fingerprinting).
try {
var iterFn = function() {
var it = fontIterator();
// Make the iterator iterable (per JS spec it should be self-iterable).
it[Symbol.iterator] = function() { return it; };
return it;
};
__mochi_register_native__(iterFn, "[Symbol.iterator]");
__mochi_defineProperty__(fonts, Symbol.iterator, {
configurable: true, enumerable: false, writable: true, value: iterFn,
});
} catch (_e) {}
// size getter.
try {
__mochi_defineProperty__(fonts, "size", {
configurable: true,
enumerable: true,
get: function() { return fakeList.length; },
});
} catch (_e) {}
// forEach — emit each fake entry once.
function forEach(cb, thisArg) {
if (typeof cb !== "function") return;
for (var i = 0; i < fakeList.length; i++) {
cb.call(thisArg, fakeList[i], fakeList[i], fonts);
}
}
__mochi_register_native__(forEach, "forEach");
try {
__mochi_defineProperty__(fonts, "forEach", {
configurable: true, enumerable: false, writable: true, value: forEach,
});
} catch (_e) {}
// check(spec) — parses the family from a CSS font shorthand and answers
// true iff that family appears in the spoofed list. Heuristic: take the
// last non-numeric, non-keyword token as the family. Good enough for
// common probe shapes like "12px Arial" or "16px 'Comic Sans MS'".
var origCheck = fonts.check;
function check(spec, _text) {
try {
if (typeof spec !== "string") return false;
// Strip quoted family if present.
var m = spec.match(/['"]([^'"]+)['"]\\s*$/);
var family;
if (m !== null) {
family = m[1];
} else {
// Last whitespace-separated token.
var toks = spec.split(/\\s+/);
family = toks.length > 0 ? toks[toks.length - 1] : "";
}
return !!SPOOF_FAMILIES_LC[String(family).toLowerCase()];
} catch (_e) {
// Fall back to native if our parser failed.
if (typeof origCheck === "function") {
return __mochi_apply__.call(origCheck, fonts, [spec, _text]);
}
return false;
}
}
__mochi_register_native__(check, "check");
try {
__mochi_defineProperty__(fonts, "check", {
configurable: true, enumerable: false, writable: true, value: check,
});
} catch (_e) {}
})();
`;
}
/**
* Spoof module: `navigator.mediaDevices.{enumerateDevices,getSupportedConstraints}`.
*
* Reads from the matrix:
* - `matrix.uaCh["media-devices"]` (R-034) — JSON `[{kind,label}, ...]`
* - `matrix.uaCh["media-supported-constraints"]` (R-035) — JSON map
*
* `deviceId` and `groupId` MUST be deterministic per `(profile, seed)`. We
* derive them via SHA-256(`<profile.id>:<seed>:mediaDevices:<index>:<kind>`)
* truncated to 32 hex chars. This is computed at build time (Bun has
* `crypto.subtle`) and embedded in the payload so the payload itself is
* still byte-stable per (profile, seed).
*
* @see PLAN.md §9.5
* @see tasks/0070-consistency-rules-full.md (media-devices)
*/
import type { MatrixV1 } from "@mochi.js/consistency";
interface DeviceShape {
readonly kind: "audioinput" | "audiooutput" | "videoinput";
readonly label: string;
}
function tryParse<T>(s: unknown): T | null {
if (typeof s !== "string" || s.length === 0) return null;
try {
return JSON.parse(s) as T;
} catch {
return null;
}
}
/**
* Compute a deterministic 32-hex-char ID from a key string. Uses Bun's
* `CryptoHasher` (sync) when available; matches the SHA-256 path used by
* the inject payload's builder.
*/
function sha256Hex(input: string): string {
type CryptoHasherCtor = new (
algo: "sha256",
) => {
update(data: string): void;
digest(encoding: "hex"): string;
};
const maybeBun = (globalThis as { Bun?: { CryptoHasher?: CryptoHasherCtor } }).Bun;
if (maybeBun !== undefined && maybeBun.CryptoHasher !== undefined) {
const h = new maybeBun.CryptoHasher("sha256");
h.update(input);
return h.digest("hex");
}
throw new Error("[mochi/inject] media-devices requires Bun CryptoHasher");
}
export function emitMediaDevicesModule(matrix: MatrixV1): string {
const devices = tryParse<readonly DeviceShape[]>(matrix.uaCh["media-devices"]) ?? [];
const constraints =
tryParse<Record<string, true>>(matrix.uaCh["media-supported-constraints"]) ?? {};
if (devices.length === 0 && Object.keys(constraints).length === 0) {
return `
// ---- mediaDevices spoof (skipped — no matrix.uaCh["media-*"]) -------------
`;
}
// Derive deterministic IDs at build time. The harness's normalize layer
// sentinelizes mediaDevices.devices[*].deviceId/groupId, so the exact
// bytes don't need to match across runs — they just need to be
// structurally consistent and seed-stable.
const profileBase = `${matrix.id}:${matrix.seed}:mediaDevices`;
const enriched = devices.map((d, i) => {
const deviceId = sha256Hex(`${profileBase}:${i}:${d.kind}:deviceId`).slice(0, 64);
const groupId = sha256Hex(`${profileBase}:${i}:${d.kind}:groupId`).slice(0, 64);
return { kind: d.kind, label: d.label, deviceId, groupId };
});
const devicesLiteral = JSON.stringify(enriched);
const constraintsLiteral = JSON.stringify(constraints);
return `
// ---- mediaDevices spoof ----------------------------------------------------
(function() {
if (typeof navigator === "undefined") return;
var md = navigator.mediaDevices;
if (md === undefined || md === null) return;
var SPOOF_DEVICES = ${devicesLiteral};
var SPOOF_CONSTRAINTS = ${constraintsLiteral};
// Build MediaDeviceInfo-shape stand-ins. Real Chrome's MediaDeviceInfo
// exposes deviceId/kind/label/groupId via prototype getters; a plain
// frozen object with the same keys reads identically through probe code.
function buildDeviceInfos() {
var out = [];
for (var i = 0; i < SPOOF_DEVICES.length; i++) {
var s = SPOOF_DEVICES[i];
out.push(Object.freeze({
deviceId: s.deviceId,
kind: s.kind,
label: s.label,
groupId: s.groupId,
toJSON: function() {
return { deviceId: this.deviceId, kind: this.kind, label: this.label, groupId: this.groupId };
},
}));
}
return out;
}
function enumerateDevices() {
return Promise.resolve(buildDeviceInfos());
}
__mochi_register_native__(enumerateDevices, "enumerateDevices");
function getSupportedConstraints() {
// Return a frozen map matching the captured shape.
var out = {};
for (var k in SPOOF_CONSTRAINTS) {
if (Object.prototype.hasOwnProperty.call(SPOOF_CONSTRAINTS, k)) out[k] = true;
}
return out;
}
__mochi_register_native__(getSupportedConstraints, "getSupportedConstraints");
// Patch on the MediaDevices prototype if reachable (matches Chrome's
// native slot layout). Fall back to the instance otherwise.
var proto = __mochi_getPrototypeOf__(md);
var target = proto !== null && proto !== undefined && typeof proto.enumerateDevices === "function"
? proto
: md;
try {
__mochi_defineProperty__(target, "enumerateDevices", {
configurable: true, enumerable: false, writable: true, value: enumerateDevices,
});
} catch (_e) {}
try {
__mochi_defineProperty__(target, "getSupportedConstraints", {
configurable: true, enumerable: false, writable: true, value: getSupportedConstraints,
});
} catch (_e) {}
})();
`;
}
/**
* Spoof module: `navigator.*`.
*
* Reads from the matrix:
* - `matrix.userAgent` → navigator.userAgent
* - `matrix.uaCh["navigator-platform"]` → navigator.platform
* - `matrix.uaCh["navigator-vendor"]` → navigator.vendor
* - `matrix.uaCh["navigator-appVersion"]` → navigator.appVersion
* - `matrix.uaCh["navigator-appCodeName"]` → navigator.appCodeName
* - `matrix.uaCh["navigator-product"]` → navigator.product
* - `matrix.uaCh["navigator-cookieEnabled"]` → navigator.cookieEnabled (string "true"/"false")
* - `matrix.uaCh["navigator-maxTouchPoints"]` → navigator.maxTouchPoints (string "0")
* - `matrix.uaCh["navigator-webdriver"]` → navigator.webdriver (string "true"/"false")
* - `matrix.device.cores` → navigator.hardwareConcurrency
* - `matrix.device.memoryGB` → navigator.deviceMemory
* - `matrix.locale` → navigator.language
* - `matrix.languages` → navigator.languages
*
* The uaCh bag stores values as strings (PLAN.md §6.1 — open-keyed string
* map). Where the spoofed property is logically a boolean or number, this
* module parses the string at build time. If a key is missing, that
* particular property override is skipped — the bare browser value remains
* (PLAN.md I-5: never invent values).
*
* @see tasks/0030-inject-engine-v0.md §"navigator.ts"
* @see PLAN.md §5.3
*/
import type { MatrixV1 } from "@mochi.js/consistency";
/**
* Build the navigator-spoof JS snippet for this matrix. The returned source
* runs inside the master IIFE *after* the runtime helpers are installed.
*/
export function emitNavigatorModule(matrix: MatrixV1): string {
const ua = matrix.uaCh;
const lines: string[] = [];
// navigator.userAgent — always present (top-level matrix slot).
lines.push(line("userAgent", JSON.stringify(matrix.userAgent)));
// navigator.appVersion — UA without leading "Mozilla/" (R-026).
const appVersion = ua["navigator-appVersion"];
if (typeof appVersion === "string") {
lines.push(line("appVersion", JSON.stringify(appVersion)));
}
// navigator.platform — "MacIntel"/"Win32"/"Linux x86_64" (R-017).
const platform = ua["navigator-platform"];
if (typeof platform === "string") {
lines.push(line("platform", JSON.stringify(platform)));
}
// navigator.vendor — "Google Inc." for chromium-family (R-018).
const vendor = ua["navigator-vendor"];
if (typeof vendor === "string") {
lines.push(line("vendor", JSON.stringify(vendor)));
}
// navigator.appCodeName — "Mozilla" universally (R-027).
const appCodeName = ua["navigator-appCodeName"];
if (typeof appCodeName === "string") {
lines.push(line("appCodeName", JSON.stringify(appCodeName)));
}
// navigator.product — "Gecko" universally (R-028).
const product = ua["navigator-product"];
if (typeof product === "string") {
lines.push(line("product", JSON.stringify(product)));
}
// navigator.cookieEnabled — boolean (R-030).
const cookieEnabled = ua["navigator-cookieEnabled"];
if (typeof cookieEnabled === "string") {
const b = cookieEnabled === "true";
lines.push(line("cookieEnabled", b ? "true" : "false"));
}
// navigator.maxTouchPoints — number (R-020).
const maxTouchPoints = ua["navigator-maxTouchPoints"];
if (typeof maxTouchPoints === "string") {
const n = Number.parseInt(maxTouchPoints, 10);
if (Number.isFinite(n)) {
lines.push(line("maxTouchPoints", String(n)));
}
}
// navigator.webdriver — boolean (R-022). Always returns false on real Chrome.
const webdriver = ua["navigator-webdriver"];
if (typeof webdriver === "string") {
const b = webdriver === "true";
lines.push(line("webdriver", b ? "true" : "false"));
}
// navigator.hardwareConcurrency — number (R-008). Use device.cores.
lines.push(line("hardwareConcurrency", String(matrix.device.cores)));
// navigator.deviceMemory — number (R-009). Capped at 8.
lines.push(line("deviceMemory", String(matrix.device.memoryGB)));
// navigator.language — string (R-015).
lines.push(line("language", JSON.stringify(matrix.locale)));
// navigator.languages — frozen array (R-016). Use a fresh frozen Array
// each call so page code can't mutate it; matches Chrome's behaviour
// (Chrome returns an array reference but the slot is a getter so each
// access returns a fresh array; we mimic that).
const langsLiteral = `Object.freeze([${matrix.languages
.map((l) => JSON.stringify(l))
.join(",")}])`;
lines.push(line("languages", langsLiteral));
return `
// ---- navigator spoof -------------------------------------------------------
(function() {
var __nav__ = navigator;
var __navProto__ = __mochi_getPrototypeOf__(__nav__);
${lines.join("\n")}
})();
`;
}
/**
* Helper: emit one defineProperty call against `__nav__` AND its prototype,
* mirroring Chrome's slot layout (most navigator properties live on the
* Navigator.prototype, but page script reads them from the instance — so
* defining only the prototype is safe and matches native shape).
*
* We define on the prototype because navigator's own descriptor is
* empty for these properties — they're all inherited. Defining on the
* instance directly creates a "shadowing" own property that fingerprint
* libraries can detect via `Object.getOwnPropertyNames(navigator)`.
*
* Caveat: `__mochi_define__` already walks the prototype chain to find the
* descriptor's enumerability, so passing `__navProto__` keeps the slot on
* the prototype where it belongs.
*/
function line(prop: string, valueExpr: string): string {
return ` __mochi_define__(__navProto__, ${JSON.stringify(prop)}, ${valueExpr});`;
}
/**
* Spoof module: `navigator.connection` (Network Information API).
*
* Reads from the matrix:
* - `matrix.uaCh["connection"]` (R-037) — JSON
* `{effectiveType, downlink, rtt, saveData}`.
*
* Defines `navigator.connection.{effectiveType, downlink, rtt, saveData,
* type}` so that probes pulling these values get matrix-locked answers.
* Chrome's native `connection` is a `NetworkInformation` instance — we
* don't reconstruct the full prototype; the probe page only reads the
* four (now five) properties below.
*
* @see tasks/0070-consistency-rules-full.md (network-info)
*/
import type { MatrixV1 } from "@mochi.js/consistency";
interface Connection {
readonly effectiveType?: string;
readonly downlink?: number;
readonly rtt?: number;
readonly saveData?: boolean;
}
function tryParse<T>(s: unknown): T | null {
if (typeof s !== "string" || s.length === 0) return null;
try {
return JSON.parse(s) as T;
} catch {
return null;
}
}
export function emitNetworkInfoModule(matrix: MatrixV1): string {
const conn = tryParse<Connection>(matrix.uaCh.connection) ?? {};
if (
conn.effectiveType === undefined &&
conn.downlink === undefined &&
conn.rtt === undefined &&
conn.saveData === undefined
) {
return `
// ---- network-info spoof (skipped — no matrix.uaCh["connection"]) ----------
`;
}
const effectiveType = conn.effectiveType ?? "4g";
const downlink = typeof conn.downlink === "number" ? conn.downlink : 10;
const rtt = typeof conn.rtt === "number" ? conn.rtt : 50;
const saveData = conn.saveData === true;
return `
// ---- network-info spoof ----------------------------------------------------
(function() {
if (typeof navigator === "undefined") return;
var c = navigator.connection;
if (c === undefined || c === null) return;
// The values are accessor-style on real Chrome's NetworkInformation
// prototype; redefining on the instance is the simplest faithful match.
// configurable:false matches __mochi_define__'s contract.
__mochi_define__(c, "effectiveType", ${JSON.stringify(effectiveType)});
__mochi_define__(c, "downlink", ${downlink});
__mochi_define__(c, "rtt", ${rtt});
__mochi_define__(c, "saveData", ${saveData ? "true" : "false"});
})();
`;
}
/**
* Spoof module: `Permissions.prototype.query({name})`.
*
* Reads from the matrix:
* - `matrix.uaCh["permissions-defaults"]` (R-036) — JSON map of
* `<name>` → `"granted" | "prompt" | "denied"`.
*
* Replaces `Permissions.prototype.query` with a wrapper that returns a
* Promise resolving to a `PermissionStatus`-shape object whose `state`
* property comes from the matrix map. Names not in the map fall through
* to the original (preserves Chrome's "unsupported permission" behaviour).
*
* The returned `PermissionStatus` exposes `.state`, `.name`, and
* `.onchange = null` to satisfy the probe-page surface.
*
* @see PLAN.md §9.5
* @see tasks/0070-consistency-rules-full.md (permissions)
*/
import type { MatrixV1 } from "@mochi.js/consistency";
function tryParse<T>(s: unknown): T | null {
if (typeof s !== "string" || s.length === 0) return null;
try {
return JSON.parse(s) as T;
} catch {
return null;
}
}
export function emitPermissionsModule(matrix: MatrixV1): string {
const defaults = tryParse<Record<string, string>>(matrix.uaCh["permissions-defaults"]) ?? {};
if (Object.keys(defaults).length === 0) {
return `
// ---- permissions spoof (skipped — no matrix.uaCh["permissions-defaults"]) -
`;
}
const defaultsLiteral = JSON.stringify(defaults);
return `
// ---- permissions spoof -----------------------------------------------------
(function() {
if (typeof navigator === "undefined") return;
var perms = navigator.permissions;
if (perms === undefined || perms === null) return;
if (typeof Permissions === "undefined") return;
var proto = Permissions.prototype;
if (proto === undefined || proto === null) return;
var orig = proto.query;
if (typeof orig !== "function") return;
var SPOOF_DEFAULTS = ${defaultsLiteral};
function makeStatus(name, state) {
// PermissionStatus is an EventTarget — we don't recreate the full
// prototype, but the probe-page only reads .state. Add .name +
// .onchange = null so anti-bot heuristics that check for those see
// them.
var status = Object.create(null);
Object.defineProperty(status, "state", {
configurable: true, enumerable: true, get: function() { return state; },
});
Object.defineProperty(status, "name", {
configurable: true, enumerable: true, get: function() { return name; },
});
status.onchange = null;
status.addEventListener = function() {};
status.removeEventListener = function() {};
status.dispatchEvent = function() { return true; };
return status;
}
function query(descriptor) {
try {
var name = descriptor && descriptor.name;
if (typeof name === "string" && Object.prototype.hasOwnProperty.call(SPOOF_DEFAULTS, name)) {
return Promise.resolve(makeStatus(name, SPOOF_DEFAULTS[name]));
}
} catch (_e) {}
// Fall through to native — preserves "unsupported permission" rejection.
return __mochi_apply__.call(orig, this, [descriptor]);
}
__mochi_register_native__(query, "query");
try {
__mochi_defineProperty__(proto, "query", {
configurable: true, enumerable: false, writable: true, value: query,
});
} catch (_e) {}
})();
`;
}
/**
* Spoof module: `navigator.plugins` + `navigator.mimeTypes`.
*
* Real Chrome 92+ ships a curated 5-plugin PluginArray (see
* `packages/profiles/data/<id>/baseline.manifest.json` `navigator.plugins`):
*
* 1. PDF Viewer internal-pdf-viewer application/pdf, text/pdf
* 2. Chrome PDF Viewer internal-pdf-viewer application/pdf, text/pdf
* 3. Chromium PDF Viewer internal-pdf-viewer application/pdf, text/pdf
* 4. Microsoft Edge PDF internal-pdf-viewer application/pdf, text/pdf
* 5. WebKit built-in PDF internal-pdf-viewer application/pdf, text/pdf
*
* Stock Chromium-for-Testing builds may report `plugins.length === 0`
* depending on flag set, which CloakBrowser's `test_plugins_present`
* (line 52) flags as automation. The shim is **defensive** — we only
* install our PluginArray if the underlying browser reports an empty
* list. When the underlying browser already exposes ≥ 5 plugins (the
* common case on real Chrome.app), we leave them alone so the existing
* Probe Manifest Zero-Diff gate doesn't regress.
*
* The shim builds a frozen array-like object with:
* - `.length`, integer-keyed access, `Symbol.iterator`
* - `namedItem(name)`, `item(idx)` — PluginArray API surface
* - per-plugin `.length`, `.namedItem`, `.item` — Plugin API surface
* - per-mimetype `application/pdf` / `text/pdf` entries with `.enabledPlugin`
*
* Future: we may key the catalog off `matrix.os.name` to vary plugins
* per OS (Windows / Linux Chrome ship the same 5 in 2026), but for v1
* the catalog is profile-invariant.
*
* @see CloakBrowser tests/test_stealth.py:52-56
* @see packages/profiles/data/mac-m4-chrome-stable/baseline.manifest.json
* @see tasks/0140-stealth-conformance.md §"plugins spoof"
*/
import type { MatrixV1 } from "@mochi.js/consistency";
interface PluginShape {
readonly name: string;
readonly filename: string;
readonly description: string;
readonly mimeTypes: readonly { readonly type: string; readonly suffixes: string }[];
}
/**
* Curated catalog. Mirrors the baseline-captured list in
* `packages/profiles/data/mac-m4-chrome-stable/baseline.manifest.json`.
*
* The mime-type list is the deduplicated union across all 5 plugins —
* real Chrome's `navigator.mimeTypes` shows 2 entries (`application/pdf`,
* `text/pdf`), each pointing to the first plugin that registered it.
*/
const CHROMIUM_PLUGINS: readonly PluginShape[] = [
{
name: "PDF Viewer",
filename: "internal-pdf-viewer",
description: "Portable Document Format",
mimeTypes: [
{ type: "application/pdf", suffixes: "pdf" },
{ type: "text/pdf", suffixes: "pdf" },
],
},
{
name: "Chrome PDF Viewer",
filename: "internal-pdf-viewer",
description: "Portable Document Format",
mimeTypes: [
{ type: "application/pdf", suffixes: "pdf" },
{ type: "text/pdf", suffixes: "pdf" },
],
},
{
name: "Chromium PDF Viewer",
filename: "internal-pdf-viewer",
description: "Portable Document Format",
mimeTypes: [
{ type: "application/pdf", suffixes: "pdf" },
{ type: "text/pdf", suffixes: "pdf" },
],
},
{
name: "Microsoft Edge PDF Viewer",
filename: "internal-pdf-viewer",
description: "Portable Document Format",
mimeTypes: [
{ type: "application/pdf", suffixes: "pdf" },
{ type: "text/pdf", suffixes: "pdf" },
],
},
{
name: "WebKit built-in PDF",
filename: "internal-pdf-viewer",
description: "Portable Document Format",
mimeTypes: [
{ type: "application/pdf", suffixes: "pdf" },
{ type: "text/pdf", suffixes: "pdf" },
],
},
] as const;
export function emitPluginsModule(matrix: MatrixV1): string {
if (matrix.engine !== "chromium") {
return `\n// ---- plugins spoof (skipped — non-chromium engine) -----------------------\n`;
}
const pluginsLiteral = JSON.stringify(CHROMIUM_PLUGINS);
return `
// ---- plugins spoof ---------------------------------------------------------
(function() {
if (typeof navigator === "undefined") return;
// Defensive: only install if the underlying browser reports an empty list.
// Real Chrome.app ships the same 5-plugin PluginArray natively; overwriting
// it would regress the harness Zero-Diff gate.
try {
if (navigator.plugins !== undefined && navigator.plugins !== null) {
var existingLen = navigator.plugins.length;
if (typeof existingLen === "number" && existingLen >= 5) {
return;
}
}
} catch (_e) { /* fall through */ }
var SPOOF_PLUGINS = ${pluginsLiteral};
// Build mimetype objects. Real Chrome dedupes — each mimetype's
// .enabledPlugin points at the first plugin that registered it.
var mimeByType = {};
function makeMimeType(mt, plugin) {
var obj = Object.create(null);
obj.type = mt.type;
obj.suffixes = mt.suffixes;
obj.description = "";
Object.defineProperty(obj, "enabledPlugin", {
configurable: true, enumerable: true, get: function() { return plugin; },
});
return obj;
}
function makePlugin(spec) {
var plug = Object.create(null);
plug.name = spec.name;
plug.filename = spec.filename;
plug.description = spec.description;
var mimes = [];
for (var i = 0; i < spec.mimeTypes.length; i++) {
var mt = makeMimeType(spec.mimeTypes[i], plug);
mimes.push(mt);
// First-registered mimetype wins for the global mimeTypes array.
if (mimeByType[mt.type] === undefined) mimeByType[mt.type] = mt;
}
plug.length = mimes.length;
for (var j = 0; j < mimes.length; j++) {
plug[String(j)] = mimes[j];
plug[mimes[j].type] = mimes[j];
}
plug.item = function(idx) { return mimes[idx] || null; };
__mochi_register_native__(plug.item, "item");
plug.namedItem = function(name) {
for (var k = 0; k < mimes.length; k++) {
if (mimes[k].type === name) return mimes[k];
}
return null;
};
__mochi_register_native__(plug.namedItem, "namedItem");
return plug;
}
var plugins = [];
for (var i = 0; i < SPOOF_PLUGINS.length; i++) {
plugins.push(makePlugin(SPOOF_PLUGINS[i]));
}
// Build a PluginArray-like object. Real Chrome's PluginArray.length,
// .item, .namedItem, .refresh are all on the prototype; for the shim
// we put them on the instance (the assertion is on length and indexed
// access, both of which work the same way).
var pluginArr = Object.create(null);
pluginArr.length = plugins.length;
for (var p = 0; p < plugins.length; p++) {
pluginArr[String(p)] = plugins[p];
pluginArr[plugins[p].name] = plugins[p];
}
pluginArr.item = function(idx) { return plugins[idx] || null; };
__mochi_register_native__(pluginArr.item, "item");
pluginArr.namedItem = function(name) {
for (var k = 0; k < plugins.length; k++) {
if (plugins[k].name === name) return plugins[k];
}
return null;
};
__mochi_register_native__(pluginArr.namedItem, "namedItem");
pluginArr.refresh = function() {};
__mochi_register_native__(pluginArr.refresh, "refresh");
pluginArr[Symbol.iterator] = function() {
var i = 0;
return {
next: function() {
if (i < plugins.length) return { value: plugins[i++], done: false };
return { value: undefined, done: true };
},
};
};
// Build a MimeTypeArray-like object from the deduplicated map.
var mimeKeys = Object.keys(mimeByType);
var mimeArr = Object.create(null);
mimeArr.length = mimeKeys.length;
for (var m = 0; m < mimeKeys.length; m++) {
mimeArr[String(m)] = mimeByType[mimeKeys[m]];
mimeArr[mimeKeys[m]] = mimeByType[mimeKeys[m]];
}
mimeArr.item = function(idx) {
var k = mimeKeys[idx]; return k ? mimeByType[k] : null;
};
__mochi_register_native__(mimeArr.item, "item");
mimeArr.namedItem = function(name) { return mimeByType[name] || null; };
__mochi_register_native__(mimeArr.namedItem, "namedItem");
mimeArr[Symbol.iterator] = function() {
var idx = 0;
return {
next: function() {
if (idx < mimeKeys.length) return { value: mimeByType[mimeKeys[idx++]], done: false };
return { value: undefined, done: true };
},
};
};
// Install on the Navigator prototype so getOwnPropertyNames(navigator)
// doesn't show new own properties (mirrors how the navigator module
// installs its accessors).
var navProto = __mochi_getPrototypeOf__(navigator);
try {
__mochi_define__(navProto, "plugins", pluginArr);
} catch (_e) {}
try {
__mochi_define__(navProto, "mimeTypes", mimeArr);
} catch (_e) {}
})();
`;
}
/**
* Spoof module: `screen.orientation` and `window.matchMedia(spec)` answers.
*
* Reads from the matrix:
* - `matrix.uaCh["screen-orientation"]` (R-038) — JSON `{type, angle}`
* - `matrix.uaCh["media-queries"]` (R-039) — JSON map of feature →
* answer
* - `matrix.uaCh["storage-estimate"]` (R-040) — JSON `{quota, usage}`.
* Optional: when present, also overrides
* `navigator.storage.estimate()`.
*
* `screen.orientation`: Chrome exposes a `ScreenOrientation` instance with
* `.type` and `.angle` accessor properties, plus event-target methods. We
* only override `.type` and `.angle` — that's what fingerprint probes read.
*
* `window.matchMedia(spec)`: wrap so that for each tracked feature, the
* returned `MediaQueryList` reports `matches` according to the matrix's
* curated answer. Unknown specs fall through to native.
*
* @see tasks/0070-consistency-rules-full.md (screen-orientation)
*/
import type { MatrixV1 } from "@mochi.js/consistency";
interface OrientationShape {
readonly type?: string;
readonly angle?: number;
}
function tryParse<T>(s: unknown): T | null {
if (typeof s !== "string" || s.length === 0) return null;
try {
return JSON.parse(s) as T;
} catch {
return null;
}
}
export function emitScreenOrientationModule(matrix: MatrixV1): string {
const orientation = tryParse<OrientationShape>(matrix.uaCh["screen-orientation"]);
const mediaQueries =
tryParse<Record<string, string | boolean>>(matrix.uaCh["media-queries"]) ?? {};
const storageEstimate = tryParse<{ quota?: number; usage?: number }>(
matrix.uaCh["storage-estimate"],
);
const orientationLiteral = orientation
? JSON.stringify({
type: typeof orientation.type === "string" ? orientation.type : "landscape-primary",
angle: typeof orientation.angle === "number" ? orientation.angle : 0,
})
: "null";
const mediaQueriesLiteral = JSON.stringify(mediaQueries);
const storageEstimateLiteral = storageEstimate
? JSON.stringify({
quota: typeof storageEstimate.quota === "number" ? storageEstimate.quota : 0,
usage: typeof storageEstimate.usage === "number" ? storageEstimate.usage : 0,
})
: "null";
return `
// ---- screen.orientation + matchMedia + storage.estimate spoof -------------
(function() {
// -- screen.orientation -------------------------------------------------
try {
var SPOOF_ORIENTATION = ${orientationLiteral};
if (typeof screen !== "undefined" && SPOOF_ORIENTATION !== null) {
var orientation = screen.orientation;
if (orientation !== undefined && orientation !== null) {
__mochi_define__(orientation, "type", SPOOF_ORIENTATION.type);
__mochi_define__(orientation, "angle", SPOOF_ORIENTATION.angle);
}
}
} catch (_e) {}
// -- matchMedia answers --------------------------------------------------
try {
if (typeof window !== "undefined" && typeof window.matchMedia === "function") {
var SPOOF_MQ = ${mediaQueriesLiteral};
var origMatchMedia = window.matchMedia;
function matchMedia(spec) {
try {
if (typeof spec !== "string") return __mochi_apply__.call(origMatchMedia, this, [spec]);
var s = spec.replace(/^\\(|\\)$/g, "").trim();
// Forms: "feature: value" | "feature" | "min-resolution: <n>dpi"
var colon = s.indexOf(":");
var feature, queryValue;
if (colon === -1) {
feature = s.trim();
queryValue = null;
} else {
feature = s.slice(0, colon).trim();
queryValue = s.slice(colon + 1).trim();
}
if (Object.prototype.hasOwnProperty.call(SPOOF_MQ, feature)) {
var spoof = SPOOF_MQ[feature];
var matches;
if (queryValue === null) {
// Boolean form: "(feature)" matches when spoof is truthy.
matches = spoof === true;
} else if (feature === "min-resolution") {
// "min-resolution: <N>dpi" matches when query <= spoof's dpi.
var qN = parseFloat(queryValue);
var sN = parseFloat(String(spoof));
matches = !isNaN(qN) && !isNaN(sN) && sN >= qN;
} else {
matches = String(spoof) === queryValue;
}
// Build a MediaQueryList-shape stand-in. The probe page reads
// .matches and .media; the rest are EventTarget no-ops.
var mql = Object.create(null);
__mochi_defineProperty__(mql, "matches", {
configurable: true, enumerable: true, get: function() { return matches; },
});
__mochi_defineProperty__(mql, "media", {
configurable: true, enumerable: true, get: function() { return spec; },
});
mql.onchange = null;
mql.addEventListener = function() {};
mql.removeEventListener = function() {};
mql.addListener = function() {};
mql.removeListener = function() {};
mql.dispatchEvent = function() { return true; };
return mql;
}
} catch (_e) {}
return __mochi_apply__.call(origMatchMedia, this, [spec]);
}
__mochi_register_native__(matchMedia, "matchMedia");
try {
__mochi_defineProperty__(window, "matchMedia", {
configurable: true, enumerable: true, writable: true, value: matchMedia,
});
} catch (_e) {}
}
} catch (_e) {}
// -- storage.estimate ----------------------------------------------------
try {
var SPOOF_SE = ${storageEstimateLiteral};
if (SPOOF_SE !== null && typeof navigator !== "undefined" && navigator.storage) {
var storage = navigator.storage;
if (typeof storage.estimate === "function") {
function estimate() {
return Promise.resolve({ quota: SPOOF_SE.quota, usage: SPOOF_SE.usage });
}
__mochi_register_native__(estimate, "estimate");
try {
var sproto = __mochi_getPrototypeOf__(storage);
var starget = sproto !== null && sproto !== undefined && typeof sproto.estimate === "function"
? sproto
: storage;
__mochi_defineProperty__(starget, "estimate", {
configurable: true, enumerable: false, writable: true, value: estimate,
});
} catch (_e) {}
}
}
} catch (_e) {}
})();
`;
}
/**
* Spoof module: `screen.*` and `window.{innerWidth,innerHeight,outerWidth,
* outerHeight,devicePixelRatio}`.
*
* Reads from the matrix:
* - `matrix.display.width/height/colorDepth/pixelDepth/dpr` (R-010..R-012)
* - `matrix.uaCh["screen-availSize"]` (R-021) — JSON `{availWidth, availHeight}`
* - `matrix.uaCh["window-viewport"]` (R-029) — JSON `{innerWidth, innerHeight, outerWidth, outerHeight}`
*
* Missing uaCh keys are skipped (PLAN.md I-5).
*
* @see tasks/0030-inject-engine-v0.md §"screen.ts"
*/
import type { MatrixV1 } from "@mochi.js/consistency";
/** Best-effort JSON parse — returns null on any failure. */
function tryParse(s: unknown): unknown {
if (typeof s !== "string") return null;
try {
return JSON.parse(s);
} catch {
return null;
}
}
function isFiniteNumber(n: unknown): n is number {
return typeof n === "number" && Number.isFinite(n);
}
export function emitScreenModule(matrix: MatrixV1): string {
const screenLines: string[] = [];
const winLines: string[] = [];
// screen.width / screen.height — from display.
screenLines.push(` __mochi_define__(__sp__, "width", ${matrix.display.width});`);
screenLines.push(` __mochi_define__(__sp__, "height", ${matrix.display.height});`);
// screen.colorDepth / screen.pixelDepth — from display.
screenLines.push(` __mochi_define__(__sp__, "colorDepth", ${matrix.display.colorDepth});`);
screenLines.push(` __mochi_define__(__sp__, "pixelDepth", ${matrix.display.pixelDepth});`);
// screen.availWidth/availHeight — derived in R-021.
const avail = tryParse(matrix.uaCh["screen-availSize"]) as {
availWidth?: number;
availHeight?: number;
} | null;
if (avail !== null) {
if (isFiniteNumber(avail.availWidth)) {
screenLines.push(` __mochi_define__(__sp__, "availWidth", ${avail.availWidth});`);
}
if (isFiniteNumber(avail.availHeight)) {
screenLines.push(` __mochi_define__(__sp__, "availHeight", ${avail.availHeight});`);
}
}
// window.devicePixelRatio — from display.dpr (R-012). DPR lives on the
// Window instance, not a prototype — Chrome returns it via a getter on
// Window. Defining on the instance is the correct match.
winLines.push(` __mochi_define__(window, "devicePixelRatio", ${matrix.display.dpr});`);
// window.{innerWidth,innerHeight,outerWidth,outerHeight} — R-029 JSON.
const vp = tryParse(matrix.uaCh["window-viewport"]) as {
innerWidth?: number;
innerHeight?: number;
outerWidth?: number;
outerHeight?: number;
} | null;
if (vp !== null) {
for (const key of ["innerWidth", "innerHeight", "outerWidth", "outerHeight"] as const) {
const v = vp[key];
if (isFiniteNumber(v)) {
winLines.push(` __mochi_define__(window, ${JSON.stringify(key)}, ${v});`);
}
}
}
return `
// ---- screen + viewport spoof -----------------------------------------------
(function() {
var __sp__ = __mochi_getPrototypeOf__(screen);
${screenLines.join("\n")}
${winLines.join("\n")}
})();
`;
}
/**
* Spoof module: `Intl.DateTimeFormat().resolvedOptions().timeZone` and
* `Date.prototype.getTimezoneOffset` / related timezone-derived APIs.
*
* Reads from the matrix:
* - `matrix.timezone` (R-014) — IANA timezone identifier
*
* v0.3 strategy:
* - Replace `Intl.DateTimeFormat.prototype.resolvedOptions` with a wrapper
* that overrides `timeZone` on the returned options object. Other
* fields pass through.
* - Don't spoof `performance.now()` precision (PLAN.md §9.6 — Chrome's
* natural 100µs coarsening is what we want for same-engine v1).
* - Don't override `Date.prototype.getTimezoneOffset` — when Chrome is
* launched with `TZ=<iana>` (or via the system clock), the Date object
* already produces correct offsets. Inject-layer spoofing of
* getTimezoneOffset would conflict with the page's own Date math. The
* core launch path is responsible for setting `TZ` if requested
* (deferred to phase 0.7 / harness work; documented in docs/limits.md).
*
* @see tasks/0030-inject-engine-v0.md §"timing.ts"
*/
import type { MatrixV1 } from "@mochi.js/consistency";
export function emitTimingModule(matrix: MatrixV1): string {
const tz = JSON.stringify(matrix.timezone);
return `
// ---- timing spoof (timezone) -----------------------------------------------
(function() {
if (typeof Intl === "undefined" || typeof Intl.DateTimeFormat !== "function") return;
var SPOOF_TZ = ${tz};
var proto = Intl.DateTimeFormat.prototype;
var origResolved = proto.resolvedOptions;
if (typeof origResolved !== "function") return;
function resolvedOptions() {
var opts = __mochi_apply__.call(origResolved, this, []);
if (opts !== null && opts !== undefined && typeof opts === "object") {
try { opts.timeZone = SPOOF_TZ; } catch (_e) {}
}
return opts;
}
__mochi_register_native__(resolvedOptions, "resolvedOptions");
try {
__mochi_defineProperty__(proto, "resolvedOptions", {
configurable: true,
enumerable: false,
writable: true,
value: resolvedOptions,
});
} catch (_e) {}
})();
`;
}
/**
* Spoof module: `WebGLRenderingContext.prototype.getParameter` +
* `WebGL2RenderingContext.prototype.getParameter`.
*
* Replaces the prototype's `getParameter` with a wrapper that:
* - returns the matrix value for `UNMASKED_VENDOR_WEBGL` (37445),
* `UNMASKED_RENDERER_WEBGL` (37446), `MAX_TEXTURE_SIZE` (3379),
* and `MAX_COLOR_ATTACHMENTS` (36063, WebGL2 only).
* - falls through to the original `getParameter` for all other queries.
*
* The `WEBGL_debug_renderer_info` extension's masked constants (37445/37446)
* are queried via `getParameter` directly on the context — Chrome returns
* the unmasked vendor/renderer regardless of whether the extension was
* obtained, in headless / non-strict mode. The matrix's
* `gpu.webglUnmaskedVendor/Renderer` are the spoofed values.
*
* Caveats:
* - `WebGL2RenderingContext.prototype.getParameter` is a separate slot
* (it overrides the WebGL1 inherited method in some Chrome builds; in
* others it shares — we patch both defensively).
* - We don't currently spoof the `WEBGL_debug_renderer_info` extension's
* `getParameter` path (that's the same path; both routes call the
* proto's getParameter which we've patched).
*
* @see tasks/0030-inject-engine-v0.md §"webgl.ts"
*/
import type { MatrixV1 } from "@mochi.js/consistency";
/** GLenum constants we care about. Hard-coded to avoid Chrome dependence. */
const UNMASKED_VENDOR_WEBGL = 0x9245; // 37445
const UNMASKED_RENDERER_WEBGL = 0x9246; // 37446
const MAX_TEXTURE_SIZE = 0x0d33; // 3379
const MAX_COLOR_ATTACHMENTS = 0x8cdf; // 36063 (WebGL2)
export function emitWebglModule(matrix: MatrixV1): string {
const vendor = JSON.stringify(matrix.gpu.webglUnmaskedVendor);
const renderer = JSON.stringify(matrix.gpu.webglUnmaskedRenderer);
const maxTex = String(matrix.gpu.webglMaxTextureSize);
const maxAttach = String(matrix.gpu.webglMaxColorAttachments);
return `
// ---- WebGL spoof -----------------------------------------------------------
(function() {
var UNMASKED_VENDOR_WEBGL = ${UNMASKED_VENDOR_WEBGL};
var UNMASKED_RENDERER_WEBGL = ${UNMASKED_RENDERER_WEBGL};
var MAX_TEXTURE_SIZE = ${MAX_TEXTURE_SIZE};
var MAX_COLOR_ATTACHMENTS = ${MAX_COLOR_ATTACHMENTS};
var SPOOF_VENDOR = ${vendor};
var SPOOF_RENDERER = ${renderer};
var SPOOF_MAX_TEX = ${maxTex};
var SPOOF_MAX_ATTACH = ${maxAttach};
/**
* Patch one prototype's getParameter slot. The wrapper preserves
* \`this\` and forwards to the original for non-spoofed pnames.
*/
function patch(proto, isWebgl2) {
if (proto === undefined || proto === null) return;
var orig = proto.getParameter;
if (typeof orig !== "function") return;
function getParameter(pname) {
if (pname === UNMASKED_VENDOR_WEBGL) return SPOOF_VENDOR;
if (pname === UNMASKED_RENDERER_WEBGL) return SPOOF_RENDERER;
if (pname === MAX_TEXTURE_SIZE) return SPOOF_MAX_TEX;
if (isWebgl2 && pname === MAX_COLOR_ATTACHMENTS) return SPOOF_MAX_ATTACH;
// Use Function.prototype.call.call (apply form) on the captured original
// to defend against any later page-side mutation of orig.call.
return __mochi_apply__.call(orig, this, [pname]);
}
__mochi_register_native__(getParameter, "getParameter");
// Replace the proto slot. configurable:true matches Chrome native.
try {
__mochi_defineProperty__(proto, "getParameter", {
configurable: true,
enumerable: false,
writable: true,
value: getParameter,
});
} catch (_e) {}
}
if (typeof WebGLRenderingContext !== "undefined") {
patch(WebGLRenderingContext.prototype, false);
}
if (typeof WebGL2RenderingContext !== "undefined") {
patch(WebGL2RenderingContext.prototype, true);
}
})();
`;
}
/**
* Spoof module: `navigator.gpu.requestAdapter()` adapter shape.
*
* Reads from the matrix:
* - `matrix.uaCh["webgpu-features"]` (R-032) — JSON string array
* - `matrix.uaCh["webgpu-info"]` (R-033) — JSON `{vendor, architecture,
* device, description}`
*
* Spoofs `adapter.features` (a `GPUSupportedFeatures` set) and
* `adapter.info` (a `GPUAdapterInfo` shape) on the adapter Promise that
* `requestAdapter()` returns. Falls through to the original adapter when
* the matrix is missing the keys.
*
* Adapter `isFallbackAdapter` is forced false (real-hardware behaviour).
*
* Implementation strategy:
* - Wrap `GPU.prototype.requestAdapter` (where `GPU` is the type of
* `navigator.gpu`). When the wrapped Promise resolves, return a Proxy
* over the adapter that intercepts `features`, `info`, and
* `isFallbackAdapter` reads.
* - When `requestAdapter` returns null (e.g. no GPU available in the
* headless capture), build a synthetic adapter from the matrix data.
* The probe-page only reads `features` / `info` / `isFallbackAdapter`,
* so we don't need a full adapter implementation.
*
* @see tasks/0070-consistency-rules-full.md (webgpu)
* @see PLAN.md §9.5
*/
import type { MatrixV1 } from "@mochi.js/consistency";
interface WebGpuInfo {
readonly vendor: string;
readonly architecture: string;
readonly device: string;
readonly description: string;
}
function tryParse<T>(s: unknown): T | null {
if (typeof s !== "string" || s.length === 0) return null;
try {
return JSON.parse(s) as T;
} catch {
return null;
}
}
export function emitWebgpuModule(matrix: MatrixV1): string {
const features = tryParse<readonly string[]>(matrix.uaCh["webgpu-features"]) ?? [];
const info = tryParse<WebGpuInfo>(matrix.uaCh["webgpu-info"]) ?? {
vendor: "",
architecture: "",
device: "",
description: "",
};
// If the matrix carries no WebGPU data, skip — emit a comment.
if (features.length === 0 && info.vendor === "") {
return `
// ---- WebGPU spoof (skipped — no matrix.uaCh["webgpu-*"]) ------------------
`;
}
const featuresLiteral = JSON.stringify(features);
const infoLiteral = JSON.stringify(info);
return `
// ---- WebGPU spoof ----------------------------------------------------------
(function() {
if (typeof navigator === "undefined") return;
var gpu = navigator.gpu;
if (gpu === undefined || gpu === null) return;
var SPOOF_FEATURES = ${featuresLiteral};
var SPOOF_INFO = ${infoLiteral};
// Build the spoofed adapter. We don't need to satisfy the full GPUAdapter
// interface — the probe-page reads .features, .info, .isFallbackAdapter
// only. Real adapter methods (requestDevice etc.) fall through to the
// wrapped original via Proxy when an underlying adapter exists.
function buildSpoofFeatures() {
// adapter.features is a GPUSupportedFeatures (Set-like). Build a Set
// populated with the spoofed strings; iteration order matches insertion.
var s = new Set();
for (var i = 0; i < SPOOF_FEATURES.length; i++) s.add(SPOOF_FEATURES[i]);
return s;
}
function buildSpoofInfo() {
// GPUAdapterInfo is dictionary-shaped; freezing prevents page mutation.
return Object.freeze({
vendor: SPOOF_INFO.vendor,
architecture: SPOOF_INFO.architecture,
device: SPOOF_INFO.device,
description: SPOOF_INFO.description,
});
}
function wrapAdapter(realAdapter) {
// The probe checks adapter && adapter.features etc. Use a Proxy so all
// unhandled property accesses pass through to the real adapter.
var spoofFeatures = buildSpoofFeatures();
var spoofInfo = buildSpoofInfo();
if (realAdapter === null || realAdapter === undefined) {
// No real adapter — return a synthetic stub that satisfies the
// probe-page surface. requestDevice() resolves to undefined so
// probes that try to instantiate a device get an unhelpful (but
// non-throwing) result. isFallbackAdapter is intentionally omitted
// so the probe-page's \`adapter.isFallbackAdapter\` read returns
// undefined — matching the captured baseline shape on the M4 capture.
return {
features: spoofFeatures,
info: spoofInfo,
requestDevice: function() { return Promise.resolve(undefined); },
};
}
return new Proxy(realAdapter, {
get: function(target, prop) {
if (prop === "features") return spoofFeatures;
if (prop === "info") return spoofInfo;
// Pass through isFallbackAdapter — Chromium versions vary on whether
// the property exists. Returning the real value keeps the harness
// diff aligned with the captured baseline shape.
var v = target[prop];
if (typeof v === "function") return v.bind(target);
return v;
},
});
}
// Wrap GPU.prototype.requestAdapter so the returned Promise resolves to
// the spoofed adapter. Fall back to gpu.requestAdapter directly if the
// prototype isn't reachable (test sandboxes).
var proto = __mochi_getPrototypeOf__(gpu);
var target = proto !== null && proto !== undefined && typeof proto.requestAdapter === "function"
? proto
: gpu;
var orig = target.requestAdapter;
if (typeof orig !== "function") return;
function requestAdapter(opts) {
try {
var p = __mochi_apply__.call(orig, this, [opts]);
if (p && typeof p.then === "function") {
return p.then(function(adapter) { return wrapAdapter(adapter); }, function(_e) {
// If the underlying call rejects, synthesize from matrix.
return wrapAdapter(null);
});
}
} catch (_e) {}
// Fallback: synthesize from matrix.
return Promise.resolve(wrapAdapter(null));
}
__mochi_register_native__(requestAdapter, "requestAdapter");
try {
__mochi_defineProperty__(target, "requestAdapter", {
configurable: true,
enumerable: false,
writable: true,
value: requestAdapter,
});
} catch (_e) {}
})();
`;
}
/**
* Spoof module: `window.chrome` shim.
*
* Real Chrome (headed and `--headless=new`) exposes `window.chrome` as
* an object with `loadTimes`, `csi`, `app` — and, for extension contexts,
* `runtime`. Stock Chromium-for-Testing builds may or may not expose
* `window.chrome` depending on flag set; CloakBrowser's
* `test_window_chrome_exists` (line 47) and `test_device_and_browser_info`
* (line 158, `hasInconsistentChromeObject`) both check for this.
*
* The shim is **defensive** — we install it only when `window.chrome` is
* absent or non-object. If the underlying browser already exposes a real
* `window.chrome` (the common case on real Chrome.app), we leave it alone
* so the existing Probe Manifest Zero-Diff gate doesn't regress.
*
* The shim mirrors Chrome's actual non-extension shape:
* - `chrome.app` — `{ isInstalled: false, getDetails(), getIsInstalled(), … }`
* stripped to the no-op stubs Chrome reports for non-installed-app pages.
* - `chrome.csi()` — returns a benign timing object.
* - `chrome.loadTimes()` — returns a benign timings object.
* - `chrome.runtime` — INTENTIONALLY undefined for non-extension contexts.
* CloakBrowser's `bot.detection` test (`hasInconsistentChromeObject`)
* and `chromeRuntime` probe both consider `chrome.runtime` truthy as
* suspicious unless an actual extension API is present, since real
* Chrome only exposes runtime to extension callers.
*
* Reads no matrix slots — `window.chrome` is profile-invariant for
* Chromium-family browsers. The matrix's `engine === "chromium"` is the
* gating condition (other engines would not have `window.chrome`).
*
* @see CloakBrowser tests/test_stealth.py:47-50,158
* @see tasks/0140-stealth-conformance.md §"window.chrome spoof"
*/
import type { MatrixV1 } from "@mochi.js/consistency";
/**
* Emit the `window.chrome` shim. The IIFE checks if `window.chrome` is
* already an object and returns early if so — we never overwrite a
* native `window.chrome`.
*/
export function emitWindowChromeModule(matrix: MatrixV1): string {
if (matrix.engine !== "chromium") {
// No shim for non-Chromium engines (the v1 catalog is Chromium-only,
// so this path is currently unreachable, but guarding it keeps the
// module forward-compatible with the v2 cross-engine roadmap).
return `\n// ---- window.chrome spoof (skipped — non-chromium engine) ------------------\n`;
}
return `
// ---- window.chrome spoof ---------------------------------------------------
(function() {
if (typeof window === "undefined") return;
// If the underlying browser already exposes window.chrome as an object,
// do not overwrite it — real Chrome.app's native shape is richer than
// any synthesized shim and overwriting would regress the harness diff.
var existing = window.chrome;
if (existing !== undefined && existing !== null && typeof existing === "object") {
return;
}
// Build the no-op shape that Chromium-for-Testing produces in non-
// extension contexts. Functions are registered with the toString cloak
// so .toString() reports native shape.
function makeAppApi() {
function isInstalled() { return false; }
__mochi_register_native__(isInstalled, "isInstalled");
function getDetails() { return null; }
__mochi_register_native__(getDetails, "getDetails");
function getIsInstalled() { return false; }
__mochi_register_native__(getIsInstalled, "getIsInstalled");
function installState(cb) {
// Real Chrome answers "not_installed" via the callback.
try { if (typeof cb === "function") cb("not_installed"); } catch (_e) {}
}
__mochi_register_native__(installState, "installState");
function runningState() { return "cannot_run"; }
__mochi_register_native__(runningState, "runningState");
var app = Object.create(null);
app.isInstalled = false;
app.InstallState = Object.freeze({
DISABLED: "disabled",
INSTALLED: "installed",
NOT_INSTALLED: "not_installed",
});
app.RunningState = Object.freeze({
CANNOT_RUN: "cannot_run",
READY_TO_RUN: "ready_to_run",
RUNNING: "running",
});
app.getDetails = getDetails;
app.getIsInstalled = getIsInstalled;
app.installState = installState;
app.runningState = runningState;
return app;
}
function csi() {
// Approximate Chrome's csi() shape — a benign timing snapshot.
return {
onloadT: 0,
pageT: 0,
startE: 0,
tran: 15,
};
}
__mochi_register_native__(csi, "csi");
function loadTimes() {
// Approximate Chrome's loadTimes() shape. Values are filled with zeros
// — the assertion in CloakBrowser is on object-shape, not field
// values; sites that scrutinize the timings further would also
// need the harness-baseline-driven values, which is a v2 concern.
return {
requestTime: 0,
startLoadTime: 0,
commitLoadTime: 0,
finishDocumentLoadTime: 0,
finishLoadTime: 0,
firstPaintTime: 0,
firstPaintAfterLoadTime: 0,
navigationType: "Other",
wasFetchedViaSpdy: false,
wasNpnNegotiated: false,
npnNegotiatedProtocol: "unknown",
wasAlternateProtocolAvailable: false,
connectionInfo: "unknown",
};
}
__mochi_register_native__(loadTimes, "loadTimes");
var chromeShim = Object.create(null);
chromeShim.app = makeAppApi();
chromeShim.csi = csi;
chromeShim.loadTimes = loadTimes;
// chrome.runtime is intentionally undefined for non-extension contexts.
// CloakBrowser's hasInconsistentChromeObject + chromeRuntime probes both
// expect runtime to be falsy unless extension messaging is active.
try {
__mochi_defineProperty__(window, "chrome", {
configurable: true,
enumerable: true,
writable: true,
value: chromeShim,
});
} catch (_e) {
// Non-configurable native window.chrome — nothing to do; the existing
// value is what page script will see.
}
})();
`;
}
/**
* Pure-JS-string runtime helper: `__mochi_define__`.
*
* Emits the JS source for a tiny helper that wraps `Object.defineProperty`
* with the descriptor shape mochi uses for every spoofed property:
*
* - `configurable: false` so page code cannot re-define our overrides
* (PLAN.md §5.3 — "Every override uses Object.defineProperty with
* configurable: false so page code can't unwrap us by re-defining").
* - `enumerable` matched to the original native descriptor so that
* `for…in` enumeration and `Object.keys` shape stays Chrome-natural.
* - Accessor (`get`) form by default — most spoofed Navigator/Screen
* surface is accessor-style natively. A separate helper is provided
* for value-style props if a future module needs it.
*
* The helper captures the *original* `Object.defineProperty` and
* `Object.getOwnPropertyDescriptor` references inside the IIFE closure so
* page scripts running later can't observe a swapped-out
* `Object.defineProperty` to detect us.
*
* @see PLAN.md §5.3, §8.4
* @see tasks/0030-inject-engine-v0.md
*/
/**
* Emit the helper source. The returned string is concatenated into the IIFE
* by `build.ts`. After this snippet runs, the helper is available on the
* IIFE-local scope as `__mochi_define__` and `__mochi_define_value__`.
*/
export function emitDefinePropertyHelper(): string {
return `
// ---- defineProperty helper -------------------------------------------------
// Captured natives — used by every spoof module. We grab references inside
// the IIFE so page-script overrides of these globals can't trip us.
var __mochi_defineProperty__ = Object.defineProperty;
var __mochi_getOwnPropertyDescriptor__ = Object.getOwnPropertyDescriptor;
var __mochi_getPrototypeOf__ = Object.getPrototypeOf;
/**
* Replace an accessor property with a spoofed getter while preserving the
* original descriptor's enumerability. \`configurable\` is forced to false so
* page script can't redefine — see PLAN.md §5.3.
*
* The created getter is registered with the toString cloak (after that
* helper is installed) so \`getter.toString()\` returns native shape — see
* PLAN.md §5.3. The string key is used as the registered "native name" so
* fingerprint libraries that stringify the descriptor's getter see the
* expected accessor.
*
* Silently no-ops on non-existent or non-configurable target descriptors so
* we never throw a TypeError that page code could detect.
*/
function __mochi_define__(target, key, value) {
try {
var d = __mochi_getOwnPropertyDescriptor__(target, key);
if (d === undefined) {
// Walk the prototype chain to find the descriptor's natural shape.
var p = __mochi_getPrototypeOf__(target);
while (p !== null && p !== undefined) {
var pd = __mochi_getOwnPropertyDescriptor__(p, key);
if (pd !== undefined) { d = pd; break; }
p = __mochi_getPrototypeOf__(p);
}
}
var enumerable = d !== undefined ? !!d.enumerable : true;
var getter = function() { return value; };
// Register the getter with the toString cloak if available. The cloak
// installs \`__mochi_register_native__\` immediately after this helper
// is emitted, so by the time spoof modules call \`__mochi_define__\` it
// is in scope. The native name is the property key (Chrome's native
// accessors stringify as \`function get propName() { [native code] }\`
// but stock fingerprint libraries match on \`[native code]\` substring,
// so we standardize on \`function key() { [native code] }\`).
if (typeof __mochi_register_native__ === "function") {
__mochi_register_native__(getter, "get " + String(key));
}
__mochi_defineProperty__(target, key, {
configurable: false,
enumerable: enumerable,
get: getter,
});
} catch (_e) {
// Swallow — never let our injection throw to page script. PLAN.md §5.3.
}
}
/**
* Like \`__mochi_define__\` but for value-style properties (e.g. when we want
* the descriptor to be \`{ value, writable: false }\` rather than an accessor).
* Currently unused in v0.3 modules but kept for future modules.
*/
function __mochi_define_value__(target, key, value) {
try {
var d = __mochi_getOwnPropertyDescriptor__(target, key);
var enumerable = d !== undefined ? !!d.enumerable : true;
__mochi_defineProperty__(target, key, {
configurable: false,
enumerable: enumerable,
writable: false,
value: value,
});
} catch (_e) {}
}
`;
}
/**
* Pure-JS-string runtime helper: `Function.prototype.toString` cloak.
*
* Every function we replace must answer `.toString()` with the exact native
* shape Chrome would emit:
*
* `function ${name}() { [native code] }`
*
* Otherwise fingerprint libraries (FPJS, creep.js, sannysoft) detect us by
* stringifying our overrides and seeing real JS source.
*
* Implementation: a single `Function.prototype.toString` proxy that consults
* a per-spoofed-fn map keyed on the spoofed function reference. Falls
* through to the *original* `Function.prototype.toString` for everything
* else — including for the proxy's own toString (Chrome reports
* `function toString() { [native code] }` for the native one, and that's
* what users will see when they query our installed proxy too).
*
* @see PLAN.md §5.3, tasks/0030 §"toString cloaking"
*/
/**
* Emit the cloak helper source. Exposes:
* - `__mochi_register_native__(fn, name)` — register fn so its toString
* returns the native-shape string for `name`.
* - implicit: replaces `Function.prototype.toString` once.
*/
export function emitToStringCloak(): string {
return `
// ---- toString cloak --------------------------------------------------------
// Captured native references — saved BEFORE we install the proxy. PLAN.md §5.3.
var __mochi_originalFnToString__ = Function.prototype.toString;
var __mochi_call__ = Function.prototype.call;
var __mochi_apply__ = Function.prototype.apply;
// Map<Function, string nativeName>. Populated by spoof modules via
// __mochi_register_native__(fn, "navigator.userAgent" or "getParameter").
var __mochi_nativeMap__ = new WeakMap();
function __mochi_register_native__(fn, name) {
try {
__mochi_nativeMap__.set(fn, name);
} catch (_e) {}
}
/**
* The proxy that replaces \`Function.prototype.toString\`. For registered
* functions, returns the native-shape string. Otherwise calls the original
* to keep all other behaviour identical (anonymous fns, arrow fns, builtin
* native fns we didn't touch, user-defined named functions, etc.).
*
* Note: we re-read \`__mochi_nativeMap__\` from the IIFE-local closure rather
* than from any global — page script can shadow globals but cannot reach
* into our closure.
*/
function __mochi_fnToString__() {
// \`this\` is the function being stringified.
if (this !== undefined && this !== null) {
var registered = __mochi_nativeMap__.get(this);
if (registered !== undefined) {
return "function " + registered + "() { [native code] }";
}
}
// Fall through. Use Function.prototype.call.call (apply form) so we don't
// recurse if a misbehaving page swapped Function.prototype.toString again.
return __mochi_call__.call(__mochi_originalFnToString__, this);
}
// Register the proxy itself so that .toString() on our proxy returns the
// native shape (Chrome shows "function toString() { [native code] }" for
// the real one). This makes our proxy indistinguishable from the original
// at the toString-of-toString level.
__mochi_register_native__(__mochi_fnToString__, "toString");
// Install. Critical: configurable:true matches Chrome's native descriptor
// for Function.prototype.toString — fingerprint libraries DO check
// configurable on this slot. enumerable:false also matches native.
try {
__mochi_defineProperty__(Function.prototype, "toString", {
configurable: true,
enumerable: false,
writable: true,
value: __mochi_fnToString__,
});
} catch (_e) {}
`;
}
+6
-3
{
"name": "@mochi.js/inject",
"version": "0.0.1",
"version": "0.1.0",
"description": "Zero-jitter stealth payload for mochi — JIT-friendly proxies installed before any page script.",

@@ -23,6 +23,6 @@ "license": "MIT",

},
"homepage": "https://github.com/0xchasercat/mochi.js",
"homepage": "https://github.com/0xchasercat/mochi",
"repository": {
"type": "git",
"url": "git+https://github.com/0xchasercat/mochi.js.git",
"url": "git+https://github.com/0xchasercat/mochi.git",
"directory": "packages/inject"

@@ -42,2 +42,5 @@ },

},
"dependencies": {
"@mochi.js/consistency": "workspace:*"
},
"publishConfig": {

@@ -44,0 +47,0 @@ "access": "public"

# @mochi.js/inject
Zero-jitter stealth payload for [mochi](https://github.com/0xchasercat/mochi.js). Builds a single TurboFan-friendly IIFE that installs JS-layer fingerprint proxies before any page script runs.
Zero-jitter stealth payload for [mochi](https://github.com/0xchasercat/mochi). Builds a single TurboFan-friendly IIFE that installs JS-layer fingerprint proxies before any page script runs.

@@ -9,2 +9,2 @@ Internal package consumed by `@mochi.js/core`.

See [PLAN.md §5.3 and §8.4](https://github.com/0xchasercat/mochi.js/blob/main/PLAN.md).
See [PLAN.md §5.3 and §8.4](https://github.com/0xchasercat/mochi/blob/main/PLAN.md).

@@ -6,21 +6,15 @@ /**

* `Page.addScriptToEvaluateOnNewDocument(runImmediately:true, worldName:"")`
* before any page script runs. v0.0.1 claim release; payload lands in phase 0.3.
* before any page script runs.
*
* v0.3 covers the surface that v0.2 produces (the 30 rules from
* R-001..R-030 — navigator, screen, simple GPU strings, fonts/baseline-only,
* locale, timezone, hardware basics). Audio precomputed bytes, canvas hash
* maps, and full WebGL extension catalogs land in phase 0.7.
*
* @see PLAN.md §5.3 and §8.4
* @see tasks/0030-inject-engine-v0.md
*/
export const VERSION = "0.0.1" as const;
export interface PayloadResult {
readonly code: string;
readonly sha256: string;
}
export const VERSION = "0.1.0" as const;
/**
* Build the inject payload. Lands in phase 0.3.
*/
export function buildPayload(_matrix: unknown): PayloadResult {
throw new Error(
"@mochi.js/inject.buildPayload is not yet implemented (v0.0.1 claim). " +
"Lands in phase 0.3; see PLAN.md §5.3.",
);
}
export { buildPayload, type PayloadResult } from "./build";
import { describe, expect, it } from "bun:test";
import { buildPayload, VERSION } from "../index";
describe("@mochi.js/inject (claim release)", () => {
it("exports VERSION", () => {
expect(VERSION).toMatch(/^\d+\.\d+\.\d+/);
});
it("buildPayload throws until phase 0.3", () => {
expect(() => buildPayload({})).toThrow(/not yet implemented/);
});
});