@melaya/runner 1.1.13 → 1.1.14
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/dist/browserBridge.js +141 -11
- package/package.json +1 -1
package/dist/browserBridge.js
CHANGED
|
@@ -59,7 +59,7 @@ const MELAYA_BRAND_SCRIPT = `(() => {
|
|
|
59
59
|
if (document.getElementById(ID + "_border")) return;
|
|
60
60
|
const style = document.createElement("style");
|
|
61
61
|
style.id = ID + "_style";
|
|
62
|
-
style.textContent = "@keyframes melBrandPulse{0%,100%{box-shadow:inset 0 0 9px 2px rgba(34,211,238,.5),inset 0 0 30px 7px rgba(124,111,240,.18)}50%{box-shadow:inset 0 0 16px 4px rgba(34,211,238,.8),inset 0 0 54px 13px rgba(124,111,240,.32)}}@keyframes melBrandGlow{0%,100%{opacity:.78}50%{opacity:1}}#" + ID + "_border{position:fixed;inset:0;pointer-events:none;z-index:2147483647;border-radius:7px;animation:melBrandPulse 2.6s ease-in-out infinite}#" + ID + "_badge{position:fixed;top:10px;left:50%;transform:translateX(-50%);pointer-events:none;z-index:2147483647;font-family:-apple-system,Segoe UI,Roboto,sans-serif;font-weight:600;font-size:12px;line-height:
|
|
62
|
+
style.textContent = "@keyframes melBrandPulse{0%,100%{box-shadow:inset 0 0 9px 2px rgba(34,211,238,.5),inset 0 0 30px 7px rgba(124,111,240,.18)}50%{box-shadow:inset 0 0 16px 4px rgba(34,211,238,.8),inset 0 0 54px 13px rgba(124,111,240,.32)}}@keyframes melBrandGlow{0%,100%{opacity:.78}50%{opacity:1}}#" + ID + "_border{position:fixed;inset:0;pointer-events:none;z-index:2147483647;border-radius:7px;animation:melBrandPulse 2.6s ease-in-out infinite}#" + ID + "_badge{position:fixed;top:10px;left:50%;transform:translateX(-50%);pointer-events:none;z-index:2147483647;font-family:-apple-system,Segoe UI,Roboto,sans-serif;font-weight:600;font-size:12px;line-height:16px;white-space:nowrap;max-width:calc(100vw - 24px);box-sizing:border-box;color:#E2E8F0;background:linear-gradient(180deg,rgba(15,23,42,.94),rgba(15,23,42,.82));padding:5px 13px 5px 7px;border-radius:999px;letter-spacing:.2px;border:1px solid rgba(34,211,238,.45);box-shadow:0 0 16px 1px rgba(34,211,238,.4),0 4px 14px rgba(0,0,0,.45);display:flex;align-items:center;gap:8px;backdrop-filter:blur(6px);animation:melBrandGlow 2.6s ease-in-out infinite}#" + ID + "_logo{height:16px;width:auto;display:block;filter:drop-shadow(0 0 4px rgba(34,211,238,.55))}@media (max-width:600px){#" + ID + "_badge{font-size:10px;line-height:13px;padding:4px 10px 4px 6px;gap:6px;top:6px}#" + ID + "_logo{height:13px}}";
|
|
63
63
|
(document.head || root).appendChild(style);
|
|
64
64
|
const border = document.createElement("div"); border.id = ID + "_border";
|
|
65
65
|
const badge = document.createElement("div"); badge.id = ID + "_badge";
|
|
@@ -70,6 +70,120 @@ const MELAYA_BRAND_SCRIPT = `(() => {
|
|
|
70
70
|
if (document.readyState === "loading") document.addEventListener("DOMContentLoaded", inject); else inject();
|
|
71
71
|
try { new MutationObserver(() => { if (!document.getElementById(ID + "_border")) inject(); }).observe(document.documentElement, { childList: true }); } catch (e) {}
|
|
72
72
|
})();`;
|
|
73
|
+
// ── Stealth init script ────────────────────────────────────────────────────
|
|
74
|
+
// Runs in EVERY frame BEFORE page scripts (context.addInitScript). A headed
|
|
75
|
+
// Brave/Chrome already carries a genuine fingerprint, so this only patches the
|
|
76
|
+
// handful of vectors a CDP-driven session can still leak (webdriver flag, an
|
|
77
|
+
// inconsistent Notification permission, a missing chrome stub, empty plugins,
|
|
78
|
+
// masked WebGL vendor). It cannot hide the CDP transport itself — that is why
|
|
79
|
+
// behaviour is also humanised below — but it defeats the common JS bot checks.
|
|
80
|
+
const STEALTH_SCRIPT = `(() => {
|
|
81
|
+
const def = (o, k, get) => { try { Object.defineProperty(o, k, { get, configurable: true }); } catch (e) {} };
|
|
82
|
+
// navigator.webdriver → false (belt-and-suspenders; the launch flag already
|
|
83
|
+
// disables it, but some builds still expose it truthy under CDP).
|
|
84
|
+
def(Object.getPrototypeOf(navigator) || navigator, "webdriver", () => false);
|
|
85
|
+
try { delete Navigator.prototype.webdriver; } catch (e) {}
|
|
86
|
+
// Minimal chrome runtime stub when absent (headless-style gap).
|
|
87
|
+
try { window.chrome = window.chrome || {}; window.chrome.runtime = window.chrome.runtime || {}; } catch (e) {}
|
|
88
|
+
def(navigator, "languages", () => ["en-US", "en"]);
|
|
89
|
+
// Non-empty plugins/mimeTypes so length checks pass.
|
|
90
|
+
try { if (!navigator.plugins || navigator.plugins.length === 0) def(navigator, "plugins", () => [1, 2, 3, 4, 5]); } catch (e) {}
|
|
91
|
+
// Notification permission must agree between Notification.permission and
|
|
92
|
+
// permissions.query — a classic mismatch CDP introduces.
|
|
93
|
+
try {
|
|
94
|
+
const q = navigator.permissions && navigator.permissions.query ? navigator.permissions.query.bind(navigator.permissions) : null;
|
|
95
|
+
if (q) navigator.permissions.query = (p) => (p && p.name === "notifications")
|
|
96
|
+
? Promise.resolve({ state: (typeof Notification !== "undefined" ? Notification.permission : "default"), onchange: null })
|
|
97
|
+
: q(p);
|
|
98
|
+
} catch (e) {}
|
|
99
|
+
// Mask WebGL UNMASKED_VENDOR/RENDERER (37445/37446) to a plausible GPU.
|
|
100
|
+
try {
|
|
101
|
+
const patch = (proto) => { if (!proto) return; const gp = proto.getParameter; if (!gp) return; proto.getParameter = function (p) { if (p === 37445) return "Intel Inc."; if (p === 37446) return "Intel Iris OpenGL Engine"; return gp.call(this, p); }; };
|
|
102
|
+
patch(window.WebGLRenderingContext && WebGLRenderingContext.prototype);
|
|
103
|
+
patch(window.WebGL2RenderingContext && WebGL2RenderingContext.prototype);
|
|
104
|
+
} catch (e) {}
|
|
105
|
+
def(navigator, "hardwareConcurrency", () => 8);
|
|
106
|
+
def(navigator, "deviceMemory", () => 8);
|
|
107
|
+
})();`;
|
|
108
|
+
// ── Humanized input helpers ────────────────────────────────────────────────
|
|
109
|
+
// Instant teleport-clicks and zero-delay typing are a behavioural bot tell.
|
|
110
|
+
// These move the pointer along an eased, jittered Bezier path, dwell before
|
|
111
|
+
// pressing, type with variable per-key cadence, and scroll in small steps.
|
|
112
|
+
// (PWPage is already aliased above to import("playwright").Page.)
|
|
113
|
+
const _rand = (min, max) => min + Math.random() * (max - min);
|
|
114
|
+
const _randInt = (min, max) => Math.floor(_rand(min, max + 1));
|
|
115
|
+
const _lastMouse = new WeakMap();
|
|
116
|
+
async function humanPause(page, min = 40, max = 140) {
|
|
117
|
+
await page.waitForTimeout(_randInt(min, max));
|
|
118
|
+
}
|
|
119
|
+
// Eased, jittered pointer travel from the last known position to (tx,ty).
|
|
120
|
+
async function humanMove(page, tx, ty) {
|
|
121
|
+
const vp = page.viewportSize() ?? { width: 1280, height: 800 };
|
|
122
|
+
const from = _lastMouse.get(page) ?? { x: _rand(0, vp.width), y: _rand(0, vp.height) };
|
|
123
|
+
const dist = Math.hypot(tx - from.x, ty - from.y);
|
|
124
|
+
const steps = Math.max(6, Math.min(42, Math.round(dist / _rand(18, 34))));
|
|
125
|
+
const curve = Math.min(90, dist * 0.28);
|
|
126
|
+
const midX = (from.x + tx) / 2 + _rand(-1, 1) * curve;
|
|
127
|
+
const midY = (from.y + ty) / 2 + _rand(-1, 1) * curve;
|
|
128
|
+
for (let i = 1; i <= steps; i++) {
|
|
129
|
+
const t = i / steps;
|
|
130
|
+
const e = t < 0.5 ? 2 * t * t : -1 + (4 - 2 * t) * t; // easeInOutQuad
|
|
131
|
+
const mt = 1 - e;
|
|
132
|
+
let x = mt * mt * from.x + 2 * mt * e * midX + e * e * tx;
|
|
133
|
+
let y = mt * mt * from.y + 2 * mt * e * midY + e * e * ty;
|
|
134
|
+
if (i < steps) {
|
|
135
|
+
x += _rand(-1.2, 1.2);
|
|
136
|
+
y += _rand(-1.2, 1.2);
|
|
137
|
+
} // hand tremor
|
|
138
|
+
await page.mouse.move(x, y);
|
|
139
|
+
if (i % _randInt(3, 5) === 0)
|
|
140
|
+
await page.waitForTimeout(_randInt(4, 16));
|
|
141
|
+
}
|
|
142
|
+
_lastMouse.set(page, { x: tx, y: ty });
|
|
143
|
+
}
|
|
144
|
+
// Move to a point a few px off the exact center, dwell, then press/release.
|
|
145
|
+
async function humanClick(page, x, y, clickCount = 1) {
|
|
146
|
+
const jx = x + _rand(-3, 3), jy = y + _rand(-3, 3);
|
|
147
|
+
await humanMove(page, jx, jy);
|
|
148
|
+
await humanPause(page, 30, 120);
|
|
149
|
+
for (let c = 0; c < clickCount; c++) {
|
|
150
|
+
await page.mouse.down();
|
|
151
|
+
await page.waitForTimeout(_randInt(40, 110)); // press dwell
|
|
152
|
+
await page.mouse.up();
|
|
153
|
+
if (c + 1 < clickCount)
|
|
154
|
+
await page.waitForTimeout(_randInt(60, 130));
|
|
155
|
+
}
|
|
156
|
+
_lastMouse.set(page, { x: jx, y: jy });
|
|
157
|
+
}
|
|
158
|
+
// Type with variable cadence: slower on spaces, longer at punctuation, and an
|
|
159
|
+
// occasional "thinking" pause.
|
|
160
|
+
async function humanType(page, text) {
|
|
161
|
+
for (const ch of text) {
|
|
162
|
+
await page.keyboard.type(ch);
|
|
163
|
+
let d = _rand(45, 150);
|
|
164
|
+
if (ch === " ")
|
|
165
|
+
d += _rand(10, 60);
|
|
166
|
+
if (/[.,!?;:]/.test(ch))
|
|
167
|
+
d += _rand(80, 220);
|
|
168
|
+
if (Math.random() < 0.04)
|
|
169
|
+
d += _rand(180, 450);
|
|
170
|
+
await page.waitForTimeout(Math.round(d));
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
// Break a wheel scroll into a few smaller, jittered steps with pauses.
|
|
174
|
+
async function humanScroll(page, dx, dy) {
|
|
175
|
+
const steps = _randInt(3, 6);
|
|
176
|
+
let sx = 0, sy = 0;
|
|
177
|
+
for (let i = 0; i < steps; i++) {
|
|
178
|
+
const px = Math.round(dx / steps + _rand(-8, 8));
|
|
179
|
+
const py = Math.round(dy / steps + _rand(-14, 14));
|
|
180
|
+
await page.mouse.wheel(px, py);
|
|
181
|
+
sx += px;
|
|
182
|
+
sy += py;
|
|
183
|
+
await page.waitForTimeout(_randInt(40, 130));
|
|
184
|
+
}
|
|
185
|
+
return { dx: sx, dy: sy };
|
|
186
|
+
}
|
|
73
187
|
const MAX_BODY_BYTES = 512 * 1024; // /browser/run scripts included
|
|
74
188
|
const NODE_CAP = 240; // snapshot node cap (Section 8)
|
|
75
189
|
const SCREENSHOT_MAX_B64 = 1_400_000; // ~1 MB binary; ladder recompresses
|
|
@@ -260,17 +374,24 @@ export async function startBrowserBridge(opts) {
|
|
|
260
374
|
headless: reg.spec.headless === true,
|
|
261
375
|
viewport: { width: 1280, height: 800 },
|
|
262
376
|
acceptDownloads: false, // downloads default-denied (Section 10)
|
|
377
|
+
ignoreDefaultArgs: ["--enable-automation"],
|
|
263
378
|
args: [
|
|
264
379
|
"--no-first-run",
|
|
265
380
|
"--no-default-browser-check",
|
|
266
381
|
"--disable-background-networking",
|
|
267
382
|
"--disable-sync",
|
|
383
|
+
"--disable-blink-features=AutomationControlled",
|
|
268
384
|
],
|
|
269
385
|
});
|
|
270
386
|
sessions.attachHandles(rec, {
|
|
271
387
|
context,
|
|
272
388
|
ephemeralUserDataDir: ephemeral ? userDataDir : null,
|
|
273
389
|
});
|
|
390
|
+
// Stealth patches before any page script (see STEALTH_SCRIPT).
|
|
391
|
+
try {
|
|
392
|
+
await context.addInitScript(STEALTH_SCRIPT);
|
|
393
|
+
}
|
|
394
|
+
catch { /* non-fatal */ }
|
|
274
395
|
await enforceOnContext(context, reg.policy, hooks);
|
|
275
396
|
const page = context.pages()[0] ?? (await context.newPage());
|
|
276
397
|
sessions.leaseTarget(rec, reg.spec.grant.target.ref, page);
|
|
@@ -401,6 +522,14 @@ export async function startBrowserBridge(opts) {
|
|
|
401
522
|
// all navigations and new pages. The one-shot evaluate covers the
|
|
402
523
|
// already-open page (typically about:blank) immediately.
|
|
403
524
|
// Both are wrapped so a branding failure cannot fail the launch.
|
|
525
|
+
// Stealth FIRST (runs before page + brand scripts in every frame) so the
|
|
526
|
+
// navigator/webgl patches are in place before any site fingerprinting.
|
|
527
|
+
try {
|
|
528
|
+
await context.addInitScript(STEALTH_SCRIPT);
|
|
529
|
+
}
|
|
530
|
+
catch (stealthErr) {
|
|
531
|
+
log(`[browser-launch] stealth init-script install failed (non-fatal): ${stealthErr?.message || stealthErr}`);
|
|
532
|
+
}
|
|
404
533
|
try {
|
|
405
534
|
await context.addInitScript(MELAYA_BRAND_SCRIPT);
|
|
406
535
|
}
|
|
@@ -852,16 +981,16 @@ export async function startBrowserBridge(opts) {
|
|
|
852
981
|
}
|
|
853
982
|
const pt = { x: box.x + box.width / 2, y: box.y + box.height / 2 };
|
|
854
983
|
if (kind === "hover")
|
|
855
|
-
await page
|
|
984
|
+
await humanMove(page, pt.x, pt.y);
|
|
856
985
|
else
|
|
857
|
-
await page
|
|
986
|
+
await humanClick(page, pt.x, pt.y, kind === "dblclick" ? 2 : 1);
|
|
858
987
|
return { at: { x: Math.round(pt.x), y: Math.round(pt.y) }, matched_text: label };
|
|
859
988
|
}
|
|
860
989
|
const pt = await resolveActionPoint(reg, rec, lease, args);
|
|
861
990
|
if (kind === "hover")
|
|
862
|
-
await page
|
|
991
|
+
await humanMove(page, pt.x, pt.y);
|
|
863
992
|
else
|
|
864
|
-
await page
|
|
993
|
+
await humanClick(page, pt.x, pt.y, kind === "dblclick" ? 2 : 1);
|
|
865
994
|
return { at: { x: Math.round(pt.x), y: Math.round(pt.y) } };
|
|
866
995
|
}
|
|
867
996
|
case "tap": {
|
|
@@ -869,7 +998,7 @@ export async function startBrowserBridge(opts) {
|
|
|
869
998
|
// browser.py sends {x, y} as 0..1 fractions or CSS px.
|
|
870
999
|
// resolveActionPoint already handles the fraction/px heuristic.
|
|
871
1000
|
const pt = await resolveActionPoint(reg, rec, lease, args);
|
|
872
|
-
await page
|
|
1001
|
+
await humanClick(page, pt.x, pt.y);
|
|
873
1002
|
return { at: { x: Math.round(pt.x), y: Math.round(pt.y) } };
|
|
874
1003
|
}
|
|
875
1004
|
case "input_text": {
|
|
@@ -877,10 +1006,11 @@ export async function startBrowserBridge(opts) {
|
|
|
877
1006
|
if (text.length > 20_000)
|
|
878
1007
|
throw new BridgeError("act_args_invalid", "text exceeds 20000 chars");
|
|
879
1008
|
const pt = await resolveActionPoint(reg, rec, lease, args);
|
|
880
|
-
await page
|
|
881
|
-
// Clear-then-type: select-all + type preserves site key handlers.
|
|
1009
|
+
await humanClick(page, pt.x, pt.y);
|
|
1010
|
+
// Clear-then-type: select-all + humanized type preserves site key handlers.
|
|
882
1011
|
await page.keyboard.press(process.platform === "darwin" ? "Meta+A" : "Control+A").catch(() => { });
|
|
883
|
-
await page
|
|
1012
|
+
await humanPause(page, 40, 120);
|
|
1013
|
+
await humanType(page, text);
|
|
884
1014
|
return { typed: text.length };
|
|
885
1015
|
}
|
|
886
1016
|
case "press_key": {
|
|
@@ -920,8 +1050,8 @@ export async function startBrowserBridge(opts) {
|
|
|
920
1050
|
else {
|
|
921
1051
|
dy = Math.max(-4000, Math.min(4000, Number(args.dy ?? 600)));
|
|
922
1052
|
}
|
|
923
|
-
await page
|
|
924
|
-
return { scrolled
|
|
1053
|
+
const scrolled = await humanScroll(page, dx, dy);
|
|
1054
|
+
return { scrolled };
|
|
925
1055
|
}
|
|
926
1056
|
case "get_text": {
|
|
927
1057
|
// Fix 1: read-only page-text extraction.
|