@mochi.js/core 0.6.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +3 -4
- package/src/__tests__/cookies-jar.test.ts +2 -3
- package/src/__tests__/default-profile.test.ts +6 -8
- package/src/__tests__/dx-cluster.e2e.test.ts +1 -2
- package/src/__tests__/geo-consistency.test.ts +0 -1
- package/src/__tests__/geo-probe.test.ts +13 -13
- package/src/__tests__/init-injector.e2e.test.ts +0 -1
- package/src/__tests__/init-injector.test.ts +1 -2
- package/src/__tests__/inject.test.ts +1 -2
- package/src/__tests__/page-dx-cluster.test.ts +3 -4
- package/src/__tests__/piercing.test.ts +1 -1
- package/src/__tests__/proc-linux-server.test.ts +4 -4
- package/src/__tests__/proc.test.ts +3 -3
- package/src/__tests__/proxy-auth.test.ts +1 -2
- package/src/__tests__/screenshot.e2e.test.ts +1 -1
- package/src/__tests__/screenshot.test.ts +1 -1
- package/src/__tests__/window-size.e2e.test.ts +0 -1
- package/src/cdp/types.ts +0 -1
- package/src/default-profile.ts +6 -8
- package/src/geo-consistency.ts +0 -1
- package/src/geo-probe.ts +37 -32
- package/src/index.ts +1 -1
- package/src/launch.ts +38 -52
- package/src/page/element-handle.ts +0 -1
- package/src/page/piercing.ts +0 -1
- package/src/page/selector.ts +0 -1
- package/src/page.ts +31 -14
- package/src/proc.ts +5 -6
- package/src/proxy-auth.ts +1 -3
- package/src/session.ts +489 -124
- package/src/version.ts +1 -1
package/README.md
CHANGED
|
@@ -22,7 +22,7 @@ await session.close();
|
|
|
22
22
|
|
|
23
23
|
## Status
|
|
24
24
|
|
|
25
|
-
`v0.
|
|
25
|
+
`v0.7.x`. `mochi.launch()` is fully wired: pipe-mode CDP transport, relational `(profile, seed)` Matrix, JIT-friendly inject delivered via `Fetch.fulfillRequest` body splice (with `Page.addScriptToEvaluateOnNewDocument({ runImmediately: true, worldName: "" })` as the `about:blank` / `data:` fallback), behavioral synth, and a Chromium-native `session.fetch` (routes through CDP — `Network.loadNetworkResource` for simple GETs, `page.evaluate("fetch")` for non-GET — so JA4 is real Chrome by definition).
|
|
26
26
|
|
|
27
27
|
The full [v0.1.4 → v0.2] surface lands as additive minor bumps. See [`CHANGELOG.md`](https://github.com/0xchasercat/mochi/blob/main/CHANGELOG.md).
|
|
28
28
|
|
|
@@ -31,7 +31,7 @@ The full [v0.1.4 → v0.2] surface lands as additive minor bumps. See [`CHANGELO
|
|
|
31
31
|
- `mochi.launch(opts)` — spawn a Chromium-for-Testing instance with a relationally-locked fingerprint matrix derived from `(profile, seed)`. Options include `proxy`, `headless`, `binary`, `timeout`, `geoConsistency` (IP/TZ/locale exit reconciliation), and `challenges` (Turnstile auto-click).
|
|
32
32
|
- `Session` and `Page` — the runtime objects you drive.
|
|
33
33
|
- `page.humanClick / humanType / humanScroll` — biomechanically-shaped input synthesis (Bezier + Fitts + Gaussian jitter).
|
|
34
|
-
- `session.fetch` — out-of-band requests
|
|
34
|
+
- `session.fetch` — out-of-band requests routed through Chromium itself via CDP. JA4/JA3/H2 are real Chrome by definition because Chromium is the client; cookies inherit from the page's origin; CORS applies for non-GET cross-origin calls.
|
|
35
35
|
- `page.screenshot(opts?)` — PNG / JPEG / WebP via CDP `Page.captureScreenshot`. Options: `format`, `quality`, `fullPage`, `clip`, `omitBackground`, `encoding`. Element-bounded capture (`{ element: handle }`) is deferred — see <https://mochijs.com/docs/reference/limits>.
|
|
36
36
|
- `session.cookies.{save,load}(path, { pattern? })` — JSON cookie jar with version header + regex domain filter. Round-trips losslessly via `Storage.getCookies` / `Storage.setCookies`.
|
|
37
37
|
- `page.localStorage.{get,set}` and `page.sessionStorage.{get,set}` — direct `DOMStorage` CDP access, frame-scoped (defaults to current main-frame origin; pass `{ origin }` for cross-origin).
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mochi.js/core",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "
|
|
3
|
+
"version": "0.8.0",
|
|
4
|
+
"description": "The library for faithful browser automation. Bun-native; relational fingerprint matrix, biomechanical input, stock Chromium-for-Testing.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"main": "./src/index.ts",
|
|
@@ -52,8 +52,7 @@
|
|
|
52
52
|
"@mochi.js/behavioral": "^0.1.4",
|
|
53
53
|
"@mochi.js/challenges": "^0.2.1",
|
|
54
54
|
"@mochi.js/consistency": "^0.1.3",
|
|
55
|
-
"@mochi.js/inject": "^0.3.0"
|
|
56
|
-
"@mochi.js/net": "^0.1.1"
|
|
55
|
+
"@mochi.js/inject": "^0.3.0"
|
|
57
56
|
},
|
|
58
57
|
"publishConfig": {
|
|
59
58
|
"access": "public"
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for the {@link Session.cookies} jar surface
|
|
2
|
+
* Unit tests for the {@link Session.cookies} jar surface:
|
|
3
3
|
*
|
|
4
4
|
* - `cookies.get()` → `Storage.getCookies` round-trip with optional
|
|
5
5
|
* url-host filter.
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
* `ChromiumProcess` whose pipe reader/writer let us observe every CDP
|
|
20
20
|
* request sent and inject canned responses.
|
|
21
21
|
*
|
|
22
|
-
* @see tasks/0257-dx-cluster-cookies-storage-permissions.md
|
|
23
22
|
* @see docs/audits/nodriver.md (LOW finding 2 — pickle → JSON port)
|
|
24
23
|
*/
|
|
25
24
|
|
|
@@ -193,7 +192,7 @@ const SAMPLE_COOKIES: Cookie[] = [
|
|
|
193
192
|
},
|
|
194
193
|
];
|
|
195
194
|
|
|
196
|
-
describe("Session.cookies
|
|
195
|
+
describe("Session.cookies", () => {
|
|
197
196
|
let fake: FakeBrowser;
|
|
198
197
|
let session: Session;
|
|
199
198
|
let tmpFile: string;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for the host-OS profile auto-pick
|
|
2
|
+
* Unit tests for the host-OS profile auto-pick.
|
|
3
3
|
*
|
|
4
4
|
* Three layers under test:
|
|
5
5
|
*
|
|
@@ -18,11 +18,9 @@
|
|
|
18
18
|
* exported `defaultProfileForHost` introspection helper. We do NOT spawn
|
|
19
19
|
* Chromium here — the goal is to lock the decisions against regressions
|
|
20
20
|
* without taking the cost of a real launch. Mirrors the
|
|
21
|
-
* `proc-linux-server.test.ts` pattern
|
|
21
|
+
* `proc-linux-server.test.ts` pattern.
|
|
22
22
|
*
|
|
23
23
|
* @see packages/core/src/default-profile.ts
|
|
24
|
-
* @see tasks/0271-the-linux-os-thesis.md (strategic thesis + production evidence)
|
|
25
|
-
* @see tasks/0272-host-os-profile-auto-default.md (engineering brief)
|
|
26
24
|
*/
|
|
27
25
|
|
|
28
26
|
import { afterEach, describe, expect, it } from "bun:test";
|
|
@@ -33,7 +31,7 @@ import {
|
|
|
33
31
|
unsupportedHostMessage,
|
|
34
32
|
} from "../default-profile";
|
|
35
33
|
|
|
36
|
-
describe("resolveDefaultProfileForHost — pure mapping table
|
|
34
|
+
describe("resolveDefaultProfileForHost — pure mapping table", () => {
|
|
37
35
|
it("linux/x64 → linux-chrome-stable", () => {
|
|
38
36
|
expect(resolveDefaultProfileForHost("linux", "x64")).toBe("linux-chrome-stable");
|
|
39
37
|
});
|
|
@@ -87,7 +85,7 @@ describe("resolveDefaultProfileForHost — pure mapping table (task 0272)", () =
|
|
|
87
85
|
});
|
|
88
86
|
});
|
|
89
87
|
|
|
90
|
-
describe("defaultProfileForHost — live wrapper
|
|
88
|
+
describe("defaultProfileForHost — live wrapper", () => {
|
|
91
89
|
// Stub `process.platform` / `process.arch` so the live wrapper exercises
|
|
92
90
|
// the same table as the pure resolver. `Object.defineProperty` is the
|
|
93
91
|
// standard Bun-test pattern (matches `proc-linux-server.test.ts`).
|
|
@@ -127,7 +125,7 @@ describe("defaultProfileForHost — live wrapper (task 0272)", () => {
|
|
|
127
125
|
});
|
|
128
126
|
});
|
|
129
127
|
|
|
130
|
-
describe("unsupportedHostMessage — failure-mode diagnostic
|
|
128
|
+
describe("unsupportedHostMessage — failure-mode diagnostic", () => {
|
|
131
129
|
it("names the host platform + arch verbatim", () => {
|
|
132
130
|
const msg = unsupportedHostMessage("freebsd", "x64");
|
|
133
131
|
expect(msg).toContain("platform=freebsd");
|
|
@@ -153,7 +151,7 @@ describe("unsupportedHostMessage — failure-mode diagnostic (task 0272)", () =>
|
|
|
153
151
|
});
|
|
154
152
|
});
|
|
155
153
|
|
|
156
|
-
describe("introspection contract — defaultProfileForHost is pure
|
|
154
|
+
describe("introspection contract — defaultProfileForHost is pure", () => {
|
|
157
155
|
it("returns the same value on repeated calls (no caching, no mutation)", () => {
|
|
158
156
|
// The launcher consults this helper at every launch; pinning purity
|
|
159
157
|
// here means a downstream `console.log(mochi.defaultProfileForHost())`
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Live conformance test for the DX cluster
|
|
2
|
+
* Live conformance test for the DX cluster.
|
|
3
3
|
*
|
|
4
4
|
* Gated by `MOCHI_E2E=1`. Drives a real Chromium-for-Testing through the
|
|
5
5
|
* public mochi launch path to verify, end-to-end, that:
|
|
@@ -18,7 +18,6 @@
|
|
|
18
18
|
* Budget: < 30 seconds.
|
|
19
19
|
*
|
|
20
20
|
* @see PLAN.md §14
|
|
21
|
-
* @see tasks/0257-dx-cluster-cookies-storage-permissions.md
|
|
22
21
|
*/
|
|
23
22
|
|
|
24
23
|
import { describe, expect, it } from "bun:test";
|
|
@@ -8,17 +8,18 @@
|
|
|
8
8
|
* / parser-null and respects the 4-attempt cap.
|
|
9
9
|
* - all-fail returns `null`.
|
|
10
10
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
11
|
+
* Post-0.7 the probe's `fetch` seam delegates to `globalThis.fetch` by
|
|
12
|
+
* default; production launch paths inject a `Session.fetch`-backed
|
|
13
|
+
* adapter so the probe rides Chromium's network stack (real Chrome JA4
|
|
14
|
+
* by definition).
|
|
13
15
|
*
|
|
14
|
-
* @see tasks/0262-ip-tz-locale-exit-consistency.md
|
|
15
16
|
* @see packages/core/src/geo-probe.ts
|
|
16
17
|
*/
|
|
17
18
|
|
|
18
19
|
import { describe, expect, it } from "bun:test";
|
|
19
20
|
import { ADAPTERS, type ProbeFetch, probeExitGeo } from "../geo-probe";
|
|
20
21
|
|
|
21
|
-
const MATRIX_STUB = {
|
|
22
|
+
const MATRIX_STUB = {};
|
|
22
23
|
|
|
23
24
|
/** Build a `ProbeFetch` that returns canned JSON for each URL. */
|
|
24
25
|
function fakeFetch(
|
|
@@ -335,10 +336,10 @@ describe("probeExitGeo — strategy", () => {
|
|
|
335
336
|
expect(calls).toBe(4);
|
|
336
337
|
});
|
|
337
338
|
|
|
338
|
-
it("forwards proxy
|
|
339
|
-
let captured: { url?: string;
|
|
339
|
+
it("forwards proxy diagnostic to the fetch impl", async () => {
|
|
340
|
+
let captured: { url?: string; proxy?: string } = {};
|
|
340
341
|
const fetchSpy: ProbeFetch = async (url, init) => {
|
|
341
|
-
captured = { url,
|
|
342
|
+
captured = { url, proxy: init.proxy };
|
|
342
343
|
return new Response(
|
|
343
344
|
JSON.stringify({
|
|
344
345
|
proxy: { ip: "1" },
|
|
@@ -349,22 +350,21 @@ describe("probeExitGeo — strategy", () => {
|
|
|
349
350
|
);
|
|
350
351
|
};
|
|
351
352
|
await probeExitGeo({
|
|
352
|
-
matrix:
|
|
353
|
+
matrix: MATRIX_STUB,
|
|
353
354
|
proxy: "http://user:pass@proxy.example:8080",
|
|
354
355
|
fetch: fetchSpy,
|
|
355
356
|
shuffle: noShuffle,
|
|
356
357
|
maxAttempts: 1,
|
|
357
358
|
perEndpointTimeoutMs: 100,
|
|
358
359
|
});
|
|
359
|
-
expect(captured.preset).toBe("chrome_131_linux");
|
|
360
360
|
expect(captured.proxy).toBe("http://user:pass@proxy.example:8080");
|
|
361
361
|
});
|
|
362
362
|
|
|
363
|
-
it("synchronous throw from fetch
|
|
363
|
+
it("synchronous throw from fetch → null, NEVER propagates", async () => {
|
|
364
364
|
const fetchSpy: ProbeFetch = () => {
|
|
365
|
-
// Simulate
|
|
366
|
-
//
|
|
367
|
-
throw new Error("
|
|
365
|
+
// Simulate a transport-level synchronous throw — e.g. a Session
|
|
366
|
+
// that's already been closed when the probe fires.
|
|
367
|
+
throw new Error("transport unavailable");
|
|
368
368
|
};
|
|
369
369
|
const geo = await probeExitGeo({
|
|
370
370
|
matrix: MATRIX_STUB,
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for the init-injector building blocks
|
|
2
|
+
* Unit tests for the init-injector building blocks.
|
|
3
3
|
*
|
|
4
4
|
* Covers:
|
|
5
5
|
* - {@link rewriteCsp}: no-nonce / nonce / strict-dynamic / unsafe-inline
|
|
@@ -12,7 +12,6 @@
|
|
|
12
12
|
* - {@link wrapSelfRemovingPayload}: first statement is the self-remove +
|
|
13
13
|
* marker; the inner payload is left intact.
|
|
14
14
|
*
|
|
15
|
-
* @see tasks/0266-fetch-fulfill-init-script.md
|
|
16
15
|
*/
|
|
17
16
|
|
|
18
17
|
import { describe, expect, it } from "bun:test";
|
|
@@ -10,7 +10,6 @@
|
|
|
10
10
|
* `MessageRouter`, so the test implicitly enforces those too.
|
|
11
11
|
*
|
|
12
12
|
* @see PLAN.md §12.1 — capture must run against bare Chromium.
|
|
13
|
-
* @see tasks/0040-mochi-capture.md — `bypassInject: true` requirement.
|
|
14
13
|
* @see tests/helpers/cdp-fixture.ts — shared helper consolidating fake-pipe boilerplate.
|
|
15
14
|
*/
|
|
16
15
|
|
|
@@ -129,7 +128,7 @@ describe("Session.bypassInject (PLAN.md §12.1, task 0040)", () => {
|
|
|
129
128
|
expect(session._internalBypassInject()).toBe(true);
|
|
130
129
|
});
|
|
131
130
|
|
|
132
|
-
it("with bypassInject omitted — Session installs the unified Fetch-domain injector instead of Page.addScriptToEvaluateOnNewDocument
|
|
131
|
+
it("with bypassInject omitted — Session installs the unified Fetch-domain injector instead of Page.addScriptToEvaluateOnNewDocument", async () => {
|
|
133
132
|
// Override the script identifier for this test — it asserts that the
|
|
134
133
|
// dual-mechanism `addScriptToEvaluateOnNewDocument` call (commit 2 of
|
|
135
134
|
// 0266) carries the wrapped matrix payload.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Unit tests for the Page DX cluster
|
|
2
|
+
* Unit tests for the Page DX cluster:
|
|
3
3
|
* - `Page.localStorage.{get,set}` → DOMStorage.getDOMStorageItems /
|
|
4
4
|
* DOMStorage.setDOMStorageItem.
|
|
5
5
|
* - `Page.sessionStorage.{get,set}` → same shape, `isLocalStorage: false`.
|
|
@@ -9,7 +9,6 @@
|
|
|
9
9
|
* Driven against a hand-rolled fake CDP transport — same fixture pattern as
|
|
10
10
|
* the cookies-jar tests, kept inline here so each test file stands alone.
|
|
11
11
|
*
|
|
12
|
-
* @see tasks/0257-dx-cluster-cookies-storage-permissions.md
|
|
13
12
|
*/
|
|
14
13
|
|
|
15
14
|
import { afterEach, beforeEach, describe, expect, it } from "bun:test";
|
|
@@ -114,7 +113,7 @@ function wireOriginResolver(fake: FakeBrowser, origin: string): void {
|
|
|
114
113
|
});
|
|
115
114
|
}
|
|
116
115
|
|
|
117
|
-
describe("Page.localStorage / Page.sessionStorage
|
|
116
|
+
describe("Page.localStorage / Page.sessionStorage", () => {
|
|
118
117
|
let fake: FakeBrowser;
|
|
119
118
|
let router: MessageRouter;
|
|
120
119
|
let page: Page;
|
|
@@ -224,7 +223,7 @@ describe("Page.localStorage / Page.sessionStorage (task 0257)", () => {
|
|
|
224
223
|
});
|
|
225
224
|
});
|
|
226
225
|
|
|
227
|
-
describe("Page.grantAllPermissions
|
|
226
|
+
describe("Page.grantAllPermissions", () => {
|
|
228
227
|
let fake: FakeBrowser;
|
|
229
228
|
let router: MessageRouter;
|
|
230
229
|
let page: Page;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* (`packages/core/src/page/piercing.ts`). Drives a hand-crafted
|
|
4
4
|
* `PierceDomNode` tree that mirrors the CDP `DOM.getDocument({ depth:-1,
|
|
5
5
|
* pierce:true })` shape — including a closed-shadow-rooted iframe, which is
|
|
6
|
-
* the whole point of
|
|
6
|
+
* the whole point of
|
|
7
7
|
*
|
|
8
8
|
* The findPiercingMatches output is verified by `backendNodeId`, the only
|
|
9
9
|
* field the host-side `Page.querySelectorPiercing` cares about for the
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Unit tests for the Linux-server environment auto-detection that drives
|
|
3
|
-
* `LaunchOptions.headlessMode` defaulting
|
|
3
|
+
* `LaunchOptions.headlessMode` defaulting.
|
|
4
4
|
*
|
|
5
5
|
* Two layers under test:
|
|
6
6
|
*
|
|
@@ -41,7 +41,7 @@ function probes(overrides: Partial<LinuxServerProbes> = {}): LinuxServerProbes {
|
|
|
41
41
|
};
|
|
42
42
|
}
|
|
43
43
|
|
|
44
|
-
describe("detectLinuxServerEnv — server-no-display classifier
|
|
44
|
+
describe("detectLinuxServerEnv — server-no-display classifier", () => {
|
|
45
45
|
it("flags Linux + no DISPLAY + no WAYLAND_DISPLAY as serverNoDisplay=true", () => {
|
|
46
46
|
const env = detectLinuxServerEnv(probes());
|
|
47
47
|
expect(env.serverNoDisplay).toBe(true);
|
|
@@ -130,7 +130,7 @@ describe("detectLinuxServerEnv — server-no-display classifier (task 0258)", ()
|
|
|
130
130
|
});
|
|
131
131
|
});
|
|
132
132
|
|
|
133
|
-
describe("resolveHeadlessMode — precedence table
|
|
133
|
+
describe("resolveHeadlessMode — precedence table", () => {
|
|
134
134
|
const SERVER_ENV = detectLinuxServerEnv(probes());
|
|
135
135
|
const DEV_ENV = detectLinuxServerEnv(probes({ display: ":0" }));
|
|
136
136
|
|
|
@@ -198,7 +198,7 @@ describe("resolveHeadlessMode — precedence table (task 0258)", () => {
|
|
|
198
198
|
});
|
|
199
199
|
});
|
|
200
200
|
|
|
201
|
-
describe("buildChromiumArgs — headlessMode dispatch
|
|
201
|
+
describe("buildChromiumArgs — headlessMode dispatch", () => {
|
|
202
202
|
function baseCfg(overrides: Partial<SpawnConfig> = {}): SpawnConfig {
|
|
203
203
|
return { binary: FAKE_BINARY, headless: false, ...overrides };
|
|
204
204
|
}
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* spawn a real Chromium here; the goal is to lock the flag set against
|
|
5
5
|
* regressions, particularly the matrix-derived `--lang=<locale>` flag that
|
|
6
6
|
* closes the I-5 leak between Chromium's network-layer `Accept-Language`
|
|
7
|
-
* header and the JS-layer `navigator.language(s)` spoof
|
|
7
|
+
* header and the JS-layer `navigator.language(s)` spoof.
|
|
8
8
|
*
|
|
9
9
|
* The flag is sourced from `MatrixV1.locale` (the canonical primary BCP-47
|
|
10
10
|
* string) and MUST come from the matrix, never from the host OS.
|
|
@@ -75,7 +75,7 @@ describe("buildChromiumArgs / baseline", () => {
|
|
|
75
75
|
});
|
|
76
76
|
});
|
|
77
77
|
|
|
78
|
-
describe("buildChromiumArgs / --lang
|
|
78
|
+
describe("buildChromiumArgs / --lang (— matrix.locale → Accept-Language)", () => {
|
|
79
79
|
it("appends --lang=<value> when locale is set", () => {
|
|
80
80
|
const args = buildChromiumArgs(
|
|
81
81
|
baseCfg({ locale: "en-US" }),
|
|
@@ -337,7 +337,7 @@ describe("buildChromiumArgs — task 0256 (hermetic-mode knob)", () => {
|
|
|
337
337
|
});
|
|
338
338
|
|
|
339
339
|
/**
|
|
340
|
-
* Diagnostic-tail classifier —
|
|
340
|
+
* Diagnostic-tail classifier — Locks the two patterns we currently
|
|
341
341
|
* surface (root-sandbox refusal and missing shared libs) against regressions
|
|
342
342
|
* without spawning a real Chromium.
|
|
343
343
|
*/
|
|
@@ -13,7 +13,6 @@
|
|
|
13
13
|
* - The defensive `Fetch.requestPaused` handler issues `Fetch.continueRequest`.
|
|
14
14
|
* - `dispose()` sends `Fetch.disable` and is idempotent.
|
|
15
15
|
*
|
|
16
|
-
* @see tasks/0160-proxy-auth-and-ci-fix.md
|
|
17
16
|
*/
|
|
18
17
|
|
|
19
18
|
import { describe, expect, it } from "bun:test";
|
|
@@ -152,7 +151,7 @@ function makeRouter(): FakeRouter {
|
|
|
152
151
|
}
|
|
153
152
|
|
|
154
153
|
describe("installProxyAuth", () => {
|
|
155
|
-
it("sends Fetch.enable with handleAuthRequests:true and Document-first patterns
|
|
154
|
+
it("sends Fetch.enable with handleAuthRequests:true and Document-first patterns", async () => {
|
|
156
155
|
const f = makeRouter();
|
|
157
156
|
const handle = await installProxyAuth(f.router, { username: "u", password: "p" });
|
|
158
157
|
const enable = f.pipe.written.find((c) => c.parsed.method === "Fetch.enable");
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Live conformance test for `Page.screenshot
|
|
2
|
+
* Live conformance test for `Page.screenshot`.
|
|
3
3
|
*
|
|
4
4
|
* Gated by `MOCHI_E2E=1` so the default `bun test` run stays fast and offline.
|
|
5
5
|
* Spawns a real Chromium-for-Testing instance, navigates to a tiny data URL,
|
package/src/cdp/types.ts
CHANGED
|
@@ -83,7 +83,6 @@ export interface DomNode {
|
|
|
83
83
|
*
|
|
84
84
|
* @see PLAN.md §8.2 — `DOM.getDocument` and `DOM.resolveNode` are not on the
|
|
85
85
|
* forbidden list; both are fine to use.
|
|
86
|
-
* @see tasks/0253-closed-shadow-piercing-locator.md
|
|
87
86
|
*/
|
|
88
87
|
export interface PierceDomNode {
|
|
89
88
|
nodeId: number;
|
package/src/default-profile.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Auto-pick the host-OS-matching profile when `LaunchOptions.profile` is
|
|
3
|
-
* omitted
|
|
3
|
+
* omitted. The function below is the pure decision table the
|
|
4
4
|
* launcher consults; tests stub `(platform, arch)` and assert the mapping
|
|
5
5
|
* without spinning a Chromium.
|
|
6
6
|
*
|
|
@@ -17,7 +17,7 @@
|
|
|
17
17
|
* by hand" into a default removes the entire class of "user accidentally
|
|
18
18
|
* spoofed Windows from a Linux DC and looked weird to the WAF" failures —
|
|
19
19
|
* the same argument that drove `detectLinuxServerEnv` for headless mode in
|
|
20
|
-
*
|
|
20
|
+
*
|
|
21
21
|
*
|
|
22
22
|
* ## Mapping
|
|
23
23
|
*
|
|
@@ -38,12 +38,10 @@
|
|
|
38
38
|
* The current profile catalog (`packages/profiles/data/`) ships
|
|
39
39
|
* `mac-chrome-stable` as a darwin/arm64 capture (its `os.arch === "arm64"`
|
|
40
40
|
* in `profile.json`). The mapping above still routes darwin/x64 to
|
|
41
|
-
* `mac-chrome-stable
|
|
41
|
+
* `mac-chrome-stable`; users on Intel Macs
|
|
42
42
|
* who want a strict arch match should pass `profile` explicitly until an
|
|
43
43
|
* `mac-intel-chrome-stable` capture lands.
|
|
44
44
|
*
|
|
45
|
-
* @see tasks/0271-the-linux-os-thesis.md — the strategic thesis + evidence
|
|
46
|
-
* @see tasks/0272-host-os-profile-auto-default.md — engineering brief
|
|
47
45
|
*/
|
|
48
46
|
|
|
49
47
|
import type { ProfileId } from "./launch";
|
|
@@ -64,7 +62,7 @@ export function defaultProfileForHost(): ProfileId | null {
|
|
|
64
62
|
/**
|
|
65
63
|
* Internal pure resolver, exposed so the unit tests can drive the table
|
|
66
64
|
* without stubbing global `process`. Mirrors the precedence-table style of
|
|
67
|
-
* `resolveHeadlessMode
|
|
65
|
+
* `resolveHeadlessMode`.
|
|
68
66
|
*
|
|
69
67
|
* @internal
|
|
70
68
|
*/
|
|
@@ -82,7 +80,7 @@ export function resolveDefaultProfileForHost(
|
|
|
82
80
|
/**
|
|
83
81
|
* The six real-device profile IDs that `defaultProfileForHost` can return,
|
|
84
82
|
* surfaced by the launcher's failure-mode diagnostic. Order matches the
|
|
85
|
-
*
|
|
83
|
+
* brief verbatim so the user-facing message is stable.
|
|
86
84
|
*
|
|
87
85
|
* @internal
|
|
88
86
|
*/
|
|
@@ -97,7 +95,7 @@ export const EXPLICIT_PROFILE_IDS = [
|
|
|
97
95
|
|
|
98
96
|
/**
|
|
99
97
|
* Build the precise diagnostic emitted when `profile` is omitted on an
|
|
100
|
-
* unsupported host. Format pinned
|
|
98
|
+
* unsupported host. Format pinned — keep stable so docs +
|
|
101
99
|
* LLM-context blocks stay correct.
|
|
102
100
|
*
|
|
103
101
|
* @internal
|
package/src/geo-consistency.ts
CHANGED
package/src/geo-probe.ts
CHANGED
|
@@ -9,11 +9,13 @@
|
|
|
9
9
|
* first half of the fix (the second half is {@link reconcileGeoConsistency}
|
|
10
10
|
* in `launch.ts`).
|
|
11
11
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
12
|
+
* Post-0.7 the probe rides Chromium itself — same network stack as
|
|
13
|
+
* `page.goto`, same TLS / H2 / JA4 by definition. The default `ProbeFetch`
|
|
14
|
+
* adapter delegates to `globalThis.fetch` so {@link probeExitGeo} can run
|
|
15
|
+
* in a test runner without requiring a live Session; production callers
|
|
16
|
+
* inject a `Session.fetch`-backed adapter via `launch.ts`. The probe
|
|
17
|
+
* respects the `proxy` option as a diagnostic-only field — Chromium picks
|
|
18
|
+
* up `--proxy-server` from the launch flags directly.
|
|
17
19
|
*
|
|
18
20
|
* ### Endpoint registry (verified working 2026-05-09)
|
|
19
21
|
*
|
|
@@ -34,14 +36,12 @@
|
|
|
34
36
|
* mode (default `privacy-fallback`).
|
|
35
37
|
*
|
|
36
38
|
* **No cross-session caching** — proxy IPs rotate; stale cache is worse
|
|
37
|
-
* than no cache. (`docs/limits.md` —
|
|
39
|
+
* than no cache. (`docs/limits.md` —)
|
|
38
40
|
*
|
|
39
41
|
* @see PLAN.md §9 (relational consistency — IP/TZ/Locale axis)
|
|
40
|
-
* @see tasks/0262-ip-tz-locale-exit-consistency.md
|
|
41
42
|
*/
|
|
42
43
|
|
|
43
44
|
import type { MatrixV1 } from "@mochi.js/consistency";
|
|
44
|
-
import { fetch as netFetch } from "@mochi.js/net";
|
|
45
45
|
|
|
46
46
|
/**
|
|
47
47
|
* Normalised geolocation derived from one of the probe endpoints. The
|
|
@@ -429,22 +429,30 @@ const DEFAULT_PER_ENDPOINT_TIMEOUT_MS = 2000;
|
|
|
429
429
|
|
|
430
430
|
/**
|
|
431
431
|
* Injection seam for the underlying HTTP transport. Production uses
|
|
432
|
-
*
|
|
433
|
-
*
|
|
432
|
+
* `Session.fetch` (so the probe carries the same JA4/headers as user
|
|
433
|
+
* traffic — it IS the browser). Tests inject a stub.
|
|
434
|
+
*
|
|
435
|
+
* The `proxy` field is forwarded for diagnostic purposes only; the
|
|
436
|
+
* actual proxy egress is wired at the Chromium `--proxy-server` flag.
|
|
434
437
|
*
|
|
435
438
|
* @internal
|
|
436
439
|
*/
|
|
437
440
|
export type ProbeFetch = (
|
|
438
441
|
url: string,
|
|
439
|
-
init: {
|
|
442
|
+
init: { proxy?: string; timeoutMs: number },
|
|
440
443
|
) => Promise<Response>;
|
|
441
444
|
|
|
442
445
|
/** Options for {@link probeExitGeo}. */
|
|
443
446
|
export interface ProbeOptions {
|
|
444
|
-
/** Optional outbound proxy URL —
|
|
447
|
+
/** Optional outbound proxy URL — diagnostic-only post-0.7. */
|
|
445
448
|
readonly proxy?: string;
|
|
446
|
-
/**
|
|
447
|
-
|
|
449
|
+
/**
|
|
450
|
+
* The matrix is retained on the API surface for forward-compat (so any
|
|
451
|
+
* future per-locale endpoint shuffle can read from it) and to keep
|
|
452
|
+
* call sites stable across the 0.6 → 0.7 transition. The probe itself
|
|
453
|
+
* no longer reads any field — Chromium owns the TLS fingerprint.
|
|
454
|
+
*/
|
|
455
|
+
readonly matrix?: Partial<MatrixV1>;
|
|
448
456
|
/**
|
|
449
457
|
* Override the default 4-attempt cap. Tests use 2 to keep wall-time low;
|
|
450
458
|
* production sticks with 4.
|
|
@@ -454,7 +462,9 @@ export interface ProbeOptions {
|
|
|
454
462
|
readonly perEndpointTimeoutMs?: number;
|
|
455
463
|
/**
|
|
456
464
|
* Inject a custom `fetch` transport (for tests). Defaults to
|
|
457
|
-
*
|
|
465
|
+
* `globalThis.fetch` (test/standalone use) — production calls override
|
|
466
|
+
* this with a `Session.fetch`-backed adapter so the probe rides the
|
|
467
|
+
* same Chromium network stack as user traffic.
|
|
458
468
|
* @internal
|
|
459
469
|
*/
|
|
460
470
|
readonly fetch?: ProbeFetch;
|
|
@@ -467,27 +477,23 @@ export interface ProbeOptions {
|
|
|
467
477
|
}
|
|
468
478
|
|
|
469
479
|
/**
|
|
470
|
-
* Default `ProbeFetch` —
|
|
471
|
-
*
|
|
480
|
+
* Default `ProbeFetch` — falls back to `globalThis.fetch` so the probe
|
|
481
|
+
* works standalone in tests without a live Session. Production launch
|
|
482
|
+
* paths inject a `Session.fetch`-backed adapter so the probe shares
|
|
483
|
+
* Chromium's network stack (and therefore JA4) with user traffic.
|
|
472
484
|
*
|
|
473
|
-
* The per-call timeout is
|
|
474
|
-
* NOT also wrap with `AbortController` because Bun's `Response` from the
|
|
475
|
-
* FFI path is synchronous and we want the wreq layer to own the timeout
|
|
476
|
-
* (a layered `AbortController` would race with the Rust handle drop).
|
|
485
|
+
* The per-call timeout is enforced via `AbortController`.
|
|
477
486
|
*/
|
|
478
487
|
const defaultFetch: ProbeFetch = (url, init) => {
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
488
|
+
const ac = new AbortController();
|
|
489
|
+
const timer = setTimeout(() => ac.abort(), init.timeoutMs);
|
|
490
|
+
return globalThis
|
|
491
|
+
.fetch(url, {
|
|
483
492
|
method: "GET",
|
|
484
493
|
headers: { Accept: "application/json" },
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
connectTimeoutMs: init.timeoutMs,
|
|
489
|
-
}),
|
|
490
|
-
);
|
|
494
|
+
signal: ac.signal,
|
|
495
|
+
})
|
|
496
|
+
.finally(() => clearTimeout(timer));
|
|
491
497
|
};
|
|
492
498
|
|
|
493
499
|
/** Fisher-Yates shuffle — non-deterministic, Math.random-backed. */
|
|
@@ -570,7 +576,6 @@ export async function probeExitGeo(opts: ProbeOptions): Promise<ExitGeo | null>
|
|
|
570
576
|
new Promise<Response>((resolve, reject) => {
|
|
571
577
|
try {
|
|
572
578
|
fetchFn(adapter.url, {
|
|
573
|
-
preset: opts.matrix.wreqPreset,
|
|
574
579
|
...(opts.proxy !== undefined ? { proxy: opts.proxy } : {}),
|
|
575
580
|
timeoutMs: perEndpointTimeoutMs,
|
|
576
581
|
}).then(resolve, reject);
|
package/src/index.ts
CHANGED
|
@@ -20,7 +20,7 @@ export {
|
|
|
20
20
|
type Unsubscribe,
|
|
21
21
|
} from "./cdp/router";
|
|
22
22
|
// Auto-pick host-OS-matching profile when `LaunchOptions.profile` is omitted
|
|
23
|
-
//
|
|
23
|
+
// (— paired with the strategic thesis in task 0271).
|
|
24
24
|
export {
|
|
25
25
|
defaultProfileForHost,
|
|
26
26
|
EXPLICIT_PROFILE_IDS,
|