@mochi.js/core 0.1.2 → 0.3.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/package.json +5 -5
- package/src/__tests__/geo-consistency.test.ts +277 -0
- package/src/__tests__/geo-probe.test.ts +415 -0
- package/src/__tests__/inject.test.ts +4 -0
- package/src/__tests__/integration.e2e.test.ts +24 -0
- package/src/__tests__/piercing.test.ts +164 -0
- package/src/__tests__/proc.test.ts +383 -0
- package/src/__tests__/selector.test.ts +188 -0
- package/src/__tests__/window-size.e2e.test.ts +130 -0
- package/src/cdp/types.ts +47 -0
- package/src/geo-consistency.ts +343 -0
- package/src/geo-probe.ts +603 -0
- package/src/index.ts +11 -0
- package/src/launch.ts +145 -9
- package/src/page/element-handle.ts +110 -0
- package/src/page/piercing.ts +135 -0
- package/src/page/selector.ts +423 -0
- package/src/page.ts +152 -1
- package/src/proc.ts +386 -41
- package/src/session.ts +358 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@mochi.js/core",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Bun-native browser automation framework — relational fingerprint locking, zero-jitter injection, behavioral playback. The primary entry point.",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -49,10 +49,10 @@
|
|
|
49
49
|
"build": "echo 'no build step yet — Bun consumes src/ directly'"
|
|
50
50
|
},
|
|
51
51
|
"dependencies": {
|
|
52
|
-
"@mochi.js/behavioral": "^0.1.
|
|
53
|
-
"@mochi.js/challenges": "^0.2.
|
|
54
|
-
"@mochi.js/consistency": "^0.1.
|
|
55
|
-
"@mochi.js/inject": "^0.
|
|
52
|
+
"@mochi.js/behavioral": "^0.1.3",
|
|
53
|
+
"@mochi.js/challenges": "^0.2.1",
|
|
54
|
+
"@mochi.js/consistency": "^0.1.2",
|
|
55
|
+
"@mochi.js/inject": "^0.2.1",
|
|
56
56
|
"@mochi.js/net": "^0.1.1"
|
|
57
57
|
},
|
|
58
58
|
"publishConfig": {
|
|
@@ -0,0 +1,277 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Unit tests for the geo-consistency reconciler — covers all 4 modes
|
|
3
|
+
* × {match, mismatch, probe-null} cases per the brief, plus the
|
|
4
|
+
* timezone-OFFSET-not-zone-name compare and the locale-region extraction.
|
|
5
|
+
*
|
|
6
|
+
* Pure JS — no FFI, no CDP, no network. The {@link reconcileGeoConsistency}
|
|
7
|
+
* function is a pure transform on `(matrix, geo, mode)`.
|
|
8
|
+
*
|
|
9
|
+
* @see tasks/0262-ip-tz-locale-exit-consistency.md
|
|
10
|
+
* @see packages/core/src/geo-consistency.ts
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { describe, expect, it } from "bun:test";
|
|
14
|
+
import { deriveMatrix, type MatrixV1, type ProfileV1 } from "@mochi.js/consistency";
|
|
15
|
+
import {
|
|
16
|
+
GeoMismatchError,
|
|
17
|
+
localeRegion,
|
|
18
|
+
reconcileGeoConsistency,
|
|
19
|
+
tzOffsetMinutes,
|
|
20
|
+
} from "../geo-consistency";
|
|
21
|
+
import type { ExitGeo } from "../geo-probe";
|
|
22
|
+
|
|
23
|
+
const PROFILE_US: ProfileV1 = {
|
|
24
|
+
id: "test-us",
|
|
25
|
+
version: "0.0.0-test",
|
|
26
|
+
engine: "chromium",
|
|
27
|
+
browser: { name: "chrome", channel: "stable", minVersion: "131", maxVersion: "133" },
|
|
28
|
+
os: { name: "macos", version: "14", arch: "arm64" },
|
|
29
|
+
device: {
|
|
30
|
+
vendor: "apple",
|
|
31
|
+
model: "macbook-air-m2",
|
|
32
|
+
cpuFamily: "apple-m2",
|
|
33
|
+
cores: 8,
|
|
34
|
+
memoryGB: 16,
|
|
35
|
+
},
|
|
36
|
+
display: { width: 1512, height: 982, dpr: 2, colorDepth: 30, pixelDepth: 30 },
|
|
37
|
+
gpu: {
|
|
38
|
+
vendor: "Apple Inc.",
|
|
39
|
+
renderer: "Apple M2",
|
|
40
|
+
webglUnmaskedVendor: "Apple Inc.",
|
|
41
|
+
webglUnmaskedRenderer: "Apple M2",
|
|
42
|
+
webglMaxTextureSize: 16384,
|
|
43
|
+
webglMaxColorAttachments: 8,
|
|
44
|
+
webglExtensions: [],
|
|
45
|
+
},
|
|
46
|
+
audio: { contextSampleRate: 48000, audioWorkletLatency: 0.005, destinationMaxChannelCount: 2 },
|
|
47
|
+
fonts: { family: "macos-baseline", list: ["Helvetica"] },
|
|
48
|
+
timezone: "America/Los_Angeles",
|
|
49
|
+
locale: "en-US",
|
|
50
|
+
languages: ["en-US", "en"],
|
|
51
|
+
behavior: { hand: "right", tremor: 0.18, wpm: 65, scrollStyle: "smooth" },
|
|
52
|
+
wreqPreset: "chrome_131_macos",
|
|
53
|
+
userAgent: "Mozilla/5.0 ... Chrome/131.0.0.0 Safari/537.36",
|
|
54
|
+
uaCh: {},
|
|
55
|
+
entropyBudget: { fixed: [], perSeed: [] },
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
function makeMatrix(): MatrixV1 {
|
|
59
|
+
return deriveMatrix(PROFILE_US, "geo-test");
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
const GEO_US: ExitGeo = {
|
|
63
|
+
ip: "8.8.8.8",
|
|
64
|
+
country: "US",
|
|
65
|
+
city: "Los Angeles",
|
|
66
|
+
region: "CA",
|
|
67
|
+
timezone: "America/Los_Angeles",
|
|
68
|
+
source: "test",
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
const GEO_DETROIT: ExitGeo = {
|
|
72
|
+
// Same offset family as Los_Angeles? No — Detroit is Eastern, LA is Pacific.
|
|
73
|
+
// Use Detroit as a US-East but same country to test "same country, diff tz"
|
|
74
|
+
ip: "1.2.3.4",
|
|
75
|
+
country: "US",
|
|
76
|
+
timezone: "America/Detroit",
|
|
77
|
+
source: "test",
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
const GEO_DE: ExitGeo = {
|
|
81
|
+
ip: "5.6.7.8",
|
|
82
|
+
country: "DE",
|
|
83
|
+
city: "Berlin",
|
|
84
|
+
timezone: "Europe/Berlin",
|
|
85
|
+
source: "test",
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
const GEO_GB: ExitGeo = {
|
|
89
|
+
ip: "9.10.11.12",
|
|
90
|
+
country: "GB",
|
|
91
|
+
city: "London",
|
|
92
|
+
timezone: "Europe/London",
|
|
93
|
+
source: "test",
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
describe("tzOffsetMinutes — offset compare, not zone-name compare", () => {
|
|
97
|
+
it("returns 0 for UTC", () => {
|
|
98
|
+
expect(tzOffsetMinutes("UTC", new Date("2026-05-09T12:00:00Z"))).toBe(0);
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
it("returns -480 for America/Los_Angeles in winter (PST)", () => {
|
|
102
|
+
expect(tzOffsetMinutes("America/Los_Angeles", new Date("2026-01-15T12:00:00Z"))).toBe(-480);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("returns +330 for Asia/Kolkata (no DST)", () => {
|
|
106
|
+
expect(tzOffsetMinutes("Asia/Kolkata", new Date("2026-05-09T12:00:00Z"))).toBe(330);
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
it("returns same offset for America/New_York and America/Detroit (equivalent for fingerprinting)", () => {
|
|
110
|
+
const ref = new Date("2026-05-09T12:00:00Z");
|
|
111
|
+
const ny = tzOffsetMinutes("America/New_York", ref);
|
|
112
|
+
const det = tzOffsetMinutes("America/Detroit", ref);
|
|
113
|
+
expect(ny).not.toBeNull();
|
|
114
|
+
expect(ny).toBe(det);
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
it("returns null for an unknown zone", () => {
|
|
118
|
+
expect(tzOffsetMinutes("Bogus/Made_Up")).toBeNull();
|
|
119
|
+
});
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
describe("localeRegion", () => {
|
|
123
|
+
it("'en-US' → 'US'", () => {
|
|
124
|
+
expect(localeRegion("en-US")).toBe("US");
|
|
125
|
+
});
|
|
126
|
+
it("'de-DE' → 'DE'", () => {
|
|
127
|
+
expect(localeRegion("de-DE")).toBe("DE");
|
|
128
|
+
});
|
|
129
|
+
it("'en' → null (no region)", () => {
|
|
130
|
+
expect(localeRegion("en")).toBeNull();
|
|
131
|
+
});
|
|
132
|
+
it("returns null on garbage input", () => {
|
|
133
|
+
expect(localeRegion("!!not a locale!!")).toBeNull();
|
|
134
|
+
});
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
describe("reconcileGeoConsistency — mode: off", () => {
|
|
138
|
+
it("returns the matrix unchanged regardless of geo", () => {
|
|
139
|
+
const m = makeMatrix();
|
|
140
|
+
const r = reconcileGeoConsistency(m, GEO_DE, "off");
|
|
141
|
+
expect(r.action).toBe("off");
|
|
142
|
+
expect(r.matrix).toBe(m);
|
|
143
|
+
expect(r.matrix.timezone).toBe("America/Los_Angeles");
|
|
144
|
+
expect(r.matrix.locale).toBe("en-US");
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
it("ignores null geo too", () => {
|
|
148
|
+
const m = makeMatrix();
|
|
149
|
+
const r = reconcileGeoConsistency(m, null, "off");
|
|
150
|
+
expect(r.action).toBe("off");
|
|
151
|
+
expect(r.matrix).toBe(m);
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
describe("reconcileGeoConsistency — mode: privacy-fallback", () => {
|
|
156
|
+
it("on probe-null → UTC + en-US fallback", () => {
|
|
157
|
+
const m = makeMatrix();
|
|
158
|
+
const r = reconcileGeoConsistency(m, null, "privacy-fallback");
|
|
159
|
+
expect(r.action).toBe("privacy-fallback");
|
|
160
|
+
expect(r.matrix.timezone).toBe("UTC");
|
|
161
|
+
expect(r.matrix.locale).toBe("en-US");
|
|
162
|
+
expect(r.matrix.languages).toEqual(["en-US", "en"]);
|
|
163
|
+
expect(r.reason).toContain("probe returned null");
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it("on tz mismatch (US matrix, DE proxy) → UTC + en-US fallback", () => {
|
|
167
|
+
const m = makeMatrix();
|
|
168
|
+
const r = reconcileGeoConsistency(m, GEO_DE, "privacy-fallback");
|
|
169
|
+
expect(r.action).toBe("privacy-fallback");
|
|
170
|
+
expect(r.matrix.timezone).toBe("UTC");
|
|
171
|
+
expect(r.matrix.locale).toBe("en-US");
|
|
172
|
+
expect(r.reason).toContain("tz offset");
|
|
173
|
+
expect(r.reason).toContain("locale region");
|
|
174
|
+
});
|
|
175
|
+
|
|
176
|
+
it("on locale-only mismatch (US tz, GB country, en-US locale) → UTC + en-US fallback", () => {
|
|
177
|
+
// GB has same offset as UTC in winter and +60 in summer; pick a date so
|
|
178
|
+
// the offsets DO match — only the country differs.
|
|
179
|
+
const m: MatrixV1 = { ...makeMatrix(), timezone: "Europe/London" };
|
|
180
|
+
// Force a date where London is in BST (UTC+60) for predictability — we
|
|
181
|
+
// pass GEO_GB which is also Europe/London, so offsets match exactly.
|
|
182
|
+
const r = reconcileGeoConsistency(m, GEO_GB, "privacy-fallback");
|
|
183
|
+
expect(r.action).toBe("privacy-fallback");
|
|
184
|
+
expect(r.matrix.timezone).toBe("UTC");
|
|
185
|
+
expect(r.reason).toContain("locale region US");
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("on full match (US matrix, US proxy, same offset) → passthrough", () => {
|
|
189
|
+
const m = makeMatrix();
|
|
190
|
+
const r = reconcileGeoConsistency(m, GEO_US, "privacy-fallback");
|
|
191
|
+
expect(r.action).toBe("ok");
|
|
192
|
+
expect(r.matrix).toBe(m);
|
|
193
|
+
expect(r.geo).toBe(GEO_US);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("zone-name diff but offset match (NY vs Detroit) → passthrough when in same country/offset", () => {
|
|
197
|
+
// Set matrix to NY; geo says Detroit (same offset, same country).
|
|
198
|
+
const m: MatrixV1 = { ...makeMatrix(), timezone: "America/New_York" };
|
|
199
|
+
const r = reconcileGeoConsistency(m, GEO_DETROIT, "privacy-fallback");
|
|
200
|
+
expect(r.action).toBe("ok");
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("reconcileGeoConsistency — mode: auto-correct", () => {
|
|
205
|
+
it("on probe-null → passthrough (best effort)", () => {
|
|
206
|
+
const m = makeMatrix();
|
|
207
|
+
const r = reconcileGeoConsistency(m, null, "auto-correct");
|
|
208
|
+
expect(r.action).toBe("no-probe");
|
|
209
|
+
expect(r.matrix).toBe(m);
|
|
210
|
+
});
|
|
211
|
+
|
|
212
|
+
it("on mismatch → override to IP-derived tz + locale", () => {
|
|
213
|
+
const m = makeMatrix();
|
|
214
|
+
const r = reconcileGeoConsistency(m, GEO_DE, "auto-correct");
|
|
215
|
+
expect(r.action).toBe("auto-correct");
|
|
216
|
+
expect(r.matrix.timezone).toBe("Europe/Berlin");
|
|
217
|
+
expect(r.matrix.locale).toBe("de-DE");
|
|
218
|
+
expect(r.matrix.languages[0]).toBe("de-DE");
|
|
219
|
+
expect(r.matrix.languages).toContain("de");
|
|
220
|
+
expect(r.matrix.languages).toContain("en");
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
it("on match → passthrough", () => {
|
|
224
|
+
const m = makeMatrix();
|
|
225
|
+
const r = reconcileGeoConsistency(m, GEO_US, "auto-correct");
|
|
226
|
+
expect(r.action).toBe("ok");
|
|
227
|
+
});
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
describe("reconcileGeoConsistency — mode: strict", () => {
|
|
231
|
+
it("on probe-null → passthrough (no probe = no provable mismatch)", () => {
|
|
232
|
+
const m = makeMatrix();
|
|
233
|
+
const r = reconcileGeoConsistency(m, null, "strict");
|
|
234
|
+
expect(r.action).toBe("no-probe");
|
|
235
|
+
expect(r.matrix).toBe(m);
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("on match → passthrough", () => {
|
|
239
|
+
const m = makeMatrix();
|
|
240
|
+
const r = reconcileGeoConsistency(m, GEO_US, "strict");
|
|
241
|
+
expect(r.action).toBe("ok");
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("on mismatch → throws GeoMismatchError", () => {
|
|
245
|
+
const m = makeMatrix();
|
|
246
|
+
expect(() => reconcileGeoConsistency(m, GEO_DE, "strict")).toThrow(GeoMismatchError);
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
it("GeoMismatchError carries the diagnostic", () => {
|
|
250
|
+
const m = makeMatrix();
|
|
251
|
+
try {
|
|
252
|
+
reconcileGeoConsistency(m, GEO_DE, "strict");
|
|
253
|
+
expect(false).toBe(true);
|
|
254
|
+
} catch (err) {
|
|
255
|
+
expect(err).toBeInstanceOf(GeoMismatchError);
|
|
256
|
+
const e = err as GeoMismatchError;
|
|
257
|
+
expect(e.matrix.locale).toBe("en-US");
|
|
258
|
+
expect(e.geo.country).toBe("DE");
|
|
259
|
+
expect(e.message).toContain("strict");
|
|
260
|
+
expect(e.message).toContain("America/Los_Angeles");
|
|
261
|
+
expect(e.message).toContain("Europe/Berlin");
|
|
262
|
+
}
|
|
263
|
+
});
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
describe("reconcileGeoConsistency — relational invariant: input matrix is never mutated", () => {
|
|
267
|
+
it("override path returns a fresh object; inputs untouched", () => {
|
|
268
|
+
const m = makeMatrix();
|
|
269
|
+
const tzBefore = m.timezone;
|
|
270
|
+
const localeBefore = m.locale;
|
|
271
|
+
const r = reconcileGeoConsistency(m, GEO_DE, "privacy-fallback");
|
|
272
|
+
expect(r.matrix).not.toBe(m);
|
|
273
|
+
expect(m.timezone).toBe(tzBefore);
|
|
274
|
+
expect(m.locale).toBe(localeBefore);
|
|
275
|
+
expect(r.matrix.timezone).toBe("UTC");
|
|
276
|
+
});
|
|
277
|
+
});
|