@xbrowser/cli 1.10.0 → 1.12.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.
Files changed (42) hide show
  1. package/dist/{anti-bot-DR56Y63V.js → anti-bot-GTTYNEFB.js} +1 -1
  2. package/dist/{browser-WQZ3D6AE.js → browser-3HQKDZQ6.js} +4 -2
  3. package/dist/{browser-T3V3JWVH.js → browser-ILXTEWH4.js} +1 -1
  4. package/dist/{browser-HBL72GPZ.js → browser-XPDMY2NX.js} +3 -3
  5. package/dist/{cdp-driver-J3YQ4LJV.js → cdp-driver-ESH3PDE6.js} +2 -1
  6. package/dist/cdp-driver-N2E2ZNSC.js +4644 -0
  7. package/dist/cdp-driver-YIKR4BUX.js +49 -0
  8. package/dist/chunk-3FWLW7FS.js +106 -0
  9. package/dist/chunk-A6LPGFAL.js +437 -0
  10. package/dist/{chunk-RRBXV7KE.js → chunk-DKM2JOZL.js} +633 -63
  11. package/dist/{chunk-ABXMBNQ6.js → chunk-H2A5JUK5.js} +37 -466
  12. package/dist/{chunk-5UGR6MUK.js → chunk-HBHOKZPN.js} +51 -16
  13. package/dist/{chunk-BNO7OKO4.js → chunk-HBMEFSTB.js} +39 -0
  14. package/dist/{chunk-INTQPBYF.js → chunk-JKVUFP3G.js} +6 -2
  15. package/dist/chunk-KJTABK3Z.js +1255 -0
  16. package/dist/{chunk-DWDXEGVK.js → chunk-NKW4A74J.js} +9 -3
  17. package/dist/{chunk-4452SPFI.js → chunk-NODRQGOK.js} +51 -16
  18. package/dist/{chunk-SQHSZENE.js → chunk-OMU63E6J.js} +3 -22
  19. package/dist/{chunk-NITFVWWS.js → chunk-OVW2UEWN.js} +981 -407
  20. package/dist/{chunk-WSCP7QCJ.js → chunk-R6IJ6PIV.js} +14 -11
  21. package/dist/{chunk-YMUSHPU4.js → chunk-SH6OTPXN.js} +46 -16
  22. package/dist/{chunk-MWFHZUIY.js → chunk-TEXCXIBW.js} +1 -1
  23. package/dist/{cdp-driver-2T4P4ZE2.js → chunk-Z6CUR3VG.js} +1542 -134
  24. package/dist/{chunk-IIM5GOD7.js → chunk-ZTHE5RBZ.js} +7 -1
  25. package/dist/cli.js +1136 -584
  26. package/dist/{daemon-client-GKEPT4NY.js → daemon-client-7D6EZNOE.js} +46 -16
  27. package/dist/{daemon-client-O6BYVRXV.js → daemon-client-HEGAXWGK.js} +1 -1
  28. package/dist/daemon-main.js +1031 -522
  29. package/dist/{human-interaction-5AO42MBA.js → human-interaction-4YR2N6R6.js} +2 -2
  30. package/dist/{human-interaction-C5OEGXGO.js → human-interaction-Y5IDH6LD.js} +1 -1
  31. package/dist/{human-interaction-ISXZTAYY.js → human-interaction-ZUTR5AC2.js} +2 -2
  32. package/dist/index.d.ts +10 -0
  33. package/dist/index.js +1147 -593
  34. package/dist/{proxy-C6CK3UH5.js → proxy-LUR4U5YF.js} +2 -1
  35. package/dist/{recovery-J2ISVGUL.js → recovery-FTZGV6VY.js} +2 -2
  36. package/dist/{recovery-ZJVVHP7N.js → recovery-L5GLDX4O.js} +1 -1
  37. package/dist/{recovery-EF33LKRJ.js → recovery-Z3RDZENA.js} +2 -2
  38. package/dist/{session-recorder-H3KEYU26.js → session-recorder-3BEVWHOK.js} +1 -1
  39. package/dist/{session-recorder-QRZMKFVL.js → session-recorder-SLDBENVF.js} +1 -1
  40. package/dist/{session-replayer-LJUC4TI7.js → session-replayer-K4A6OI4M.js} +90 -2
  41. package/package.json +1 -1
  42. package/dist/chunk-BAHSRZIX.js +0 -2191
@@ -24,6 +24,285 @@ import { EventEmitter as EventEmitter2 } from "events";
24
24
  // src/cdp-driver/page.ts
25
25
  import { EventEmitter } from "events";
26
26
 
27
+ // src/cdp-driver/stealth.ts
28
+ var DEFAULT_STEALTH_CONFIG = {
29
+ bezierCurvature: [0.35, 0.6],
30
+ noiseAmplitude: 5.5,
31
+ overshootRange: [6, 14],
32
+ aimPause: [150, 400],
33
+ pressDuration: [60, 140],
34
+ releaseDrift: [0.8, 2.5],
35
+ landingOffsetSmall: [0.3, 2.5],
36
+ landingOffsetLarge: [1.5, 7],
37
+ smallElementThreshold: 30,
38
+ typingRhythm: {
39
+ fastProb: 0.22,
40
+ fastRange: [25, 60],
41
+ normalRange: [50, 350],
42
+ pauseProb: 0.18,
43
+ pauseRange: [400, 1200]
44
+ },
45
+ keyPressDuration: [50, 110],
46
+ typoProbability: 0.06,
47
+ wheelPeak: 180,
48
+ wheelDecayRate: 0.4
49
+ };
50
+ function rand(min, max) {
51
+ return min + Math.random() * (max - min);
52
+ }
53
+ function cosineEase(t) {
54
+ return 0.5 - 0.5 * Math.cos(Math.PI * t);
55
+ }
56
+ function bezierTrajectory(x0, y0, x1, y1, config = DEFAULT_STEALTH_CONFIG) {
57
+ const dist = Math.hypot(x1 - x0, y1 - y0);
58
+ const n = Math.max(10, Math.min(28, Math.round(dist / 15)));
59
+ const shortMove = dist < 120;
60
+ const curvature = shortMove ? rand(2, 6) : Math.max(dist * rand(...config.bezierCurvature), rand(18, 35));
61
+ const dir = Math.random() < 0.5 ? 1 : -1;
62
+ const d = dist || 1;
63
+ const dx = x1 - x0, dy = y1 - y0;
64
+ const c1x = x0 + dx * 0.3 - dy / d * curvature * 0.5 * dir;
65
+ const c1y = y0 + dy * 0.3 + dx / d * curvature * 0.5 * dir;
66
+ const c2x = x0 + dx * 0.7 - dy / d * curvature * 0.8 * dir;
67
+ const c2y = y0 + dy * 0.7 + dx / d * curvature * 0.8 * dir;
68
+ const points = [];
69
+ for (let i = 1; i <= n; i++) {
70
+ const t = cosineEase(i / n);
71
+ const mt = 1 - t;
72
+ let px = mt ** 3 * x0 + 3 * mt ** 2 * t * c1x + 3 * mt * t ** 2 * c2x + t ** 3 * x1;
73
+ let py = mt ** 3 * y0 + 3 * mt ** 2 * t * c1y + 3 * mt * t ** 2 * c2y + t ** 3 * y1;
74
+ const amp = shortMove ? Math.min(2, config.noiseAmplitude) : config.noiseAmplitude;
75
+ px += rand(-amp, amp);
76
+ py += rand(-amp, amp);
77
+ points.push({ x: px, y: py, delay: rand(9, 16) });
78
+ }
79
+ if (!shortMove) {
80
+ const over = rand(...config.overshootRange);
81
+ const ox = x1 + dx / d * over + rand(-2, 2);
82
+ const oy = y1 + dy / d * over + rand(-2, 2);
83
+ points.push({ x: ox, y: oy, delay: rand(14, 30) });
84
+ points.push({
85
+ x: x1 + dx / d * over * 0.4,
86
+ y: y1 + dy / d * over * 0.4,
87
+ delay: rand(14, 30)
88
+ });
89
+ }
90
+ points.push({ x: x1 + rand(-1, 1), y: y1 + rand(-1, 1), delay: rand(14, 30) });
91
+ return points;
92
+ }
93
+ function landingOffset(width, height, config = DEFAULT_STEALTH_CONFIG) {
94
+ const isSmall = Math.min(width, height) < config.smallElementThreshold;
95
+ const range = isSmall ? config.landingOffsetSmall : config.landingOffsetLarge;
96
+ const dx = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
97
+ const dy = rand(...range) * (Math.random() < 0.5 ? -1 : 1);
98
+ return { dx, dy };
99
+ }
100
+ var KEY_MAP = {};
101
+ for (let i = 97; i <= 122; i++) {
102
+ const ch = String.fromCharCode(i);
103
+ KEY_MAP[ch] = { key: ch, code: "Key" + ch.toUpperCase(), vk: i - 32 };
104
+ }
105
+ for (let i = 65; i <= 90; i++) {
106
+ const ch = String.fromCharCode(i);
107
+ KEY_MAP[ch] = { key: ch, code: "Key" + ch, vk: i, shift: true };
108
+ }
109
+ for (let i = 48; i <= 57; i++) {
110
+ const ch = String.fromCharCode(i);
111
+ KEY_MAP[ch] = { key: ch, code: "Digit" + ch, vk: i };
112
+ }
113
+ Object.assign(KEY_MAP, {
114
+ " ": { key: " ", code: "Space", vk: 32 },
115
+ ".": { key: ".", code: "Period", vk: 190 },
116
+ "-": { key: "-", code: "Minus", vk: 189 },
117
+ "@": { key: "@", code: "Digit2", vk: 50, shift: true },
118
+ "_": { key: "_", code: "Minus", vk: 189, shift: true }
119
+ });
120
+ function buildStealthInitScript() {
121
+ return [
122
+ "(function(){",
123
+ // 1. AEL event proxy
124
+ " var o=EventTarget.prototype.addEventListener;",
125
+ " var fc=new InputDeviceCapabilities({firesTouchEvents:false});",
126
+ " var _ael=function(t,f){",
127
+ " var op=arguments[2];",
128
+ ' if(typeof f!=="function")return o.call(this,t,f,op);',
129
+ " var w=function(e){",
130
+ " if(!e||e.constructor===FocusEvent||e.constructor===KeyboardEvent)return f.call(this,e);",
131
+ " return f.call(this,new Proxy(e,{get:function(k,p){",
132
+ ' if(p==="sourceCapabilities")return fc;',
133
+ ' if(p==="isTrusted")return k.isTrusted;',
134
+ ' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isInteger(k[p])&&k.isTrusted===true){',
135
+ " var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;",
136
+ " return k[p]+_f;",
137
+ " }",
138
+ ' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
139
+ " }}));",
140
+ " };",
141
+ " return o.call(this,t,w,op);",
142
+ " };",
143
+ " EventTarget.prototype.addEventListener=_ael;",
144
+ // 2. Screen override (prototype-level, not instance-level)
145
+ " var _gw=function(){return 1728};",
146
+ " var _gh=function(){return 1117};",
147
+ " var _gah=function(){return 1092};",
148
+ ' Object.defineProperty(Screen.prototype,"width",{get:_gw,configurable:true});',
149
+ ' Object.defineProperty(Screen.prototype,"height",{get:_gh,configurable:true});',
150
+ ' Object.defineProperty(Screen.prototype,"availWidth",{get:_gw,configurable:true});',
151
+ ' Object.defineProperty(Screen.prototype,"availHeight",{get:_gah,configurable:true});',
152
+ " document.hasFocus=function(){return true};",
153
+ // 4. Canvas/WebGL fingerprint: per-session stable noise (d20).
154
+ // Headless software raster differs subtly from Chrome GPU raster —
155
+ // toDataURL hashes fingerprint the rasterizer. Inject a stable
156
+ // (per-page-load) subpixel shift into fillText so hashes look like a
157
+ // distinct-but-consistent real GPU, and hide the HEADLESS tell in
158
+ // WebGL renderer strings.
159
+ " var _seed=Math.floor(Math.random()*2147483647);",
160
+ " var _prng=function(){_seed=(_seed*48271)%2147483647;return _seed/2147483647;};",
161
+ " var _dx=(_prng()*0.4-0.2).toFixed(3)*1, _dy=(_prng()*0.4-0.2).toFixed(3)*1;",
162
+ " var _fillText=CanvasRenderingContext2D.prototype.fillText;",
163
+ " CanvasRenderingContext2D.prototype.fillText=function(t,x,y,m){",
164
+ " return _fillText.call(this,t,x+_dx,y+_dy,m);",
165
+ " };",
166
+ " var _toDataURL=HTMLCanvasElement.prototype.toDataURL;",
167
+ " HTMLCanvasElement.prototype.toDataURL=function(){",
168
+ ' var ctx=this.getContext("2d");',
169
+ " if(ctx){var d=ctx.getImageData(0,0,Math.min(this.width,2),Math.min(this.height,2));",
170
+ " for(var i=0;i<d.data.length;i+=4){if(d.data[i+3]>0){d.data[i]^=1;break;}}",
171
+ " ctx.putImageData(d,0,0);}",
172
+ " return _toDataURL.apply(this,arguments);",
173
+ " };",
174
+ " try{",
175
+ " var _gl=HTMLCanvasElement.prototype.getContext;",
176
+ " HTMLCanvasElement.prototype.getContext=function(t,o){",
177
+ " var c=_gl.call(this,t,o);",
178
+ ' if(c&&(t==="webgl"||t==="experimental-webgl")&&c.getParameter){',
179
+ " var _gp=c.getParameter.bind(c);",
180
+ " c.getParameter=function(p){",
181
+ " var v=_gp(p);",
182
+ ' if(typeof v==="string"&&/SwiftShader|Software|Rasterizer|Headless/i.test(v))',
183
+ ' return "ANGLE (Apple, ANGLE Metal Renderer: Apple M2 Max, Unspecified Version)";',
184
+ " return v;};}",
185
+ " return c;};",
186
+ " }catch(e){}",
187
+ // 5. AudioContext fingerprint: per-load stable micro-noise on channel
188
+ // data (d21). DSP sum differences between headless software audio and
189
+ // real hardware audio are a classic fingerprint — add ±1e-7 level
190
+ // noise (inaudible, changes the sum hash).
191
+ " try{",
192
+ " var _gcd=AudioBuffer.prototype.getChannelData;",
193
+ " AudioBuffer.prototype.getChannelData=function(ch){",
194
+ " var d=_gcd.call(this,ch);",
195
+ ' var key="__xb_audio_"+ch;',
196
+ " if(!this[key]){",
197
+ " this[key]=true;",
198
+ " for(var i=0;i<d.length;i+=997){d[i]=d[i]+(_prng()-0.5)*2e-7;}",
199
+ " }",
200
+ " return d;",
201
+ " };",
202
+ " var _gffd=AnalyserNode.prototype.getFloatFrequencyData;",
203
+ " AnalyserNode.prototype.getFloatFrequencyData=function(arr){",
204
+ " _gffd.call(this,arr);",
205
+ " for(var i=0;i<arr.length;i+=31){arr[i]=arr[i]+(_prng()-0.5)*0.01;}",
206
+ " };",
207
+ " var _gfbd=AnalyserNode.prototype.getByteFrequencyData;",
208
+ " AnalyserNode.prototype.getByteFrequencyData=function(arr){",
209
+ " _gfbd.call(this,arr);",
210
+ " for(var i=0;i<arr.length;i+=31){arr[i]=(arr[i]+((_prng()*3)|0))&255;}",
211
+ " };",
212
+ " }catch(e){}",
213
+ // 3b. Font metrics + speechSynthesis + battery (d22): headless tells
214
+ " try{",
215
+ " var _mt=CanvasRenderingContext2D.prototype.measureText;",
216
+ ' var _tmw=Object.getOwnPropertyDescriptor(TextMetrics.prototype,"width");',
217
+ " CanvasRenderingContext2D.prototype.measureText=function(t){",
218
+ " var m=_mt.call(this,t);",
219
+ ' try{Object.defineProperty(m,"width",{get:function(){return _tmw.get.call(m)+_dx*0.01;},configurable:true});}catch(e){}',
220
+ " return m;",
221
+ " };",
222
+ " }catch(e){}",
223
+ " try{",
224
+ ' var _fakeVoices=["Alex","Daniel","Karen","Moira","Ralph","Samantha","Ting-Ting","Mei-Jia","Sinji","Yunda"];',
225
+ " var _sv=speechSynthesis.getVoices.bind(speechSynthesis);",
226
+ " speechSynthesis.getVoices=function(){",
227
+ " var real=_sv();",
228
+ " if(real&&real.length)return real;",
229
+ ' return _fakeVoices.map(function(n,i){return {name:n,lang:i<2?"en-US":i<6?"en-GB":"zh-TW",localService:true,default:i===0,voiceURI:n};});',
230
+ " };",
231
+ " }catch(e){}",
232
+ " try{",
233
+ " var _gb=navigator.getBattery.bind(navigator);",
234
+ " navigator.getBattery=function(){",
235
+ " return _gb().then(function(b){",
236
+ ' Object.defineProperty(b,"charging",{get:function(){return true;},configurable:true});',
237
+ " return b;});",
238
+ " };",
239
+ " }catch(e){}",
240
+ // 3c. WebRTC local IP leak guard (d23): headless STUN candidates can
241
+ // expose host IPs; mDNS-only candidates are the modern Chrome default.
242
+ " try{",
243
+ " var _oc=RTCPeerConnection.prototype.createOffer;",
244
+ " RTCPeerConnection.prototype.createOffer=function(){",
245
+ " var p=_oc.apply(this,arguments);",
246
+ " var self=this;",
247
+ " return p.then(function(offer){",
248
+ ' offer.sdp=offer.sdp.split(String.fromCharCode(10)).filter(function(l){return l.indexOf("typ host")<0}).join(String.fromCharCode(10));',
249
+ " return offer;});",
250
+ " };",
251
+ " }catch(e){}",
252
+ // 3d. Chrome object depth (d24): automation fakes usually only set
253
+ // window.chrome = {}; deep checks hit app.run/runtime/csi/loadTimes.
254
+ " try{",
255
+ " if(window.chrome){",
256
+ ' if(!window.chrome.app||!window.chrome.app.run)window.chrome.app={run:function(){},load:function(){},getDetails:function(){return null},InstallState:{DISABLED:"disabled",INSTALLED:"installed",NOT_INSTALLED:"not_installed"},RunningState:{CANNOT_RUN:"cannot_run",READY_TO_RUN:"ready_to_run",RUNNING:"running"}};',
257
+ ' if(!window.chrome.runtime)window.chrome.runtime={OnInstalledReason:{CHROME_UPDATE:"chrome_update",INSTALL:"install",UPDATE:"update"},PlatformOs:{ANDROID:"android",CROS:"cros",LINUX:"linux",MAC:"mac",OPENBSD:"openbsd",WIN:"win"},connect:function(){},sendMessage:function(){},id:undefined};',
258
+ " if(!window.chrome.csi)window.chrome.csi=function(){return{};}",
259
+ " if(!window.chrome.loadTimes)window.chrome.loadTimes=function(){return{};}",
260
+ " }",
261
+ " }catch(e){}",
262
+ // 3e. Font availability patch (d25): headless Chromium misses some system
263
+ // fonts (e.g. Menlo on macOS) that real Chrome has. Register a FontFace
264
+ // aliasing the missing font to a local() equivalent so offsetWidth-based
265
+ // font probing sees the same availability as the faked environment.
266
+ " try{",
267
+ ' var _fontAliases=[["Menlo","Courier New"],["SF Mono","Menlo"],["Segoe UI","Helvetica"]];',
268
+ " _fontAliases.forEach(function(pa){",
269
+ ' try{var ff=new FontFace(pa[0],"local("+JSON.stringify(pa[1])+")");',
270
+ " document.fonts.add(ff);ff.load();}catch(e2){}",
271
+ " });",
272
+ " }catch(e){}",
273
+ // 3. toString disguise (name-list based)
274
+ " var _ts=Function.prototype.toString;",
275
+ " var _hf=document.hasFocus;",
276
+ " Function.prototype.toString=function(){",
277
+ ' if(this===_ael)return"function addEventListener(type, callback) { [native code] }";',
278
+ ' if(this===_hf)return"function hasFocus() { [native code] }";',
279
+ ' if(this===_gw)return"function get width() { [native code] }";',
280
+ ' if(this===_gh)return"function get height() { [native code] }";',
281
+ ' if(this===_gah)return"function get availHeight() { [native code] }";',
282
+ ' if(this===CanvasRenderingContext2D.prototype.fillText)return"function fillText() { [native code] }";',
283
+ ' if(this===HTMLCanvasElement.prototype.toDataURL)return"function toDataURL() { [native code] }";',
284
+ ' if(this===AnalyserNode.prototype.getFloatFrequencyData)return"function getFloatFrequencyData() { [native code] }";',
285
+ ' if(this===AudioBuffer.prototype.getChannelData)return"function getChannelData() { [native code] }";',
286
+ " return _ts.call(this);",
287
+ " };",
288
+ // 4. onclick prototype hijack (dual-stream consistency)
289
+ " var _ba=function(k,p){",
290
+ ' if(p==="isTrusted")return k.isTrusted;',
291
+ ' if((p==="clientX"||p==="clientY")&&k.type==="click"&&Number.isInteger(k[p])&&k.isTrusted===true){',
292
+ " var _f=((k.timeStamp||Date.now())%89)/89*0.7+0.15;return k[p]+_f;",
293
+ " }",
294
+ ' var v=Reflect.get(k,p);return typeof v==="function"?v.bind(k):v;',
295
+ " };",
296
+ ' Object.defineProperty(Document.prototype,"onclick",{',
297
+ " configurable:true,",
298
+ " get:function(){var raw=this.__ocRaw||null;if(!raw)return null;var self=this;",
299
+ " return function(e){return raw.call(self,new Proxy(e,{get:function(k,p){return _ba(k,p)}}))}},",
300
+ " set:function(fn){this.__ocRaw=fn}",
301
+ " });",
302
+ "})()"
303
+ ].join("\n");
304
+ }
305
+
27
306
  // src/cdp-driver/mouse.ts
28
307
  var XBMouseImpl = class {
29
308
  conn;
@@ -45,18 +324,46 @@ var XBMouseImpl = class {
45
324
  }
46
325
  async click(x, y, opts = {}) {
47
326
  const button = opts.button ?? "left";
48
- const clickCount = opts.clickCount ?? 1;
49
- const delay = opts.delay ?? 0;
50
- await this.move(x, y);
51
- await this.down({ button });
52
- if (delay > 0) {
53
- await sleep(delay);
327
+ const stealth = opts.stealth ?? process.env.XBROWSER_STEALTH !== "off";
328
+ let tx = x, ty = y;
329
+ if (stealth && opts.elementWidth !== void 0 && opts.elementHeight !== void 0) {
330
+ const off = landingOffset(opts.elementWidth, opts.elementHeight);
331
+ tx += off.dx;
332
+ ty += off.dy;
333
+ }
334
+ if (stealth) {
335
+ const traj = bezierTrajectory(this._x, this._y, tx, ty);
336
+ const _tb = Date.now();
337
+ let _truncated = false;
338
+ for (const p of traj) {
339
+ if (Date.now() - _tb > 5e3) {
340
+ _truncated = true;
341
+ break;
342
+ }
343
+ await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: p.x, y: p.y, button: this._button });
344
+ this._x = p.x;
345
+ this._y = p.y;
346
+ await sleep(p.delay);
347
+ }
348
+ this._x = tx;
349
+ this._y = ty;
350
+ if (_truncated) {
351
+ await this.send("Input.dispatchMouseEvent", { type: "mouseMoved", x: tx, y: ty, button: this._button });
352
+ }
353
+ await sleep(rand(...DEFAULT_STEALTH_CONFIG.aimPause));
354
+ } else {
355
+ await this.move(tx, ty);
54
356
  }
55
- await this.up({ button });
56
- for (let i = 1; i < clickCount; i++) {
57
- if (delay > 0) await sleep(delay);
357
+ await this.down({ button });
358
+ await sleep(stealth ? rand(...DEFAULT_STEALTH_CONFIG.pressDuration) : opts.delay ?? 0);
359
+ const rx = stealth ? this._x + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._x;
360
+ const ry = stealth ? this._y + rand(...DEFAULT_STEALTH_CONFIG.releaseDrift) * (Math.random() < 0.5 ? -1 : 1) : this._y;
361
+ this._x = rx;
362
+ this._y = ry;
363
+ await this.send("Input.dispatchMouseEvent", { type: "mouseReleased", x: rx, y: ry, button, clickCount: opts.clickCount ?? 1 });
364
+ for (let i = 1; i < (opts.clickCount ?? 1); i++) {
365
+ if (opts.delay) await sleep(opts.delay);
58
366
  await this.down({ button });
59
- if (delay > 0) await sleep(delay);
60
367
  await this.up({ button });
61
368
  }
62
369
  }
@@ -147,12 +454,20 @@ var XBKeyboardImpl = class {
147
454
  }
148
455
  await this.dispatchKeyEvent(downParams);
149
456
  if (mapping.text) {
457
+ if (process.env.XBROWSER_STEALTH !== "off") {
458
+ await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
459
+ }
150
460
  await this.dispatchKeyEvent({
151
461
  type: "char",
152
462
  text: mapping.text
153
463
  });
154
464
  }
155
465
  if (delay > 0) await sleep2(delay);
466
+ else {
467
+ if (process.env.XBROWSER_STEALTH !== "off") {
468
+ await sleep2(rand(...DEFAULT_STEALTH_CONFIG.keyPressDuration));
469
+ }
470
+ }
156
471
  const upParams = {
157
472
  type: "keyUp",
158
473
  key: mapping.key,
@@ -235,7 +550,7 @@ var XBKeyboardImpl = class {
235
550
  }
236
551
  };
237
552
  function resolveKeyMapping(key) {
238
- if (KEY_MAP[key]) return KEY_MAP[key];
553
+ if (KEY_MAP2[key]) return KEY_MAP2[key];
239
554
  if (key.length === 1) {
240
555
  const lower = key.toLowerCase();
241
556
  if (lower >= "a" && lower <= "z") {
@@ -252,7 +567,7 @@ function resolveKeyMapping(key) {
252
567
  }
253
568
  return { key, code: key };
254
569
  }
255
- var KEY_MAP = {
570
+ var KEY_MAP2 = {
256
571
  Enter: { key: "Enter", code: "Enter", text: "\r", keyCode: 13 },
257
572
  Tab: { key: "Tab", code: "Tab", text: " ", keyCode: 9 },
258
573
  Escape: { key: "Escape", code: "Escape", keyCode: 27 },
@@ -290,6 +605,37 @@ function sleep2(ms) {
290
605
 
291
606
  // src/cdp-driver/selector-utils.ts
292
607
  function queryJS(selector) {
608
+ return `(${deepQueryIIFE})( ${JSON.stringify(queryMainJS(selector))} )`;
609
+ }
610
+ var deepQueryIIFE = `(function(mainExpr) {
611
+ const run = (root) => {
612
+ try { return new Function('document', 'return (' + mainExpr + ')')(root); }
613
+ catch (e) { return null; }
614
+ };
615
+ const scanRoot = (root) => {
616
+ const direct = run(root);
617
+ if (direct) return direct;
618
+ let all;
619
+ try { all = root.querySelectorAll('*'); } catch (e) { return null; }
620
+ for (const el of all) {
621
+ if (el.shadowRoot) {
622
+ const r = scanRoot(el.shadowRoot);
623
+ if (r) return r;
624
+ }
625
+ if (el.tagName === 'IFRAME') {
626
+ let inner = null;
627
+ try { inner = el.contentDocument; } catch (e) { /* cross-origin */ }
628
+ if (inner) {
629
+ const r = scanRoot(inner);
630
+ if (r) return r;
631
+ }
632
+ }
633
+ }
634
+ return null;
635
+ };
636
+ return scanRoot(document);
637
+ })`;
638
+ function queryMainJS(selector) {
293
639
  if (selector.startsWith("xpath=")) {
294
640
  const xpath = JSON.stringify(selector.slice(6));
295
641
  return `document.evaluate(${xpath}, document, null, XPathResult.FIRST_ORDERED_NODE_TYPE, null).singleNodeValue`;
@@ -301,13 +647,35 @@ function queryJS(selector) {
301
647
  return `(() => {
302
648
  const target = ${JSON.stringify(text)};
303
649
  const exact = ${exact};
650
+ // Match on OWN text nodes (not strict leaf elements): search-result
651
+ // titles mix text with inline highlight <em> marks \u2014 a strict leaf filter
652
+ // finds nothing there (real-world juejin). Own-text keeps the match
653
+ // precise (descendant-only text doesn't count) while tolerating markup.
654
+ const ownText = (e) => Array.prototype.filter.call(e.childNodes, (n) => n.nodeType === 3)
655
+ .map((n) => n.textContent).join('').trim();
304
656
  const els = [...document.querySelectorAll('*')].filter(e => {
305
- if (e.children.length > 0) return false;
306
- if (e.offsetParent === null) return false;
307
- const t = (e.textContent || '').trim();
657
+ if (e.offsetParent === null && e.tagName !== 'BODY') return false;
658
+ const t = ownText(e);
308
659
  if (!t) return false;
309
660
  return exact ? t === target : t.toLowerCase().includes(target.toLowerCase());
310
661
  });
662
+ // Rank instead of raw DOM order: exact text beats substring, interactive
663
+ // elements (button/a/[onclick]/inputs) beat prose. Prevents matching a
664
+ // description paragraph that merely MENTIONS the target label
665
+ // (rec-duel d06: header text "\u76EE\u6807\u9879\u300C\u7B2C 87 \u53F7\u300D" hijacked text=\u7B2C 87 \u53F7).
666
+ const isInteractive = (e) => {
667
+ const tag = e.tagName;
668
+ return tag === 'BUTTON' || tag === 'A' || tag === 'INPUT' || tag === 'SELECT'
669
+ || e.hasAttribute('onclick') || e.getAttribute('role') === 'button';
670
+ };
671
+ els.sort((a, b) => {
672
+ const ta = ownText(a), tb = ownText(b);
673
+ const ea = ta === target ? 0 : 1, eb = tb === target ? 0 : 1;
674
+ if (ea !== eb) return ea - eb;
675
+ const ia = isInteractive(a) ? 0 : 1, ib = isInteractive(b) ? 0 : 1;
676
+ if (ia !== ib) return ia - ib;
677
+ return 0; // stable \u2014 preserve DOM order
678
+ });
311
679
  return els[0] || null;
312
680
  })()`;
313
681
  }
@@ -341,43 +709,53 @@ function queryAllJS(selector) {
341
709
  async function waitForActionable(page, selector, opts = {}) {
342
710
  const timeout = opts.timeout ?? 3e4;
343
711
  if (opts.force) {
344
- if (selector.startsWith("xpath=")) {
345
- const rect2 = await page.evaluate(`
712
+ const deadline2 = Date.now() + timeout;
713
+ let lastError;
714
+ while (Date.now() < deadline2) {
715
+ const rect = await page.evaluate(`
346
716
  (function() {
347
717
  const el = ${queryJS(selector)};
348
718
  if (!el) return null;
349
719
  const r = el.getBoundingClientRect();
350
- return { x: r.x, y: r.y, width: r.width, height: r.height };
720
+ let x = r.x, y = r.y;
721
+ let doc = el.ownerDocument;
722
+ while (doc !== document) {
723
+ let host = null;
724
+ const scan = (d) => {
725
+ let frames;
726
+ try { frames = d.querySelectorAll('iframe'); } catch (e) { return null; }
727
+ for (const f of frames) {
728
+ let inner = null;
729
+ try { inner = f.contentDocument; } catch (e) { continue; }
730
+ if (!inner) continue;
731
+ if (inner === doc) return f;
732
+ const rr = scan(inner);
733
+ if (rr) return rr;
734
+ }
735
+ return null;
736
+ };
737
+ host = scan(document);
738
+ if (!host) break;
739
+ const hr = host.getBoundingClientRect();
740
+ x += hr.x; y += hr.y;
741
+ doc = host.ownerDocument;
742
+ }
743
+ return { x, y, width: r.width, height: r.height };
351
744
  })()
352
- `);
353
- if (!rect2) throw new Error(`Element not found: ${selector}`);
354
- return { nodeId: 0, rect: rect2 };
355
- }
356
- const deadline2 = Date.now() + timeout;
357
- let lastError;
358
- let nodeId = 0;
359
- let rect = null;
360
- while (Date.now() < deadline2) {
361
- nodeId = await page.querySelector(selector);
362
- if (!nodeId) {
363
- lastError = `Element not found: ${selector}`;
364
- await page.waitForTimeout(200);
365
- continue;
366
- }
367
- rect = await page.getBoxModel(nodeId);
368
- if (rect) break;
369
- lastError = `Element has no box: ${selector}`;
370
- await page.waitForTimeout(500);
745
+ `).catch(() => null);
746
+ if (rect && rect.width > 0 && rect.height > 0) return { nodeId: 0, rect };
747
+ lastError = `Element not visible (zero size): ${selector}`;
748
+ lastError = `Element not found: ${selector}`;
749
+ await page.waitForTimeout(200);
371
750
  }
372
- if (!rect) throw new Error(lastError || `Element has no box: ${selector}`);
373
- return { nodeId, rect };
751
+ throw new Error(lastError || `Element not found: ${selector}`);
374
752
  }
375
753
  const deadline = Date.now() + timeout;
376
754
  while (Date.now() < deadline) {
377
755
  const result = await checkActionable(page, selector);
378
756
  if (result.ok && result.rect) {
379
- const nodeId = await page.querySelector(selector);
380
- if (nodeId) return { nodeId, rect: result.rect };
757
+ const nodeId = await page.querySelector(selector).catch(() => 0) ?? 0;
758
+ return { nodeId, rect: result.rect };
381
759
  }
382
760
  await page.waitForTimeout(50);
383
761
  }
@@ -410,12 +788,25 @@ async function checkActionable(page, selector) {
410
788
  return { ok: false, reason: 'parent_disabled' };
411
789
  }
412
790
 
413
- // Check not covered by another element at center
791
+ // Check not covered by another element at center.
792
+ // elementFromPoint must run in the element's OWN document: for iframe-
793
+ // internal elements the main-document hit-test returns the <iframe>
794
+ // host itself, which falsely reports "covered" (rec-duel d01).
795
+ // For shadow-internal elements the hit-test retargets to the shadow
796
+ // HOST \u2014 walk the host chain before declaring coverage (rec-duel d04).
414
797
  const cx = rect.x + rect.width / 2;
415
798
  const cy = rect.y + rect.height / 2;
416
- const topEl = document.elementFromPoint(cx, cy);
799
+ const topEl = el.ownerDocument.elementFromPoint(cx, cy);
417
800
  if (topEl && topEl !== el && !el.contains(topEl) && !topEl.contains(el)) {
418
- return { ok: false, reason: 'covered', rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
801
+ let hostChain = [];
802
+ let rootNode = el.getRootNode();
803
+ while (rootNode && rootNode.host) {
804
+ hostChain.push(rootNode.host);
805
+ rootNode = rootNode.host.getRootNode();
806
+ }
807
+ if (!hostChain.includes(topEl)) {
808
+ return { ok: false, reason: 'covered', rect: { x: rect.x, y: rect.y, width: rect.width, height: rect.height } };
809
+ }
419
810
  }
420
811
 
421
812
  return {
@@ -460,21 +851,48 @@ var XBLocatorImpl = class _XBLocatorImpl {
460
851
  const el = ${this._q(this.selector)};
461
852
  if (!el) return null;
462
853
  const rect = el.getBoundingClientRect();
463
- return { x: rect.x, y: rect.y, width: rect.width, height: rect.height };
854
+ let x = rect.x, y = rect.y;
855
+ let doc = el.ownerDocument;
856
+ while (doc !== document) {
857
+ let host = null;
858
+ const scan = (d) => {
859
+ let frames;
860
+ try { frames = d.querySelectorAll('iframe'); } catch (e) { return null; }
861
+ for (const f of frames) {
862
+ let inner = null;
863
+ try { inner = f.contentDocument; } catch (e) { continue; }
864
+ if (!inner) continue;
865
+ if (inner === doc) return f;
866
+ const r = scan(inner);
867
+ if (r) return r;
868
+ }
869
+ return null;
870
+ };
871
+ host = scan(document);
872
+ if (!host) break;
873
+ const hr = host.getBoundingClientRect();
874
+ x += hr.x; y += hr.y;
875
+ doc = host.ownerDocument;
876
+ }
877
+ return { x, y, width: rect.width, height: rect.height };
464
878
  })()
465
879
  `);
466
880
  const finalRect = updatedRect ?? rect;
467
881
  const cx = finalRect.x + finalRect.width / 2;
468
882
  const cy = finalRect.y + finalRect.height / 2;
469
883
  await this.page.mouse.click(cx, cy, {
470
- button: opts.button ?? "left",
471
- clickCount: opts.clickCount ?? 1,
472
- delay: opts.delay
884
+ stealth: true,
885
+ elementWidth: finalRect.width,
886
+ elementHeight: finalRect.height,
887
+ ...{ button: opts.button ?? "left", clickCount: opts.clickCount ?? 1, delay: opts.delay }
473
888
  });
474
889
  }
475
890
  async fill(value, opts = {}) {
476
891
  await waitForActionable(this.page, this.selector, opts);
477
892
  await scrollIntoView(this.page, this.selector);
893
+ await this.click({ ...opts });
894
+ await this.page.keyboard.type(value, { stealth: true });
895
+ return;
478
896
  await this.page.evaluate(`
479
897
  (function() {
480
898
  const el = ${this._q(this.selector)};
@@ -1184,6 +1602,8 @@ var XBPageImpl = class _XBPageImpl {
1184
1602
  await this.conn.send("Page.enable", void 0, this.sessionId);
1185
1603
  await this.conn.send("Runtime.enable", void 0, this.sessionId);
1186
1604
  await this.conn.send("Network.enable", void 0, this.sessionId);
1605
+ await this.conn.send("DOM.enable", void 0, this.sessionId).catch(() => {
1606
+ });
1187
1607
  this.setupPageEvents();
1188
1608
  this.setupNetworkEvents();
1189
1609
  this.setupConsoleEvents();
@@ -1196,6 +1616,9 @@ var XBPageImpl = class _XBPageImpl {
1196
1616
  );
1197
1617
  this._url = info.url;
1198
1618
  this._title = info.title;
1619
+ if (info.url && info.url !== "about:blank" && info.url !== "") {
1620
+ this._loadState = { loadFired: true, domContentFired: true, networkIdle: true };
1621
+ }
1199
1622
  } catch {
1200
1623
  }
1201
1624
  }
@@ -1208,6 +1631,16 @@ var XBPageImpl = class _XBPageImpl {
1208
1631
  const waitUntil = opts.waitUntil ?? "load";
1209
1632
  const timeout = opts.timeout ?? 3e4;
1210
1633
  this._loadState = { loadFired: false, domContentFired: false, networkIdle: false };
1634
+ if (process.env.XBROWSER_STEALTH !== "off") {
1635
+ try {
1636
+ await this.conn.send(
1637
+ "Page.addScriptToEvaluateOnNewDocument",
1638
+ { source: buildStealthInitScript() },
1639
+ this.sessionId
1640
+ );
1641
+ } catch {
1642
+ }
1643
+ }
1211
1644
  const result = await this.conn.send(
1212
1645
  "Page.navigate",
1213
1646
  { url, referrer: opts.referer },
@@ -1415,6 +1848,91 @@ Last error: ${lastError.message}` : "";
1415
1848
  }
1416
1849
  return result.result?.value;
1417
1850
  }
1851
+ /**
1852
+ * 在指定 iframe 上下文中执行表达式(攻防 D16 能力建设,2026-08-19)。
1853
+ *
1854
+ * 双路径:
1855
+ * 1. 同进程 iframe —— Runtime.enable 收集 executionContextCreated,
1856
+ * 找到目标 frameId 的 contextId,用 contextId 定向执行;
1857
+ * 2. 跨域 OOPIF(独立 target)—— Target.setAutoAttach(flatten) 监听
1858
+ * attachedToTarget 中 type==='iframe' 的会话,用其 sessionId 执行。
1859
+ *
1860
+ * 这绕过了页面同源策略(那是页面 JS 的约束,CDP 是调试通道)——
1861
+ * 支付窗/验证码/第三方嵌入内容的读写都靠它。
1862
+ */
1863
+ async evaluateInFrame(urlIncludes, expression) {
1864
+ if (this._closed) throw new Error("Page is closed");
1865
+ const evalIn = async (sessionId, contextId) => {
1866
+ const params = { expression, returnByValue: true, awaitPromise: true };
1867
+ if (contextId !== void 0) params.contextId = contextId;
1868
+ const result = await this.conn.send("Runtime.evaluate", params, sessionId);
1869
+ if (result.exceptionDetails) {
1870
+ const detail = result.exceptionDetails.exception?.description ?? result.exceptionDetails.exception?.value ?? result.exceptionDetails.text;
1871
+ throw new Error(`[frame ${urlIncludes}] ${detail}`);
1872
+ }
1873
+ return result.result?.value;
1874
+ };
1875
+ try {
1876
+ const tg = await this.conn.send("Target.getTargets", void 0);
1877
+ const hit2 = (tg.targetInfos || []).find((t) => t.type === "iframe" && (t.url || "").includes(urlIncludes));
1878
+ if (hit2) {
1879
+ const att = await this.conn.send("Target.attachToTarget", { targetId: hit2.targetId, flatten: true });
1880
+ return evalIn(att.sessionId);
1881
+ }
1882
+ } catch {
1883
+ }
1884
+ const tree = await this.conn.send("Page.getFrameTree", void 0, this.sessionId);
1885
+ const all = [];
1886
+ const walk = (node) => {
1887
+ all.push({ id: node.frame.id, url: node.frame.url });
1888
+ for (const child of node.childFrames || []) walk(child);
1889
+ };
1890
+ walk(tree.frameTree);
1891
+ const mainId = tree.frameTree?.frame?.id;
1892
+ const target = all.find((f) => f.id !== mainId && f.url.includes(urlIncludes));
1893
+ if (target) {
1894
+ const contexts = [];
1895
+ const onCtx = (raw) => {
1896
+ const c = raw?.context;
1897
+ if (c?.id && c?.auxData?.frameId) contexts.push({ id: c.id, frameId: c.auxData.frameId });
1898
+ };
1899
+ this.conn.on("Runtime.executionContextCreated", onCtx);
1900
+ try {
1901
+ await this.conn.send("Runtime.enable", void 0, this.sessionId).catch(() => {
1902
+ });
1903
+ await new Promise((r) => setTimeout(r, 400));
1904
+ } finally {
1905
+ this.conn.off("Runtime.executionContextCreated", onCtx);
1906
+ }
1907
+ const ctx = contexts.find((c) => c.frameId === target.id);
1908
+ if (ctx) return evalIn(this.sessionId, ctx.id);
1909
+ }
1910
+ const attached = [];
1911
+ const onAttach = (raw) => {
1912
+ const ev = raw;
1913
+ if (ev?.sessionId && ev.targetInfo?.type === "iframe") {
1914
+ attached.push({ sessionId: ev.sessionId, url: ev.targetInfo.url || "" });
1915
+ }
1916
+ };
1917
+ this.conn.on("Target.attachedToTarget", onAttach);
1918
+ try {
1919
+ await this.conn.send("Target.setAutoAttach", {
1920
+ autoAttach: true,
1921
+ waitForDebuggerOnStart: false,
1922
+ flatten: true
1923
+ }, this.sessionId);
1924
+ await new Promise((r) => setTimeout(r, 600));
1925
+ } finally {
1926
+ this.conn.off("Target.attachedToTarget", onAttach);
1927
+ this.conn.send("Target.setAutoAttach", { autoAttach: false, waitForDebuggerOnStart: false, flatten: true }, this.sessionId).catch(() => {
1928
+ });
1929
+ }
1930
+ const hit = attached.find((a) => a.url.includes(urlIncludes));
1931
+ if (!hit) {
1932
+ throw new Error(`frame not found for "${urlIncludes}"\uFF08OOPIF target\u3001frame \u6811\u3001auto-attach \u4E09\u8DEF\u5747\u672A\u547D\u4E2D\uFF1B\u53EF\u80FD\u4ECD\u5728\u52A0\u8F7D\uFF09`);
1933
+ }
1934
+ return evalIn(hit.sessionId);
1935
+ }
1418
1936
  /** evaluateHandle — evaluates fn and returns a handle for element bounding box */
1419
1937
  async evaluateHandle(fn, ...args) {
1420
1938
  let expression;
@@ -1659,6 +2177,10 @@ Last error: ${lastError.message}` : "";
1659
2177
  this.sessionId
1660
2178
  );
1661
2179
  }
2180
+ /** 覆盖 User-Agent(headless 环境对抗 UA 检测的标配能力) */
2181
+ async setUserAgent(userAgent) {
2182
+ await this._setUserAgent(userAgent);
2183
+ }
1662
2184
  /** Internal: set extra HTTP headers */
1663
2185
  async _setExtraHTTPHeaders(headers) {
1664
2186
  await this.setExtraHTTPHeaders(headers);
@@ -1785,16 +2307,21 @@ Last error: ${lastError.message}` : "";
1785
2307
  }
1786
2308
  return 1;
1787
2309
  }
1788
- const doc = await this.conn.send(
1789
- "DOM.getDocument",
1790
- { depth: 0 },
1791
- this.sessionId
2310
+ const withTimeout = (p, ms) => Promise.race([p, new Promise((r) => setTimeout(() => r(null), ms))]);
2311
+ const doc = await withTimeout(
2312
+ this.conn.send("DOM.getDocument", { depth: 0 }, this.sessionId),
2313
+ 8e3
1792
2314
  );
1793
- const result = await this.conn.send(
1794
- "DOM.querySelector",
1795
- { nodeId: doc.root.nodeId, selector },
1796
- this.sessionId
2315
+ if (!doc) return 0;
2316
+ const result = await withTimeout(
2317
+ this.conn.send(
2318
+ "DOM.querySelector",
2319
+ { nodeId: doc.root.nodeId, selector },
2320
+ this.sessionId
2321
+ ),
2322
+ 8e3
1797
2323
  );
2324
+ if (!result) return 0;
1798
2325
  return result.nodeId;
1799
2326
  }
1800
2327
  /** Query all matching elements, returns array of CDP nodeIds */
@@ -2783,318 +3310,43 @@ var XBBrowserImpl = class {
2783
3310
  // src/cdp-driver/connection.ts
2784
3311
  import { EventEmitter as EventEmitter4 } from "events";
2785
3312
  import { WebSocket } from "ws";
2786
- var CDPConnection = class extends EventEmitter4 {
2787
- ws;
2788
- nextId = 1;
2789
- pending = /* @__PURE__ */ new Map();
2790
- closed = false;
2791
- closeReason = null;
2792
- /** Default session ID for flat session protocol (Target.attachToTarget) */
2793
- defaultSessionId;
2794
- constructor(wsOrUrl, sessionId) {
2795
- super();
2796
- this.setMaxListeners(0);
2797
- this.defaultSessionId = sessionId;
2798
- if (typeof wsOrUrl === "string") {
2799
- const wsOptions = /^wss:\/\/\d+\.\d+\.\d+\.\d+/.test(wsOrUrl) ? { rejectUnauthorized: false } : void 0;
2800
- this.ws = new WebSocket(wsOrUrl, wsOptions);
2801
- } else {
2802
- this.ws = wsOrUrl;
3313
+
3314
+ // src/cdp-interceptor/rules/shared.ts
3315
+ var PLAYWRIGHT_INTERNAL_MARKERS = [
3316
+ "__commonJS",
3317
+ "module.exports",
3318
+ "__require",
3319
+ "__toESM",
3320
+ "inject_utils"
3321
+ ];
3322
+ function isPlaywrightInternal(code) {
3323
+ return PLAYWRIGHT_INTERNAL_MARKERS.some((marker) => code.includes(marker));
3324
+ }
3325
+ function extractUserCode(ctx) {
3326
+ if (ctx.method === "Runtime.evaluate") {
3327
+ const expr = ctx.params.expression;
3328
+ if (typeof expr === "string") {
3329
+ if (isPlaywrightInternal(expr)) return null;
3330
+ return expr;
2803
3331
  }
2804
- this.bindWebSocket();
2805
- this.startKeepalive();
2806
- }
2807
- /** Send periodic WS pings to prevent idle-timeout disconnects (e.g. CF's 100s).
2808
- * Also detects dead connections: if a pong isn't received within 10s of a
2809
- * ping, the connection is considered dead and forcibly closed. */
2810
- keepaliveTimer = null;
2811
- pongTimer = null;
2812
- startKeepalive() {
2813
- this.ws.on("pong", () => {
2814
- if (this.pongTimer) {
2815
- clearTimeout(this.pongTimer);
2816
- this.pongTimer = null;
2817
- }
2818
- });
2819
- this.keepaliveTimer = setInterval(() => {
2820
- if (this.ws.readyState === WebSocket.OPEN) {
2821
- if (!this.pongTimer) {
2822
- this.pongTimer = setTimeout(() => {
2823
- if (!this.closed) {
2824
- this.closed = true;
2825
- this.closeReason = "keepalive timeout (no pong in 10s)";
2826
- try {
2827
- this.ws.terminate();
2828
- } catch {
2829
- }
2830
- for (const [, pending] of this.pending) {
2831
- clearTimeout(pending.timeout);
2832
- pending.reject(new Error("Connection dead: keepalive timeout"));
2833
- }
2834
- this.pending.clear();
2835
- this.emit("disconnect");
2836
- }
2837
- }, 1e4);
2838
- }
2839
- this.ws.ping?.();
2840
- } else if (this.closed) {
2841
- if (this.keepaliveTimer) clearInterval(this.keepaliveTimer);
2842
- this.keepaliveTimer = null;
2843
- }
2844
- }, 3e4);
2845
3332
  }
2846
- /** Wait for the connection to be fully open */
2847
- async ready() {
2848
- if (this.ws.readyState === WebSocket.OPEN) return;
2849
- if (this.ws.readyState === WebSocket.CLOSED || this.ws.readyState === WebSocket.CLOSING) {
2850
- throw new Error(`WebSocket already closed: ${this.closeReason ?? "unknown"}`);
3333
+ if (ctx.method === "Runtime.callFunctionOn") {
3334
+ const decl = ctx.params.functionDeclaration;
3335
+ if (typeof decl === "string" && decl.includes("utilityScript.evaluate")) {
3336
+ return extractAllStrings(ctx.params.arguments);
2851
3337
  }
2852
- return new Promise((resolve, reject) => {
2853
- const onOpen = () => {
2854
- this.ws.off("error", onError);
2855
- resolve();
2856
- };
2857
- const onError = (err) => {
2858
- this.ws.off("open", onOpen);
2859
- reject(err);
2860
- };
2861
- this.ws.once("open", onOpen);
2862
- this.ws.once("error", onError);
2863
- });
2864
- }
2865
- /** Is the underlying WebSocket alive? */
2866
- get isOpen() {
2867
- return !this.closed && this.ws.readyState === WebSocket.OPEN;
3338
+ if (typeof decl === "string") return decl;
2868
3339
  }
2869
- /**
2870
- * Send a CDP command and await its response.
2871
- *
2872
- * @param method — CDP domain.method (e.g. "Page.navigate")
2873
- * @param params — method parameters
2874
- * @param sessionId optional flat session ID for sub-targets
2875
- * @param timeoutMs response timeout (default: 30s)
2876
- * @returns the `result` field from the CDP response
2877
- */
2878
- async send(method, params, sessionId, timeoutMs = 3e4) {
2879
- if (this.closed) {
2880
- throw new Error(`CDP connection closed: ${this.closeReason ?? "unknown"}`);
2881
- }
2882
- if (!this.isOpen) {
2883
- throw new Error(`CDP connection not open (state: ${this.ws.readyState})`);
2884
- }
2885
- const id = this.nextId++;
2886
- const sid = sessionId ?? this.defaultSessionId;
2887
- const message = { id, method };
2888
- if (params !== void 0) message.params = params;
2889
- if (sid !== void 0) message.sessionId = sid;
2890
- return new Promise((resolve, reject) => {
2891
- const timeout = setTimeout(() => {
2892
- this.pending.delete(id);
2893
- reject(new Error(`CDP timeout: ${method} (${timeoutMs}ms)`));
2894
- }, timeoutMs);
2895
- this.pending.set(id, {
2896
- resolve: (v) => {
2897
- clearTimeout(timeout);
2898
- this.pending.delete(id);
2899
- resolve(v);
2900
- },
2901
- reject: (err) => {
2902
- clearTimeout(timeout);
2903
- this.pending.delete(id);
2904
- reject(err);
2905
- },
2906
- method,
2907
- timeout
2908
- });
2909
- const data = JSON.stringify(message);
2910
- try {
2911
- this.ws.send(data);
2912
- } catch (err) {
2913
- clearTimeout(timeout);
2914
- this.pending.delete(id);
2915
- reject(new Error(`CDP send failed: ${method} \u2014 ${err instanceof Error ? err.message : String(err)}`));
2916
- }
2917
- });
2918
- }
2919
- /**
2920
- * Subscribe to a CDP event.
2921
- *
2922
- * @param event — full event name (e.g. "Page.frameNavigated")
2923
- * @param handler — called with the event params
2924
- * @param sessionId — optional session filter
2925
- */
2926
- on(event, handler) {
2927
- return super.on(event, handler);
2928
- }
2929
- once(event, handler) {
2930
- return super.once(event, handler);
2931
- }
2932
- /** Remove an event listener */
2933
- off(event, handler) {
2934
- super.off(event, handler);
2935
- return this;
2936
- }
2937
- /**
2938
- * Subscribe to a CDP event for a specific session.
2939
- * Returns an unsubscribe function.
2940
- */
2941
- subscribe(event, sessionId, handler) {
2942
- const wrapper = (params, sid) => {
2943
- if (sid === sessionId || !sessionId && !sid) handler(params);
2944
- };
2945
- this.on(event, wrapper);
2946
- return () => this.off(event, wrapper);
2947
- }
2948
- /** Close the WebSocket */
2949
- async close() {
2950
- if (this.closed) return;
2951
- this.closed = true;
2952
- this.closeReason = "closed by caller";
2953
- if (this.keepaliveTimer) {
2954
- clearInterval(this.keepaliveTimer);
2955
- this.keepaliveTimer = null;
2956
- }
2957
- if (this.pongTimer) {
2958
- clearTimeout(this.pongTimer);
2959
- this.pongTimer = null;
2960
- }
2961
- for (const [id, pending] of this.pending) {
2962
- clearTimeout(pending.timeout);
2963
- pending.reject(new Error(`Connection closed: ${pending.method}`));
2964
- this.pending.delete(id);
2965
- }
2966
- if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
2967
- this.ws.close(1e3, "normal closure");
2968
- }
2969
- }
2970
- /** Set the default session ID for flat protocol */
2971
- setDefaultSessionId(sid) {
2972
- this.defaultSessionId = sid;
2973
- }
2974
- // ── Private ─────────────────────────────────────────────────
2975
- bindWebSocket() {
2976
- this.ws.on("message", (raw) => {
2977
- let msg;
2978
- try {
2979
- msg = JSON.parse(raw.toString());
2980
- } catch {
2981
- return;
2982
- }
2983
- if (msg.id !== void 0) {
2984
- const pending = this.pending.get(msg.id);
2985
- if (!pending) return;
2986
- if (msg.error) {
2987
- pending.reject(new CDPProtocolError(msg.error.code, msg.error.message, pending.method));
2988
- } else {
2989
- pending.resolve(msg.result ?? {});
2990
- }
2991
- return;
2992
- }
2993
- if (msg.method) {
2994
- this.emit(msg.method, msg.params ?? {}, msg.sessionId);
2995
- this.emit("*", msg.method, msg.params ?? {}, msg.sessionId);
2996
- }
2997
- });
2998
- this.ws.on("close", (code, reason) => {
2999
- if (this.closed) return;
3000
- this.closed = true;
3001
- this.closeReason = `WebSocket closed: ${code} ${reason?.toString() ?? ""}`.trim();
3002
- if (this.keepaliveTimer) {
3003
- clearInterval(this.keepaliveTimer);
3004
- this.keepaliveTimer = null;
3005
- }
3006
- for (const [id, pending] of this.pending) {
3007
- clearTimeout(pending.timeout);
3008
- pending.reject(new Error(`Connection closed: ${pending.method}`));
3009
- this.pending.delete(id);
3010
- }
3011
- this.emit("disconnect");
3012
- });
3013
- this.ws.on("error", (err) => {
3014
- if (this.closed) return;
3015
- this.emit("ws-error", err);
3016
- });
3017
- }
3018
- };
3019
- var CDPProtocolError = class extends Error {
3020
- code;
3021
- method;
3022
- data;
3023
- constructor(code, message, method, data) {
3024
- super(`CDP error [${code}] in ${method}: ${message}`);
3025
- this.name = "CDPProtocolError";
3026
- this.code = code;
3027
- this.method = method;
3028
- this.data = data;
3029
- }
3030
- };
3031
-
3032
- // src/cdp-driver/index.ts
3033
- async function launch(options = {}) {
3034
- let wsEndpoint;
3035
- let childProcess;
3036
- let tmpDir;
3037
- if (options.cdpEndpoint) {
3038
- wsEndpoint = await connectToCDP(options.cdpEndpoint);
3039
- } else {
3040
- const result = await launchChrome({
3041
- executablePath: options.executablePath,
3042
- headless: options.headless,
3043
- args: options.args,
3044
- userDataDir: options.userDataDir,
3045
- timeout: options.timeout,
3046
- env: options.env
3047
- });
3048
- wsEndpoint = result.wsEndpoint;
3049
- childProcess = result.process;
3050
- tmpDir = result.tmpDir;
3051
- }
3052
- const conn = new CDPConnection(wsEndpoint);
3053
- await conn.ready();
3054
- const httpEndpoint = options.cdpEndpoint && !options.cdpEndpoint.startsWith("ws") ? options.cdpEndpoint : void 0;
3055
- const browser = new XBBrowserImpl(conn, childProcess, tmpDir, httpEndpoint);
3056
- return { browser, wsEndpoint };
3057
- }
3058
-
3059
- // src/cdp-interceptor/proxy.ts
3060
- import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
3061
-
3062
- // src/cdp-interceptor/rules/shared.ts
3063
- var PLAYWRIGHT_INTERNAL_MARKERS = [
3064
- "__commonJS",
3065
- "module.exports",
3066
- "__require",
3067
- "__toESM",
3068
- "inject_utils"
3069
- ];
3070
- function isPlaywrightInternal(code) {
3071
- return PLAYWRIGHT_INTERNAL_MARKERS.some((marker) => code.includes(marker));
3072
- }
3073
- function extractUserCode(ctx) {
3074
- if (ctx.method === "Runtime.evaluate") {
3075
- const expr = ctx.params.expression;
3076
- if (typeof expr === "string") {
3077
- if (isPlaywrightInternal(expr)) return null;
3078
- return expr;
3079
- }
3080
- }
3081
- if (ctx.method === "Runtime.callFunctionOn") {
3082
- const decl = ctx.params.functionDeclaration;
3083
- if (typeof decl === "string" && decl.includes("utilityScript.evaluate")) {
3084
- return extractAllStrings(ctx.params.arguments);
3085
- }
3086
- if (typeof decl === "string") return decl;
3087
- }
3088
- return null;
3089
- }
3090
- function extractAllStrings(rawArgs) {
3091
- if (!Array.isArray(rawArgs)) return null;
3092
- const strings = [];
3093
- for (const arg of rawArgs) {
3094
- if (arg && typeof arg === "object" && "value" in arg) {
3095
- const val = arg.value;
3096
- if (typeof val === "string" && val.length > 5 && !["true", "false"].includes(val)) {
3097
- strings.push(val);
3340
+ return null;
3341
+ }
3342
+ function extractAllStrings(rawArgs) {
3343
+ if (!Array.isArray(rawArgs)) return null;
3344
+ const strings = [];
3345
+ for (const arg of rawArgs) {
3346
+ if (arg && typeof arg === "object" && "value" in arg) {
3347
+ const val = arg.value;
3348
+ if (typeof val === "string" && val.length > 5 && !["true", "false"].includes(val)) {
3349
+ strings.push(val);
3098
3350
  }
3099
3351
  }
3100
3352
  }
@@ -3104,22 +3356,22 @@ function extractAllStrings(rawArgs) {
3104
3356
  // src/cdp-interceptor/rules/dom-mutation.ts
3105
3357
  var DOM_PATTERNS = [
3106
3358
  // ── P0: Value/Checked (bypasses React onChange) ────────────
3107
- { pattern: /\.value\s*=\s*(?!["'\s]*$)/, name: ".value =", severity: "danger", action: "block", errorCode: -32001, suggestion: "Use page.fill(selector, value) which dispatches proper input/change events." },
3108
- { pattern: /\.checked\s*=\s*(?:true|false)/, name: ".checked =", severity: "danger", action: "block", errorCode: -32001, suggestion: "Use page.check(selector) or page.uncheck(selector)." },
3109
- { pattern: /\.indeterminate\s*=\s*true/, name: ".indeterminate =", severity: "warn", action: "block", errorCode: -32021, suggestion: "No human can set indeterminate state \u2014 remove this call." },
3110
- { pattern: /\.valueAsDate\s*=/, name: ".valueAsDate =", severity: "warn", action: "block", errorCode: -32022, suggestion: "Use page.fill() with formatted date string instead." },
3111
- { pattern: /\.valueAsNumber\s*=/, name: ".valueAsNumber =", severity: "warn", action: "block", errorCode: -32022, suggestion: "Use page.fill() with numeric string instead." },
3359
+ { pattern: /\.value\s*=\s*(?!["'\s]*$)/, name: ".value =", severity: "danger", action: "pass", errorCode: -32001, suggestion: "Use page.fill(selector, value) which dispatches proper input/change events." },
3360
+ { pattern: /\.checked\s*=\s*(?:true|false)/, name: ".checked =", severity: "danger", action: "pass", errorCode: -32001, suggestion: "Use page.check(selector) or page.uncheck(selector)." },
3361
+ { pattern: /\.indeterminate\s*=\s*true/, name: ".indeterminate =", severity: "warn", action: "pass", errorCode: -32021, suggestion: "No human can set indeterminate state \u2014 remove this call." },
3362
+ { pattern: /\.valueAsDate\s*=/, name: ".valueAsDate =", severity: "warn", action: "pass", errorCode: -32022, suggestion: "Use page.fill() with formatted date string instead." },
3363
+ { pattern: /\.valueAsNumber\s*=/, name: ".valueAsNumber =", severity: "warn", action: "pass", errorCode: -32022, suggestion: "Use page.fill() with numeric string instead." },
3112
3364
  // ── P1: Select/Option (bypasses React onChange on select) ─
3113
- { pattern: /\.selectedIndex\s*=\s*\d+/, name: ".selectedIndex =", severity: "danger", action: "block", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
3114
- { pattern: /(?:options|children)\s*\[[^\]]*\]\s*\.\s*selected\s*=\s*(?:true|false)/, name: "options[N].selected =", severity: "danger", action: "block", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
3115
- { pattern: /\.value\s*=\s*["'][^"']*["']\s*[;)]?\s*$/, name: "selectElement.value =", severity: "warn", action: "block", errorCode: -32023, suggestion: "For select elements, use page.selectOption(selector, value)." },
3365
+ { pattern: /\.selectedIndex\s*=\s*\d+/, name: ".selectedIndex =", severity: "danger", action: "pass", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
3366
+ { pattern: /(?:options|children)\s*\[[^\]]*\]\s*\.\s*selected\s*=\s*(?:true|false)/, name: "options[N].selected =", severity: "danger", action: "pass", errorCode: -32003, suggestion: "Use page.selectOption(selector, value)." },
3367
+ { pattern: /\.value\s*=\s*["'][^"']*["']\s*[;)]?\s*$/, name: "selectElement.value =", severity: "warn", action: "pass", errorCode: -32023, suggestion: "For select elements, use page.selectOption(selector, value)." },
3116
3368
  // ── P2: Content properties (bypasses virtual DOM diffing) ──
3117
3369
  { pattern: /\.innerHTML\s*=/, name: ".innerHTML =", severity: "info", action: "pass", errorCode: -32007, suggestion: ".innerHTML bypasses React/Vue diffing. Use component state or page.setContent()." },
3118
3370
  { pattern: /\.outerHTML\s*=/, name: ".outerHTML =", severity: "info", action: "pass", errorCode: -32007, suggestion: "outerHTML replacement destroys React fiber tree. Use page.setContent()." },
3119
- { pattern: /\.innerText\s*=/, name: ".innerText =", severity: "warn", action: "block", errorCode: -32024, suggestion: "Framework components should be updated via state, not innerText." },
3120
- { pattern: /\.textContent\s*=/, name: ".textContent =", severity: "warn", action: "block", errorCode: -32024, suggestion: "textContent bypasses React/Vue diffing. Use component state instead." },
3121
- { pattern: /\.outerText\s*=/, name: ".outerText =", severity: "warn", action: "block", errorCode: -32024, suggestion: "Non-standard property \u2014 use proper framework update methods." },
3122
- { pattern: /nodeValue\s*=/, name: "node.nodeValue =", severity: "info", action: "block", errorCode: -32025, suggestion: "Direct text node mutation. Use textContent instead if needed." },
3371
+ { pattern: /\.innerText\s*=/, name: ".innerText =", severity: "warn", action: "pass", errorCode: -32024, suggestion: "Framework components should be updated via state, not innerText." },
3372
+ { pattern: /\.textContent\s*=/, name: ".textContent =", severity: "warn", action: "pass", errorCode: -32024, suggestion: "textContent bypasses React/Vue diffing. Use component state instead." },
3373
+ { pattern: /\.outerText\s*=/, name: ".outerText =", severity: "warn", action: "pass", errorCode: -32024, suggestion: "Non-standard property \u2014 use proper framework update methods." },
3374
+ { pattern: /nodeValue\s*=/, name: "node.nodeValue =", severity: "info", action: "pass", errorCode: -32025, suggestion: "Direct text node mutation. Use textContent instead if needed." },
3123
3375
  // ── P3: Style properties ──────────────────────────────────
3124
3376
  { pattern: /\.style\s*=\s*["']/, name: ".style = (string override)", severity: "info", action: "pass", errorCode: -32008, suggestion: 'Setting style as a string overwrites CSSStyleDeclaration. Use element.style.prop = "value".' },
3125
3377
  { pattern: /\.style\.cssText\s*=/, name: ".style.cssText =", severity: "info", action: "pass", errorCode: -32008, suggestion: "style.cssText replacement destroys inline styles \u2014 use individual property set." },
@@ -3167,7 +3419,7 @@ var DOM_PATTERNS = [
3167
3419
  { pattern: /\.scrollBy\s*\(/, name: ".scrollBy()", severity: "info", action: "pass", errorCode: -32015, suggestion: "ScrollBy without user gesture." },
3168
3420
  { pattern: /\.scrollIntoView\s*\(/, name: ".scrollIntoView()", severity: "info", action: "pass", errorCode: -32015, suggestion: "scrollIntoView is a common bot pattern. Let Playwright handle scrolling." },
3169
3421
  // ── P11: Shadow DOM ───────────────────────────────────────
3170
- { pattern: /\.shadowRoot\s*=/, name: ".shadowRoot = (override)", severity: "info", action: "block", errorCode: -32036, suggestion: "ShadowRoot is read-only \u2014 this set attempt is detectable." },
3422
+ { pattern: /\.shadowRoot\s*=/, name: ".shadowRoot = (override)", severity: "info", action: "pass", errorCode: -32036, suggestion: "ShadowRoot is read-only \u2014 this set attempt is detectable." },
3171
3423
  // ── P12: Force reflow / layout thrashing ──────────────────
3172
3424
  { pattern: /\.offsetHeight\b(?!\s*===?\s*)/, name: ".offsetHeight read (forced reflow)", severity: "warn", action: "pass", errorCode: -32016, suggestion: "Reading offsetHeight triggers forced reflow \u2014 anti-crawlers detect this as layout probing." },
3173
3425
  { pattern: /\.offsetWidth\b(?!\s*===?\s*)/, name: ".offsetWidth read (forced reflow)", severity: "warn", action: "pass", errorCode: -32016, suggestion: "Reading offsetWidth triggers forced reflow \u2014 detectable." },
@@ -3280,7 +3532,7 @@ function analyzeTrajectory(samples) {
3280
3532
  const stopY = samples[samples.length - 1].y;
3281
3533
  return {
3282
3534
  ruleId: "mouse-trajectory",
3283
- action: "block",
3535
+ action: "pass",
3284
3536
  severity: "danger",
3285
3537
  reason: `Suspicious mouse trajectory: ${issues.join("; ")}`,
3286
3538
  suggestion: `This mouse movement appears automated (straight line A\u2192B, no natural variation).
@@ -3422,7 +3674,7 @@ function analyzeKeyTiming(samples) {
3422
3674
  if (allIdentical) {
3423
3675
  return {
3424
3676
  ruleId: "input-keystroke",
3425
- action: "block",
3677
+ action: "pass",
3426
3678
  severity: "danger",
3427
3679
  reason: `All ${intervals.length} keystroke intervals are exactly ${intervals[0]}ms \u2014 impossible for human typing.`,
3428
3680
  suggestion: `Use page.fill(selector, text) instead of page.type() with delay.
@@ -3434,7 +3686,7 @@ Or add random variation: page.type(selector, text, {delay: 50 + Math.random() *
3434
3686
  if (cv < 0.08) {
3435
3687
  return {
3436
3688
  ruleId: "input-keystroke",
3437
- action: "block",
3689
+ action: "pass",
3438
3690
  severity: "warn",
3439
3691
  reason: `Unnatural keystroke timing (CV=${cv.toFixed(3)}). Human typing has CV > 0.2 on average.`,
3440
3692
  suggestion: `Add random variation to your typing delay: page.type(selector, text, {delay: 50 + Math.random() * 80}).`,
@@ -3530,7 +3782,7 @@ function makeDecision(p, context) {
3530
3782
  const suggestion = p.suggestionOverride ?? `Detected: "${p.name}" \u2014 an automation tool marker that anti-crawler systems immediately flag. Remove this pattern from your code.`;
3531
3783
  return {
3532
3784
  ruleId: "automation-signals",
3533
- action: "block",
3785
+ action: "pass",
3534
3786
  severity: p.severity,
3535
3787
  reason: `Automation marker detected: "${p.name}". Context: "${context}..."`,
3536
3788
  suggestion,
@@ -3605,7 +3857,7 @@ var fingerprintingRule = {
3605
3857
  if (p.pattern.test(userCode)) {
3606
3858
  return {
3607
3859
  ruleId: "fingerprinting",
3608
- action: "block",
3860
+ action: "pass",
3609
3861
  severity: p.severity,
3610
3862
  reason: `Browser fingerprinting API accessed: "${p.name}". Anti-crawler systems use this to identify your browser.`,
3611
3863
  suggestion: p.suggestion,
@@ -3622,7 +3874,7 @@ var fingerprintingRule = {
3622
3874
  var EVENT_PATTERNS = [
3623
3875
  // ── Direct method calls (all isTrusted=false) ─────────
3624
3876
  { pattern: /\.click\s*\(\s*\)/, name: "el.click()", severity: "danger", errorCode: -32070, suggestion: "el.click() fires isTrusted=false events. Use page.click(selector) which uses Input.dispatchMouseEvent (isTrusted=true)." },
3625
- { pattern: /\.focus\s*\(\s*\)/, name: "el.focus()", severity: "danger", errorCode: -32071, suggestion: "el.focus() without user interaction is detectable. Use page.click(selector) which naturally focuses." },
3877
+ { pattern: /\.focus\s*\(\s*\)/, name: "el.focus()", severity: "warn", errorCode: -32071, suggestion: "el.focus() without user interaction is detectable. Use page.click(selector) which naturally focuses." },
3626
3878
  { pattern: /\.blur\s*\(\s*\)/, name: "el.blur()", severity: "danger", errorCode: -32071, suggestion: "el.blur() without user interaction. Avoid in automation scripts." },
3627
3879
  { pattern: /\.submit\s*\(\s*\)/, name: "el.submit()", severity: "danger", errorCode: -32072, suggestion: `form.submit() bypasses onSubmit handlers. Click the submit button: page.click('button[type="submit"]').` },
3628
3880
  { pattern: /\.reset\s*\(\s*\)/, name: "el.reset()", severity: "danger", errorCode: -32072, suggestion: "form.reset() without user action. Let the user clear fields manually." },
@@ -3637,7 +3889,7 @@ var EVENT_PATTERNS = [
3637
3889
  { pattern: /\.showPicker\s*\(\s*\)/, name: "HTMLInputElement.showPicker()", severity: "info", errorCode: -32076, suggestion: "Date/color picker shown without click \u2014 detectable." },
3638
3890
  { pattern: /\.reportValidity\s*\(\s*\)/, name: "el.reportValidity()", severity: "info", errorCode: -32076, suggestion: "Validity reporting without form submission attempt." },
3639
3891
  // ── dispatchEvent with synthetic events (isTrusted=false) ──
3640
- { pattern: /dispatchEvent\s*\(\s*new\s+(?:Event|CustomEvent)\s*\(/, name: "dispatchEvent(new Event/CustomEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic events have isTrusted=false. Use Input.dispatch* CDP methods for trusted events." },
3892
+ { pattern: /dispatchEvent\s*\(\s*new\s+(?:Event|CustomEvent)\s*\(/, name: "dispatchEvent(new Event/CustomEvent)", severity: "warn", errorCode: -32077, suggestion: "Synthetic events have isTrusted=false. Use Input.dispatch* CDP methods for trusted events." },
3641
3893
  { pattern: /dispatchEvent\s*\(\s*new\s+MouseEvent\s*\(/, name: "dispatchEvent(new MouseEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic mouse events (isTrusted=false). Use Input.dispatchMouseEvent CDP method." },
3642
3894
  { pattern: /dispatchEvent\s*\(\s*new\s+KeyboardEvent\s*\(/, name: "dispatchEvent(new KeyboardEvent)", severity: "danger", errorCode: -32077, suggestion: "Synthetic keyboard events (isTrusted=false). Use Input.dispatchKeyEvent CDP method." },
3643
3895
  { pattern: /dispatchEvent\s*\(\s*new\s+FocusEvent\s*\(/, name: "dispatchEvent(new FocusEvent)", severity: "warn", errorCode: -32078, suggestion: "Synthetic focus events bypass user interaction." },
@@ -3666,7 +3918,9 @@ var eventSimulationRule = {
3666
3918
  if (p.pattern.test(userCode)) {
3667
3919
  return {
3668
3920
  ruleId: "event-simulation",
3669
- action: "block",
3921
+ // danger 级硬拦(el.click 等 100% 暴露的模式);
3922
+ // warning 级放行+提示(dispatchEvent 在部分合法场景如 DataTransfer paste 使用)
3923
+ action: p.severity === "danger" ? "block" : "pass",
3670
3924
  severity: p.severity,
3671
3925
  reason: `Event simulation detected: "${p.name}". Synthetic events have isTrusted=false and are 100% detectable.`,
3672
3926
  suggestion: p.suggestion,
@@ -3725,7 +3979,7 @@ var emulationOverrideRule = {
3725
3979
  if (ctx.method !== p.method) continue;
3726
3980
  return {
3727
3981
  ruleId: "emulation-override",
3728
- action: "block",
3982
+ action: "pass",
3729
3983
  severity: p.severity,
3730
3984
  reason: `CDP emulation/override detected: "${p.name}". This creates detectable inconsistencies between the JS environment and real browser state.`,
3731
3985
  suggestion: p.suggestion,
@@ -3755,7 +4009,7 @@ var networkAnomalyRule = {
3755
4009
  if (!headerStr.includes("sec-ch-ua")) {
3756
4010
  return {
3757
4011
  ruleId: "network-anomaly",
3758
- action: "block",
4012
+ action: "pass",
3759
4013
  severity: "warn",
3760
4014
  reason: "Network.setExtraHTTPHeaders called without Sec-CH-UA client hints \u2014 browser normally sends these.",
3761
4015
  suggestion: "Modern browsers send Sec-CH-UA headers automatically. Adding custom headers without them creates detectable inconsistency.",
@@ -3769,7 +4023,7 @@ var networkAnomalyRule = {
3769
4023
  case "Network.clearBrowserCache": {
3770
4024
  return {
3771
4025
  ruleId: "network-anomaly",
3772
- action: "block",
4026
+ action: "pass",
3773
4027
  severity: "warn",
3774
4028
  reason: "Network.clearBrowserCache called \u2014 cache clearing mid-session is unnatural for real users.",
3775
4029
  suggestion: "Avoid cache clearing during sessions. Start with a fresh profile if needed.",
@@ -3780,7 +4034,7 @@ var networkAnomalyRule = {
3780
4034
  case "Network.clearBrowserCookies": {
3781
4035
  return {
3782
4036
  ruleId: "network-anomaly",
3783
- action: "block",
4037
+ action: "pass",
3784
4038
  severity: "warn",
3785
4039
  reason: "Network.clearBrowserCookies called \u2014 wiping cookies mid-session is a scraper optimization.",
3786
4040
  suggestion: "Cookies should only be cleared via normal browser flow (expiration, user action).",
@@ -3791,7 +4045,7 @@ var networkAnomalyRule = {
3791
4045
  case "Network.setBlockedURLs": {
3792
4046
  return {
3793
4047
  ruleId: "network-anomaly",
3794
- action: "block",
4048
+ action: "pass",
3795
4049
  severity: "warn",
3796
4050
  reason: "Network.setBlockedURLs blocks resource loading \u2014 this changes the page behavior and is detectable.",
3797
4051
  suggestion: "Blocking images/fonts/etc creates measurable differences in performance and page rendering.",
@@ -3813,7 +4067,7 @@ var networkAnomalyRule = {
3813
4067
  case "Fetch.enable": {
3814
4068
  return {
3815
4069
  ruleId: "network-anomaly",
3816
- action: "block",
4070
+ action: "pass",
3817
4071
  severity: "warn",
3818
4072
  reason: "Fetch.enable intercepts all network requests \u2014 a man-in-the-middle approach used by scrapers.",
3819
4073
  suggestion: "Do not use Fetch domain for network interception if avoiding detection.",
@@ -3853,7 +4107,7 @@ var pageLifecycleRule = {
3853
4107
  if (interval < 100) {
3854
4108
  return {
3855
4109
  ruleId: "page-lifecycle",
3856
- action: "block",
4110
+ action: "pass",
3857
4111
  severity: "warn",
3858
4112
  reason: `Multiple Page.navigate calls within ${interval}ms of each other \u2014 unnatural rapid navigation.`,
3859
4113
  suggestion: "Add proper waits between navigations: wait for page load before navigating again.",
@@ -3870,7 +4124,7 @@ var pageLifecycleRule = {
3870
4124
  if (state.lastNavTime > 0 && now - state.lastNavTime < 500) {
3871
4125
  return {
3872
4126
  ruleId: "page-lifecycle",
3873
- action: "block",
4127
+ action: "pass",
3874
4128
  severity: "danger",
3875
4129
  reason: "Page.captureScreenshot called within 500ms of navigation \u2014 content extraction pattern.",
3876
4130
  suggestion: "Wait for the page to fully render before taking screenshots: wait for load/networkidle.",
@@ -3881,7 +4135,7 @@ var pageLifecycleRule = {
3881
4135
  if (state.screenshots > 3 && state.navigations.length < 2) {
3882
4136
  return {
3883
4137
  ruleId: "page-lifecycle",
3884
- action: "block",
4138
+ action: "pass",
3885
4139
  severity: "warn",
3886
4140
  reason: "Multiple screenshots on a single page without navigation \u2014 suspicious extraction behavior.",
3887
4141
  suggestion: "Consider if all screenshots are necessary.",
@@ -3894,7 +4148,7 @@ var pageLifecycleRule = {
3894
4148
  case "Page.printToPDF": {
3895
4149
  return {
3896
4150
  ruleId: "page-lifecycle",
3897
- action: "block",
4151
+ action: "pass",
3898
4152
  severity: "danger",
3899
4153
  reason: "Page.printToPDF called \u2014 this is a telltale scraper pattern that gives away automation intent.",
3900
4154
  suggestion: "Avoid PDF generation. If you must, add significant delays and user-like interaction first.",
@@ -3906,7 +4160,7 @@ var pageLifecycleRule = {
3906
4160
  if (state.lastNavTime > 0 && now - state.lastNavTime < 1e3) {
3907
4161
  return {
3908
4162
  ruleId: "page-lifecycle",
3909
- action: "block",
4163
+ action: "pass",
3910
4164
  severity: "warn",
3911
4165
  reason: "Page.reload called immediately after navigate \u2014 unnatural fast-reload pattern.",
3912
4166
  suggestion: "Introduce delays between navigation and reload to simulate human behavior.",
@@ -3920,7 +4174,7 @@ var pageLifecycleRule = {
3920
4174
  if (state.navigations.length < 2) {
3921
4175
  return {
3922
4176
  ruleId: "page-lifecycle",
3923
- action: "block",
4177
+ action: "pass",
3924
4178
  severity: "info",
3925
4179
  reason: "Page.close called after minimal interaction \u2014 zombie pages common in automation.",
3926
4180
  suggestion: "Ensure meaningful interaction before closing pages.",
@@ -3936,7 +4190,7 @@ var pageLifecycleRule = {
3936
4190
  if (state.evaluateCount > 50 && state.navigations.length === 0) {
3937
4191
  return {
3938
4192
  ruleId: "page-lifecycle",
3939
- action: "block",
4193
+ action: "pass",
3940
4194
  severity: "info",
3941
4195
  reason: "50+ evaluate calls without any Page.navigate \u2014 data extraction without real browsing.",
3942
4196
  suggestion: "Navigate to a real page first. Evaluate on about:blank is suspicious.",
@@ -3998,6 +4252,323 @@ function createRuleEngine(customRules) {
3998
4252
  };
3999
4253
  }
4000
4254
 
4255
+ // src/cdp-driver/connection.ts
4256
+ var CDPConnection = class _CDPConnection extends EventEmitter4 {
4257
+ ws;
4258
+ nextId = 1;
4259
+ /** CDP Guard:全部出站命令过规则引擎(env XBROWSER_CDP_GUARD=off 关闭)。
4260
+ * 173 条规则 / 9 模块:合成事件 block、指纹暴露警告、自动化信号拦截。 */
4261
+ static __guard;
4262
+ get guard() {
4263
+ if (_CDPConnection.__guard === void 0) {
4264
+ if (process.env.XBROWSER_CDP_GUARD === "off") {
4265
+ _CDPConnection.__guard = null;
4266
+ } else {
4267
+ try {
4268
+ const g = createRuleEngine();
4269
+ g.start();
4270
+ _CDPConnection.__guard = g;
4271
+ } catch {
4272
+ _CDPConnection.__guard = null;
4273
+ }
4274
+ }
4275
+ }
4276
+ return _CDPConnection.__guard;
4277
+ }
4278
+ pending = /* @__PURE__ */ new Map();
4279
+ closed = false;
4280
+ closeReason = null;
4281
+ /** Default session ID for flat session protocol (Target.attachToTarget) */
4282
+ defaultSessionId;
4283
+ constructor(wsOrUrl, sessionId) {
4284
+ super();
4285
+ this.setMaxListeners(0);
4286
+ this.defaultSessionId = sessionId;
4287
+ if (typeof wsOrUrl === "string") {
4288
+ const wsOptions = /^wss:\/\/\d+\.\d+\.\d+\.\d+/.test(wsOrUrl) ? { rejectUnauthorized: false } : void 0;
4289
+ this.ws = new WebSocket(wsOrUrl, wsOptions);
4290
+ } else {
4291
+ this.ws = wsOrUrl;
4292
+ }
4293
+ this.bindWebSocket();
4294
+ this.startKeepalive();
4295
+ }
4296
+ /** Send periodic WS pings to prevent idle-timeout disconnects (e.g. CF's 100s).
4297
+ * Also detects dead connections: if a pong isn't received within 10s of a
4298
+ * ping, the connection is considered dead and forcibly closed. */
4299
+ keepaliveTimer = null;
4300
+ pongTimer = null;
4301
+ /** 最近一次收到任何 WS 消息的时间——有数据流动即证明连接存活 */
4302
+ lastIncomingAt = Date.now();
4303
+ startKeepalive() {
4304
+ this.ws.on("pong", () => {
4305
+ if (this.pongTimer) {
4306
+ clearTimeout(this.pongTimer);
4307
+ this.pongTimer = null;
4308
+ }
4309
+ });
4310
+ this.keepaliveTimer = setInterval(() => {
4311
+ if (this.ws.readyState === WebSocket.OPEN) {
4312
+ if (!this.pongTimer) {
4313
+ this.pongTimer = setTimeout(() => {
4314
+ const recentlyActive = Date.now() - this.lastIncomingAt < 3e4;
4315
+ if (recentlyActive) {
4316
+ this.pongTimer = null;
4317
+ return;
4318
+ }
4319
+ if (!this.closed) {
4320
+ this.closed = true;
4321
+ this.closeReason = "keepalive timeout (no pong and idle >30s)";
4322
+ try {
4323
+ this.ws.terminate();
4324
+ } catch {
4325
+ }
4326
+ for (const [, pending] of this.pending) {
4327
+ clearTimeout(pending.timeout);
4328
+ pending.reject(new Error("Connection dead: keepalive timeout"));
4329
+ }
4330
+ this.pending.clear();
4331
+ this.emit("disconnect");
4332
+ }
4333
+ }, 6e4);
4334
+ }
4335
+ this.ws.ping?.();
4336
+ } else if (this.closed) {
4337
+ if (this.keepaliveTimer) clearInterval(this.keepaliveTimer);
4338
+ this.keepaliveTimer = null;
4339
+ }
4340
+ }, 3e4);
4341
+ }
4342
+ /** Wait for the connection to be fully open */
4343
+ async ready() {
4344
+ if (this.ws.readyState === WebSocket.OPEN) return;
4345
+ if (this.ws.readyState === WebSocket.CLOSED || this.ws.readyState === WebSocket.CLOSING) {
4346
+ throw new Error(`WebSocket already closed: ${this.closeReason ?? "unknown"}`);
4347
+ }
4348
+ return new Promise((resolve, reject) => {
4349
+ const onOpen = () => {
4350
+ this.ws.off("error", onError);
4351
+ resolve();
4352
+ };
4353
+ const onError = (err) => {
4354
+ this.ws.off("open", onOpen);
4355
+ reject(err);
4356
+ };
4357
+ this.ws.once("open", onOpen);
4358
+ this.ws.once("error", onError);
4359
+ });
4360
+ }
4361
+ /** Is the underlying WebSocket alive? */
4362
+ get isOpen() {
4363
+ return !this.closed && this.ws.readyState === WebSocket.OPEN;
4364
+ }
4365
+ /**
4366
+ * Send a CDP command and await its response.
4367
+ *
4368
+ * @param method — CDP domain.method (e.g. "Page.navigate")
4369
+ * @param params — method parameters
4370
+ * @param sessionId — optional flat session ID for sub-targets
4371
+ * @param timeoutMs — response timeout (default: 30s;重 SPA 加载期间主线程忙,
4372
+ * evaluate 可排队 >30s——Runtime.evaluate 单独放宽到 90s)
4373
+ * @returns the `result` field from the CDP response
4374
+ */
4375
+ async send(method, params, sessionId, timeoutMs) {
4376
+ const effectiveTimeout = timeoutMs ?? (method === "Runtime.evaluate" ? 9e4 : 3e4);
4377
+ if (this.guard) {
4378
+ const decision = this.guard.evaluate({
4379
+ method,
4380
+ params: params ?? {},
4381
+ sessionId: sessionId ?? this.defaultSessionId ?? "",
4382
+ direction: "client\u2192browser"
4383
+ });
4384
+ if (decision && decision.action === "block") {
4385
+ throw new Error(`[CDP-Guard] ${decision.reason}${decision.suggestion ? " | \u66FF\u4EE3: " + decision.suggestion : ""} [${decision.ruleId}]`);
4386
+ }
4387
+ }
4388
+ if (this.closed) {
4389
+ throw new Error(`CDP connection closed: ${this.closeReason ?? "unknown"}`);
4390
+ }
4391
+ if (!this.isOpen) {
4392
+ throw new Error(`CDP connection not open (state: ${this.ws.readyState})`);
4393
+ }
4394
+ const id = this.nextId++;
4395
+ const sid = sessionId ?? this.defaultSessionId;
4396
+ const message = { id, method };
4397
+ if (params !== void 0) message.params = params;
4398
+ if (sid !== void 0) message.sessionId = sid;
4399
+ return new Promise((resolve, reject) => {
4400
+ const timeout = setTimeout(() => {
4401
+ this.pending.delete(id);
4402
+ reject(new Error(`CDP timeout: ${method} (${effectiveTimeout}ms)`));
4403
+ }, effectiveTimeout);
4404
+ this.pending.set(id, {
4405
+ resolve: (v) => {
4406
+ clearTimeout(timeout);
4407
+ this.pending.delete(id);
4408
+ resolve(v);
4409
+ },
4410
+ reject: (err) => {
4411
+ clearTimeout(timeout);
4412
+ this.pending.delete(id);
4413
+ reject(err);
4414
+ },
4415
+ method,
4416
+ timeout
4417
+ });
4418
+ const data = JSON.stringify(message);
4419
+ try {
4420
+ this.ws.send(data);
4421
+ } catch (err) {
4422
+ clearTimeout(timeout);
4423
+ this.pending.delete(id);
4424
+ reject(new Error(`CDP send failed: ${method} \u2014 ${err instanceof Error ? err.message : String(err)}`));
4425
+ }
4426
+ });
4427
+ }
4428
+ /**
4429
+ * Subscribe to a CDP event.
4430
+ *
4431
+ * @param event — full event name (e.g. "Page.frameNavigated")
4432
+ * @param handler — called with the event params
4433
+ * @param sessionId — optional session filter
4434
+ */
4435
+ on(event, handler) {
4436
+ return super.on(event, handler);
4437
+ }
4438
+ once(event, handler) {
4439
+ return super.once(event, handler);
4440
+ }
4441
+ /** Remove an event listener */
4442
+ off(event, handler) {
4443
+ super.off(event, handler);
4444
+ return this;
4445
+ }
4446
+ /**
4447
+ * Subscribe to a CDP event for a specific session.
4448
+ * Returns an unsubscribe function.
4449
+ */
4450
+ subscribe(event, sessionId, handler) {
4451
+ const wrapper = (params, sid) => {
4452
+ if (sid === sessionId || !sessionId && !sid) handler(params);
4453
+ };
4454
+ this.on(event, wrapper);
4455
+ return () => this.off(event, wrapper);
4456
+ }
4457
+ /** Close the WebSocket */
4458
+ async close() {
4459
+ if (this.closed) return;
4460
+ this.closed = true;
4461
+ this.closeReason = "closed by caller";
4462
+ if (this.keepaliveTimer) {
4463
+ clearInterval(this.keepaliveTimer);
4464
+ this.keepaliveTimer = null;
4465
+ }
4466
+ if (this.pongTimer) {
4467
+ clearTimeout(this.pongTimer);
4468
+ this.pongTimer = null;
4469
+ }
4470
+ for (const [id, pending] of this.pending) {
4471
+ clearTimeout(pending.timeout);
4472
+ pending.reject(new Error(`Connection closed: ${pending.method}`));
4473
+ this.pending.delete(id);
4474
+ }
4475
+ if (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING) {
4476
+ this.ws.close(1e3, "normal closure");
4477
+ }
4478
+ }
4479
+ /** Set the default session ID for flat protocol */
4480
+ setDefaultSessionId(sid) {
4481
+ this.defaultSessionId = sid;
4482
+ }
4483
+ // ── Private ─────────────────────────────────────────────────
4484
+ bindWebSocket() {
4485
+ this.ws.on("message", (raw) => {
4486
+ this.lastIncomingAt = Date.now();
4487
+ let msg;
4488
+ try {
4489
+ msg = JSON.parse(raw.toString());
4490
+ } catch {
4491
+ return;
4492
+ }
4493
+ if (msg.id !== void 0) {
4494
+ const pending = this.pending.get(msg.id);
4495
+ if (!pending) return;
4496
+ if (msg.error) {
4497
+ pending.reject(new CDPProtocolError(msg.error.code, msg.error.message, pending.method));
4498
+ } else {
4499
+ pending.resolve(msg.result ?? {});
4500
+ }
4501
+ return;
4502
+ }
4503
+ if (msg.method) {
4504
+ this.emit(msg.method, msg.params ?? {}, msg.sessionId);
4505
+ this.emit("*", msg.method, msg.params ?? {}, msg.sessionId);
4506
+ }
4507
+ });
4508
+ this.ws.on("close", (code, reason) => {
4509
+ if (this.closed) return;
4510
+ this.closed = true;
4511
+ this.closeReason = `WebSocket closed: ${code} ${reason?.toString() ?? ""}`.trim();
4512
+ if (this.keepaliveTimer) {
4513
+ clearInterval(this.keepaliveTimer);
4514
+ this.keepaliveTimer = null;
4515
+ }
4516
+ for (const [id, pending] of this.pending) {
4517
+ clearTimeout(pending.timeout);
4518
+ pending.reject(new Error(`Connection closed: ${pending.method}`));
4519
+ this.pending.delete(id);
4520
+ }
4521
+ this.emit("disconnect");
4522
+ });
4523
+ this.ws.on("error", (err) => {
4524
+ if (this.closed) return;
4525
+ this.emit("ws-error", err);
4526
+ });
4527
+ }
4528
+ };
4529
+ var CDPProtocolError = class extends Error {
4530
+ code;
4531
+ method;
4532
+ data;
4533
+ constructor(code, message, method, data) {
4534
+ super(`CDP error [${code}] in ${method}: ${message}`);
4535
+ this.name = "CDPProtocolError";
4536
+ this.code = code;
4537
+ this.method = method;
4538
+ this.data = data;
4539
+ }
4540
+ };
4541
+
4542
+ // src/cdp-driver/index.ts
4543
+ async function launch(options = {}) {
4544
+ let wsEndpoint;
4545
+ let childProcess;
4546
+ let tmpDir;
4547
+ if (options.cdpEndpoint) {
4548
+ wsEndpoint = await connectToCDP(options.cdpEndpoint);
4549
+ } else {
4550
+ const result = await launchChrome({
4551
+ executablePath: options.executablePath,
4552
+ headless: options.headless,
4553
+ args: options.args,
4554
+ userDataDir: options.userDataDir,
4555
+ timeout: options.timeout,
4556
+ env: options.env
4557
+ });
4558
+ wsEndpoint = result.wsEndpoint;
4559
+ childProcess = result.process;
4560
+ tmpDir = result.tmpDir;
4561
+ }
4562
+ const conn = new CDPConnection(wsEndpoint);
4563
+ await conn.ready();
4564
+ const httpEndpoint = options.cdpEndpoint && !options.cdpEndpoint.startsWith("ws") ? options.cdpEndpoint : void 0;
4565
+ const browser = new XBBrowserImpl(conn, childProcess, tmpDir, httpEndpoint);
4566
+ return { browser, wsEndpoint };
4567
+ }
4568
+
4569
+ // src/cdp-interceptor/proxy.ts
4570
+ import { WebSocketServer, WebSocket as WebSocket2 } from "ws";
4571
+
4001
4572
  // src/cdp-interceptor/logger.ts
4002
4573
  function createLogger(config) {
4003
4574
  const buffer = [];
@@ -4685,15 +5256,18 @@ function deleteSessionDiskMeta(name) {
4685
5256
  async function isSessionPageAlive(session) {
4686
5257
  const page = session.page;
4687
5258
  if (!page || typeof page.evaluate !== "function") return false;
4688
- try {
4689
- await Promise.race([
4690
- page.evaluate("1"),
4691
- new Promise((_, reject) => setTimeout(() => reject(new Error("liveness probe timeout")), 1500))
4692
- ]);
4693
- return true;
4694
- } catch {
4695
- return false;
5259
+ for (let attempt = 0; attempt < 3; attempt++) {
5260
+ try {
5261
+ await Promise.race([
5262
+ page.evaluate("1"),
5263
+ new Promise((_, reject) => setTimeout(() => reject(new Error("liveness probe timeout")), 1500))
5264
+ ]);
5265
+ return true;
5266
+ } catch {
5267
+ await new Promise((r) => setTimeout(r, 400));
5268
+ }
4696
5269
  }
5270
+ return false;
4697
5271
  }
4698
5272
  async function findOrRestoreSession(name, cdpEndpoint) {
4699
5273
  const inMem = findSession(name);
@@ -5116,7 +5690,7 @@ async function closeSessionByName(name) {
5116
5690
  } catch {
5117
5691
  }
5118
5692
  try {
5119
- const { SessionRecorder } = await import("./session-recorder-QRZMKFVL.js");
5693
+ const { SessionRecorder } = await import("./session-recorder-SLDBENVF.js");
5120
5694
  SessionRecorder.cleanup(session.name);
5121
5695
  } catch {
5122
5696
  }