@xbrowser/cli 1.19.0 → 1.21.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.
@@ -24,6 +24,25 @@ import { EventEmitter } from "events";
24
24
 
25
25
  // src/cdp-driver/stealth.ts
26
26
  var DEFAULT_STEALTH_CONFIG = {
27
+ // S191: macOS arm64 + Chrome 151 真实采样(来源:登录桥只读通道)
28
+ uaChProfile: {
29
+ brands: [
30
+ { brand: "Not=A?Brand", version: "99" },
31
+ { brand: "Google Chrome", version: "151" },
32
+ { brand: "Chromium", version: "151" }
33
+ ],
34
+ platform: "macOS",
35
+ platformVersion: "26.2.0",
36
+ architecture: "arm",
37
+ bitness: "64",
38
+ model: "",
39
+ uaFullVersion: "151.0.7922.175",
40
+ fullVersionList: [
41
+ { brand: "Not=A?Brand", version: "99.0.0.0" },
42
+ { brand: "Google Chrome", version: "151.0.7922.175" },
43
+ { brand: "Chromium", version: "151.0.7922.175" }
44
+ ]
45
+ },
27
46
  bezierCurvature: [0.35, 0.6],
28
47
  noiseAmplitude: 5.5,
29
48
  overshootRange: [6, 14],
@@ -130,10 +149,13 @@ Object.assign(KEY_MAP, {
130
149
  "@": { key: "@", code: "Digit2", vk: 50, shift: true },
131
150
  "_": { key: "_", code: "Minus", vk: 189, shift: true }
132
151
  });
133
- function buildStealthInitScript() {
152
+ function buildStealthInitScript(config = DEFAULT_STEALTH_CONFIG) {
153
+ const prof = config.uaChProfile;
154
+ const profJson = prof ? JSON.stringify(prof) : "null";
134
155
  return [
135
156
  "(function(){",
136
157
  ' window.__xbStealthVer="57";',
158
+ " window.__xbUaChProf=" + profJson + ";",
137
159
  // 1. AEL event proxy
138
160
  " var o=EventTarget.prototype.addEventListener;",
139
161
  " var fc=new InputDeviceCapabilities({firesTouchEvents:false});",
@@ -196,9 +218,65 @@ function buildStealthInitScript() {
196
218
  ' Object.defineProperty(Screen.prototype,"height",{get:_gh,configurable:true});',
197
219
  ' Object.defineProperty(Screen.prototype,"availWidth",{get:_gw,configurable:true});',
198
220
  ' Object.defineProperty(Screen.prototype,"availHeight",{get:_gah,configurable:true});',
221
+ // S174: UA Headless 标记清洗——headless launch 的 UA 含 HeadlessChrome/xxx
222
+ // (最高频检测面),同步清洗 userAgent/appVersion/userAgentData(brands+高熵值)。
223
+ // 原型+实例双层覆写(S172 模式)。
224
+ " try{",
225
+ ' var fixUA=function(ua){return String(ua).replace("HeadlessChrome","Chrome");};',
226
+ ' var fixBrands=function(list){return (list||[]).map(function(b){return (b.brand&&b.brand.indexOf("Headless")!==-1)?Object.assign({},b,{brand:b.brand.replace("HeadlessChrome","Chrome")}):b;});};',
227
+ ' var _nuad=Object.getOwnPropertyDescriptor(Navigator.prototype,"userAgentData");',
228
+ " var wrapUAD=function(uad){",
229
+ " if(!uad||uad.__xbUA)return uad;",
230
+ " try{",
231
+ ' Object.defineProperty(uad,"brands",{get:function(){return fixBrands(_nuad.get.call(navigator).brands);},configurable:true});',
232
+ " var _ghev=uad.getHighEntropyValues.bind(uad);",
233
+ " uad.getHighEntropyValues=function(hints){return _ghev(hints).then(function(v){",
234
+ ' var o={};for(var k in v){o[k]=(typeof v[k]==="string")?fixUA(v[k]):(Array.isArray(v[k])?fixBrands(v[k]):v[k]);}',
235
+ " return o;});};",
236
+ " uad.__xbUA=true;",
237
+ " }catch(e){}",
238
+ " return uad;",
239
+ " };",
240
+ ' var _nua=Object.getOwnPropertyDescriptor(Navigator.prototype,"userAgent");',
241
+ " if(_nua&&_nua.get){",
242
+ " var _nuag=function(){return fixUA(_nua.get.call(this));};",
243
+ ' _nuag.toString=function(){return "function get userAgent() { [native code] }";};',
244
+ ' Object.defineProperty(Navigator.prototype,"userAgent",{get:_nuag,configurable:true});',
245
+ " }",
246
+ ' Object.defineProperty(navigator,"userAgent",{get:function(){return fixUA(_nua?_nua.get.call(this):"");},configurable:true});',
247
+ " if(_nuad&&_nuad.get){",
248
+ " var _nuadg=function(){return wrapUAD(_nuad.get.call(this));};",
249
+ ' _nuadg.toString=function(){return "function get userAgentData() { [native code] }";};',
250
+ ' Object.defineProperty(Navigator.prototype,"userAgentData",{get:_nuadg,configurable:true});',
251
+ " }",
252
+ // S190: userAgentData 垫片——headless Chrome 151 不暴露 UA-CH API,
253
+ // 缺失本身即 headless 指纹。从 UA 派生 brands/platform 垫片 + getHighEntropyValues。
254
+ " if(!navigator.userAgentData && window.__xbUaChProf){",
255
+ " var prof = window.__xbUaChProf;",
256
+ " var uad={",
257
+ ' brands:prof.brands.map(function(b){return {brand:b.brand,version:b.version.split(".")[0]}}),',
258
+ " mobile:false,",
259
+ " platform:prof.platform,",
260
+ " getHighEntropyValues:function(hints){",
261
+ " return Promise.resolve().then(function(){",
262
+ " var all={brands:uad.brands,mobile:false,platform:prof.platform,",
263
+ " platformVersion:prof.platformVersion,architecture:prof.architecture,bitness:prof.bitness,model:prof.model,",
264
+ " uaFullVersion:prof.uaFullVersion,",
265
+ " fullVersionList:prof.fullVersionList};",
266
+ " var out={};",
267
+ " (hints||[]).forEach(function(h){if(h in all)out[h]=all[h]});",
268
+ " return out;",
269
+ " });",
270
+ " },",
271
+ " toJSON:function(){return{brands:uad.brands,mobile:false,platform:prof.platform}}",
272
+ " };",
273
+ ' Object.defineProperty(navigator,"userAgentData",{get:function(){return uad},configurable:true});',
274
+ " }",
275
+ " }catch(e){}",
199
276
  // hasFocus 与 visibilityState 联动(d29):恒 true 在 hidden 标签下暴露
200
277
  // —— 真实浏览器失焦/后台时 hasFocus=false
201
- ' document.hasFocus=function(){return document.visibilityState==="visible";};',
278
+ // S172: 原型层覆写——实例赋值可被 Document.prototype.hasFocus.call(document) 逃逸
279
+ ' Document.prototype.hasFocus=function(){return document.visibilityState==="visible";};',
202
280
  // 4. Canvas/WebGL fingerprint: per-session stable noise (d20).
203
281
  // Headless software raster differs subtly from Chrome GPU raster —
204
282
  // toDataURL hashes fingerprint the rasterizer. Inject a stable
@@ -261,26 +339,25 @@ function buildStealthInitScript() {
261
339
  " }catch(e){}",
262
340
  // 3b. Font metrics + speechSynthesis + battery (d22): headless tells
263
341
  " try{",
264
- " var _mt=CanvasRenderingContext2D.prototype.measureText;",
265
342
  ' var _tmw=Object.getOwnPropertyDescriptor(TextMetrics.prototype,"width");',
266
- " CanvasRenderingContext2D.prototype.measureText=function(t){",
267
- " var m=_mt.call(this,t);",
268
- ' try{Object.defineProperty(m,"width",{get:function(){return _tmw.get.call(m)+_dx*0.01;},configurable:true});}catch(e){}',
269
- " return m;",
270
- " };",
343
+ " if(_tmw&&_tmw.get){",
344
+ " // S172: \u539F\u578B\u5C42\u8986\u5199\u2014\u2014\u5B9E\u4F8B(m)\u8986\u5199\u53EF\u88AB TextMetrics.prototype.width getter \u9003\u9038",
345
+ ' Object.defineProperty(TextMetrics.prototype,"width",{get:function(){return _tmw.get.call(this)+_dx*0.01;},configurable:true});',
346
+ " }",
271
347
  " }catch(e){}",
272
348
  " try{",
273
349
  ' var _fakeVoices=["Alex","Daniel","Karen","Moira","Ralph","Samantha","Ting-Ting","Mei-Jia","Sinji","Yunda"];',
274
- " var _sv=speechSynthesis.getVoices.bind(speechSynthesis);",
275
- " speechSynthesis.getVoices=function(){",
276
- " var real=_sv();",
350
+ // S172: 原型层覆写——实例赋值可被 SpeechSynthesis.prototype.getVoices.call() 逃逸
351
+ " var _sv=SpeechSynthesis.prototype.getVoices;",
352
+ " SpeechSynthesis.prototype.getVoices=function(){",
353
+ " var real=_sv.call(this);",
277
354
  " if(real&&real.length>50)return real;",
278
355
  // 注意阈值从 0 改 50:iframe 原生约 10 个 —— 原阈值下 iframe 返回原生 10 个
279
356
  // 而不是伪装 180(d33 暴露)。>50 = 主文档伪装列表已生效,其余返回伪装。
280
357
  ' 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};});',
281
358
  " };",
282
359
  // d33 修复:iframe 的 speechSynthesis 是独立实例(主文档 hook 不可达)——
283
- // 轮询同源 iframe,对其 contentWindow.speechSynthesis 同样 patch
360
+ // 轮询同源 iframe,对其 contentWindow 的原型同样 patch(S172: 原型层,防逃逸)
284
361
  " try{",
285
362
  " var _piv=function(){",
286
363
  ' var frs=document.querySelectorAll("iframe");',
@@ -288,8 +365,8 @@ function buildStealthInitScript() {
288
365
  " var cw=frs[i].contentWindow;",
289
366
  " if(!cw||!cw.speechSynthesis||cw.speechSynthesis.__xbP)continue;",
290
367
  " cw.speechSynthesis.__xbP=true;",
291
- " var _sv2=cw.speechSynthesis.getVoices.bind(cw.speechSynthesis);",
292
- " cw.speechSynthesis.getVoices=function(){var r=_sv2();if(r&&r.length>50)return r;",
368
+ " var _sv2=cw.SpeechSynthesis.prototype.getVoices;",
369
+ " cw.SpeechSynthesis.prototype.getVoices=function(){var r=_sv2.call(this);if(r&&r.length>50)return r;",
293
370
  ' return _fakeVoices.map(function(n,j){return{name:n,lang:j<2?"en-US":j<6?"en-GB":"zh-TW",localService:true,default:j===0,voiceURI:n};});};',
294
371
  " }catch(e2){}}",
295
372
  " };",
@@ -324,10 +401,21 @@ function buildStealthInitScript() {
324
401
  // 3d-2. performance.now precision clamp (d34): full-precision timestamps
325
402
  // (11 decimals) differ from site-isolation-clamped real Chrome (~100μs).
326
403
  " try{",
327
- " var _pn=performance.now.bind(performance);",
328
- " performance.now=function(){return Math.round(_pn()*10)/10;};",
404
+ " var _pn=Performance.prototype.now;",
405
+ " // S172: \u539F\u578B\u5C42\u8986\u5199\u2014\u2014\u5B9E\u4F8B\u8D4B\u503C\u53EF\u88AB Performance.prototype.now.call(performance) \u9003\u9038",
406
+ " Performance.prototype.now=function(){return Math.round(_pn.call(this)*10)/10;};",
329
407
  ' var _pto=Object.getOwnPropertyDescriptor(Performance.prototype,"timeOrigin");',
330
- ' if(_pto&&_pto.get){Object.defineProperty(performance,"timeOrigin",{get:function(){return _pto.get.call(performance);},configurable:true});}',
408
+ " if(_pto&&_pto.get){",
409
+ " // S173: timeOrigin \u771F\u4F2A\u88C5\u2014\u2014\u5360\u4F4D\u5C42\uFF08\u8FD4\u56DE\u771F\u503C\uFF09\u5347\u7EA7\u4E3A\u4F1A\u8BDD\u7EA7\u968F\u673A\u504F\u79FB\u3002",
410
+ " // \u9632 cross-page \u5BFC\u822A\u65F6\u523B\u5173\u8054\uFF1A\u4E0D\u540C\u7AD9\u70B9\u7528 timeOrigin \u7CBE\u786E\u5BF9\u9F50\u7528\u6237\u884C\u4E3A\u6863\u6848\u3002",
411
+ " // \u504F\u79FB\u7531\u672C\u9875 _seed \u6D3E\u751F\uFF08\u9875\u5185\u81EA\u6D3D\uFF0Cnow()+\u504F\u79FB\u4E00\u81F4\uFF09\uFF0C\u8DE8\u9875\u968F\u673A\uFF08\u4E0D\u53EF\u5173\u8054\uFF09\u3002",
412
+ " var _toff=Math.floor((_seed/2147483647)*600000-300000);",
413
+ " var _tog=function(){return _pto.get.call(performance)+_toff;};",
414
+ ' _tog.toString=function(){return "function get timeOrigin() { [native code] }";};',
415
+ " // S172 \u5BA1\u8BA1\uFF1A\u5B9E\u4F8B\u8986\u5199\u53EF\u88AB\u539F\u578B getter \u9003\u9038\u2014\u2014\u539F\u578B\u5C42\u8986\u5199",
416
+ ' Object.defineProperty(Performance.prototype,"timeOrigin",{get:_tog,configurable:true});',
417
+ ' Object.defineProperty(performance,"timeOrigin",{get:_tog,configurable:true});',
418
+ " }",
331
419
  " }catch(e){}",
332
420
  // 3f. getCoalescedEvents 合成(d47):真实鼠标 125Hz 采样被浏览器按帧合并,
333
421
  // 快速移动时 pointermove.getCoalescedEvents() 返回 2~6 个事件(群内
@@ -441,6 +529,9 @@ function buildStealthInitScript() {
441
529
  ' if(this===AnalyserNode.prototype.getFloatFrequencyData)return"function getFloatFrequencyData() { [native code] }";',
442
530
  ' if(this===AudioBuffer.prototype.getChannelData)return"function getChannelData() { [native code] }";',
443
531
  ' if(this===_gceH)return"function getCoalescedEvents() { [native code] }";',
532
+ ' if(this===Document.prototype.hasFocus)return"function hasFocus() { [native code] }";',
533
+ ' if(this===Performance.prototype.now)return"function now() { [native code] }";',
534
+ ' if(this===SpeechSynthesis.prototype.getVoices)return"function getVoices() { [native code] }";',
444
535
  " return _ts.call(this);",
445
536
  " };",
446
537
  // 4. onclick prototype hijack (dual-stream consistency)
@@ -1893,7 +1984,7 @@ var XBPageImpl = class _XBPageImpl {
1893
1984
  try {
1894
1985
  await this.conn.send(
1895
1986
  "Page.addScriptToEvaluateOnNewDocument",
1896
- { source: buildStealthInitScript() },
1987
+ { source: buildStealthInitScript({ ...DEFAULT_STEALTH_CONFIG, ...this._contextImpl.stealthConfig ?? {} }) },
1897
1988
  this.sessionId
1898
1989
  );
1899
1990
  } catch {
@@ -3150,6 +3241,10 @@ var XBContextImpl = class {
3150
3241
  this.options = opts;
3151
3242
  this.setupAutoAttach();
3152
3243
  }
3244
+ /** S194: 任务 tab 的 stealth 配置(UA-CH 档案等)经 context 传给 page 层 */
3245
+ get stealthConfig() {
3246
+ return this.options.stealthConfig;
3247
+ }
3153
3248
  async newPage() {
3154
3249
  if (this.closed) throw new Error("Context is closed");
3155
3250
  const { targetId } = await this._browser._createTarget(this.contextId);
@@ -3316,20 +3411,8 @@ var XBContextImpl = class {
3316
3411
 
3317
3412
  // src/cdp-driver/browser.ts
3318
3413
  var XBBrowserImpl = class {
3319
- conn;
3320
- _emitter = new EventEmitter3();
3321
- _contexts = /* @__PURE__ */ new Map();
3322
- _disconnected = false;
3323
- childProcess = null;
3324
- tmpDir;
3325
- _exitHandler = null;
3326
- /**
3327
- * Original CDP endpoint (HTTP or ws URL) used to construct this browser.
3328
- * Used by discoverContexts() as a fallback to HTTP /json/list when
3329
- * Target.getTargets doesn't return page-type targets (e.g. cdp-tunnel proxy).
3330
- */
3331
- cdpEndpoint;
3332
- constructor(conn, childProcess, tmpDir, cdpEndpoint) {
3414
+ constructor(conn, childProcess, tmpDir, cdpEndpoint, launchOpts) {
3415
+ this.launchOpts = launchOpts;
3333
3416
  this.conn = conn;
3334
3417
  this.childProcess = childProcess ?? null;
3335
3418
  this.tmpDir = tmpDir;
@@ -3357,6 +3440,20 @@ var XBBrowserImpl = class {
3357
3440
  process.on("exit", this._exitHandler);
3358
3441
  }
3359
3442
  }
3443
+ launchOpts;
3444
+ conn;
3445
+ _emitter = new EventEmitter3();
3446
+ _contexts = /* @__PURE__ */ new Map();
3447
+ _disconnected = false;
3448
+ childProcess = null;
3449
+ tmpDir;
3450
+ _exitHandler = null;
3451
+ /**
3452
+ * Original CDP endpoint (HTTP or ws URL) used to construct this browser.
3453
+ * Used by discoverContexts() as a fallback to HTTP /json/list when
3454
+ * Target.getTargets doesn't return page-type targets (e.g. cdp-tunnel proxy).
3455
+ */
3456
+ cdpEndpoint;
3360
3457
  get disconnected() {
3361
3458
  return this._disconnected;
3362
3459
  }
@@ -3384,6 +3481,9 @@ var XBBrowserImpl = class {
3384
3481
  this._emitter.emit("disconnected");
3385
3482
  }
3386
3483
  async newContext(opts = {}) {
3484
+ if (this.launchOpts?.stealthConfig && !opts.stealthConfig) {
3485
+ opts = { ...opts, stealthConfig: this.launchOpts.stealthConfig };
3486
+ }
3387
3487
  if (this._disconnected) {
3388
3488
  throw new Error("Browser is disconnected");
3389
3489
  }
@@ -3610,6 +3710,9 @@ function extractAllStrings(rawArgs) {
3610
3710
  }
3611
3711
  return strings.length > 0 ? strings.join("\n") : null;
3612
3712
  }
3713
+ function isProbeMarked(code) {
3714
+ return code.includes("/* @xb-probe */");
3715
+ }
3613
3716
 
3614
3717
  // src/cdp-interceptor/rules/dom-mutation.ts
3615
3718
  var DOM_PATTERNS = [
@@ -4172,6 +4275,7 @@ var eventSimulationRule = {
4172
4275
  evaluate(ctx) {
4173
4276
  const userCode = extractUserCode(ctx);
4174
4277
  if (!userCode) return null;
4278
+ if (isProbeMarked(userCode)) return null;
4175
4279
  for (const p of EVENT_PATTERNS) {
4176
4280
  if (p.pattern.test(userCode)) {
4177
4281
  return {
@@ -4499,6 +4603,11 @@ function createRuleEngine(customRules) {
4499
4603
  ...ctx,
4500
4604
  sessionState: getSessionState(ctx.sessionId)
4501
4605
  };
4606
+ try {
4607
+ const code = typeof ctx.params?.expression === "string" ? ctx.params.expression : JSON.stringify(ctx.params ?? "");
4608
+ if (isProbeMarked(code)) return null;
4609
+ } catch {
4610
+ }
4502
4611
  for (const rule of rules) {
4503
4612
  if (rule.canHandle && !rule.canHandle(fullCtx)) continue;
4504
4613
  const decision = rule.evaluate(fullCtx);
@@ -4880,7 +4989,13 @@ async function launch(options = {}) {
4880
4989
  const conn = new CDPConnection(wsEndpoint);
4881
4990
  await conn.ready();
4882
4991
  const httpEndpoint = options.cdpEndpoint && !options.cdpEndpoint.startsWith("ws") ? options.cdpEndpoint : void 0;
4883
- const browser = new XBBrowserImpl(conn, childProcess, tmpDir, httpEndpoint);
4992
+ const browser = new XBBrowserImpl(
4993
+ conn,
4994
+ childProcess,
4995
+ tmpDir,
4996
+ httpEndpoint,
4997
+ options.stealthConfig ? { stealthConfig: options.stealthConfig } : void 0
4998
+ );
4884
4999
  return { browser, wsEndpoint };
4885
5000
  }
4886
5001
 
@@ -26,6 +26,25 @@ import { EventEmitter } from "events";
26
26
 
27
27
  // src/cdp-driver/stealth.ts
28
28
  var DEFAULT_STEALTH_CONFIG = {
29
+ // S191: macOS arm64 + Chrome 151 真实采样(来源:登录桥只读通道)
30
+ uaChProfile: {
31
+ brands: [
32
+ { brand: "Not=A?Brand", version: "99" },
33
+ { brand: "Google Chrome", version: "151" },
34
+ { brand: "Chromium", version: "151" }
35
+ ],
36
+ platform: "macOS",
37
+ platformVersion: "26.2.0",
38
+ architecture: "arm",
39
+ bitness: "64",
40
+ model: "",
41
+ uaFullVersion: "151.0.7922.175",
42
+ fullVersionList: [
43
+ { brand: "Not=A?Brand", version: "99.0.0.0" },
44
+ { brand: "Google Chrome", version: "151.0.7922.175" },
45
+ { brand: "Chromium", version: "151.0.7922.175" }
46
+ ]
47
+ },
29
48
  bezierCurvature: [0.35, 0.6],
30
49
  noiseAmplitude: 5.5,
31
50
  overshootRange: [6, 14],
@@ -132,10 +151,13 @@ Object.assign(KEY_MAP, {
132
151
  "@": { key: "@", code: "Digit2", vk: 50, shift: true },
133
152
  "_": { key: "_", code: "Minus", vk: 189, shift: true }
134
153
  });
135
- function buildStealthInitScript() {
154
+ function buildStealthInitScript(config = DEFAULT_STEALTH_CONFIG) {
155
+ const prof = config.uaChProfile;
156
+ const profJson = prof ? JSON.stringify(prof) : "null";
136
157
  return [
137
158
  "(function(){",
138
159
  ' window.__xbStealthVer="57";',
160
+ " window.__xbUaChProf=" + profJson + ";",
139
161
  // 1. AEL event proxy
140
162
  " var o=EventTarget.prototype.addEventListener;",
141
163
  " var fc=new InputDeviceCapabilities({firesTouchEvents:false});",
@@ -198,9 +220,65 @@ function buildStealthInitScript() {
198
220
  ' Object.defineProperty(Screen.prototype,"height",{get:_gh,configurable:true});',
199
221
  ' Object.defineProperty(Screen.prototype,"availWidth",{get:_gw,configurable:true});',
200
222
  ' Object.defineProperty(Screen.prototype,"availHeight",{get:_gah,configurable:true});',
223
+ // S174: UA Headless 标记清洗——headless launch 的 UA 含 HeadlessChrome/xxx
224
+ // (最高频检测面),同步清洗 userAgent/appVersion/userAgentData(brands+高熵值)。
225
+ // 原型+实例双层覆写(S172 模式)。
226
+ " try{",
227
+ ' var fixUA=function(ua){return String(ua).replace("HeadlessChrome","Chrome");};',
228
+ ' var fixBrands=function(list){return (list||[]).map(function(b){return (b.brand&&b.brand.indexOf("Headless")!==-1)?Object.assign({},b,{brand:b.brand.replace("HeadlessChrome","Chrome")}):b;});};',
229
+ ' var _nuad=Object.getOwnPropertyDescriptor(Navigator.prototype,"userAgentData");',
230
+ " var wrapUAD=function(uad){",
231
+ " if(!uad||uad.__xbUA)return uad;",
232
+ " try{",
233
+ ' Object.defineProperty(uad,"brands",{get:function(){return fixBrands(_nuad.get.call(navigator).brands);},configurable:true});',
234
+ " var _ghev=uad.getHighEntropyValues.bind(uad);",
235
+ " uad.getHighEntropyValues=function(hints){return _ghev(hints).then(function(v){",
236
+ ' var o={};for(var k in v){o[k]=(typeof v[k]==="string")?fixUA(v[k]):(Array.isArray(v[k])?fixBrands(v[k]):v[k]);}',
237
+ " return o;});};",
238
+ " uad.__xbUA=true;",
239
+ " }catch(e){}",
240
+ " return uad;",
241
+ " };",
242
+ ' var _nua=Object.getOwnPropertyDescriptor(Navigator.prototype,"userAgent");',
243
+ " if(_nua&&_nua.get){",
244
+ " var _nuag=function(){return fixUA(_nua.get.call(this));};",
245
+ ' _nuag.toString=function(){return "function get userAgent() { [native code] }";};',
246
+ ' Object.defineProperty(Navigator.prototype,"userAgent",{get:_nuag,configurable:true});',
247
+ " }",
248
+ ' Object.defineProperty(navigator,"userAgent",{get:function(){return fixUA(_nua?_nua.get.call(this):"");},configurable:true});',
249
+ " if(_nuad&&_nuad.get){",
250
+ " var _nuadg=function(){return wrapUAD(_nuad.get.call(this));};",
251
+ ' _nuadg.toString=function(){return "function get userAgentData() { [native code] }";};',
252
+ ' Object.defineProperty(Navigator.prototype,"userAgentData",{get:_nuadg,configurable:true});',
253
+ " }",
254
+ // S190: userAgentData 垫片——headless Chrome 151 不暴露 UA-CH API,
255
+ // 缺失本身即 headless 指纹。从 UA 派生 brands/platform 垫片 + getHighEntropyValues。
256
+ " if(!navigator.userAgentData && window.__xbUaChProf){",
257
+ " var prof = window.__xbUaChProf;",
258
+ " var uad={",
259
+ ' brands:prof.brands.map(function(b){return {brand:b.brand,version:b.version.split(".")[0]}}),',
260
+ " mobile:false,",
261
+ " platform:prof.platform,",
262
+ " getHighEntropyValues:function(hints){",
263
+ " return Promise.resolve().then(function(){",
264
+ " var all={brands:uad.brands,mobile:false,platform:prof.platform,",
265
+ " platformVersion:prof.platformVersion,architecture:prof.architecture,bitness:prof.bitness,model:prof.model,",
266
+ " uaFullVersion:prof.uaFullVersion,",
267
+ " fullVersionList:prof.fullVersionList};",
268
+ " var out={};",
269
+ " (hints||[]).forEach(function(h){if(h in all)out[h]=all[h]});",
270
+ " return out;",
271
+ " });",
272
+ " },",
273
+ " toJSON:function(){return{brands:uad.brands,mobile:false,platform:prof.platform}}",
274
+ " };",
275
+ ' Object.defineProperty(navigator,"userAgentData",{get:function(){return uad},configurable:true});',
276
+ " }",
277
+ " }catch(e){}",
201
278
  // hasFocus 与 visibilityState 联动(d29):恒 true 在 hidden 标签下暴露
202
279
  // —— 真实浏览器失焦/后台时 hasFocus=false
203
- ' document.hasFocus=function(){return document.visibilityState==="visible";};',
280
+ // S172: 原型层覆写——实例赋值可被 Document.prototype.hasFocus.call(document) 逃逸
281
+ ' Document.prototype.hasFocus=function(){return document.visibilityState==="visible";};',
204
282
  // 4. Canvas/WebGL fingerprint: per-session stable noise (d20).
205
283
  // Headless software raster differs subtly from Chrome GPU raster —
206
284
  // toDataURL hashes fingerprint the rasterizer. Inject a stable
@@ -263,26 +341,25 @@ function buildStealthInitScript() {
263
341
  " }catch(e){}",
264
342
  // 3b. Font metrics + speechSynthesis + battery (d22): headless tells
265
343
  " try{",
266
- " var _mt=CanvasRenderingContext2D.prototype.measureText;",
267
344
  ' var _tmw=Object.getOwnPropertyDescriptor(TextMetrics.prototype,"width");',
268
- " CanvasRenderingContext2D.prototype.measureText=function(t){",
269
- " var m=_mt.call(this,t);",
270
- ' try{Object.defineProperty(m,"width",{get:function(){return _tmw.get.call(m)+_dx*0.01;},configurable:true});}catch(e){}',
271
- " return m;",
272
- " };",
345
+ " if(_tmw&&_tmw.get){",
346
+ " // S172: \u539F\u578B\u5C42\u8986\u5199\u2014\u2014\u5B9E\u4F8B(m)\u8986\u5199\u53EF\u88AB TextMetrics.prototype.width getter \u9003\u9038",
347
+ ' Object.defineProperty(TextMetrics.prototype,"width",{get:function(){return _tmw.get.call(this)+_dx*0.01;},configurable:true});',
348
+ " }",
273
349
  " }catch(e){}",
274
350
  " try{",
275
351
  ' var _fakeVoices=["Alex","Daniel","Karen","Moira","Ralph","Samantha","Ting-Ting","Mei-Jia","Sinji","Yunda"];',
276
- " var _sv=speechSynthesis.getVoices.bind(speechSynthesis);",
277
- " speechSynthesis.getVoices=function(){",
278
- " var real=_sv();",
352
+ // S172: 原型层覆写——实例赋值可被 SpeechSynthesis.prototype.getVoices.call() 逃逸
353
+ " var _sv=SpeechSynthesis.prototype.getVoices;",
354
+ " SpeechSynthesis.prototype.getVoices=function(){",
355
+ " var real=_sv.call(this);",
279
356
  " if(real&&real.length>50)return real;",
280
357
  // 注意阈值从 0 改 50:iframe 原生约 10 个 —— 原阈值下 iframe 返回原生 10 个
281
358
  // 而不是伪装 180(d33 暴露)。>50 = 主文档伪装列表已生效,其余返回伪装。
282
359
  ' 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};});',
283
360
  " };",
284
361
  // d33 修复:iframe 的 speechSynthesis 是独立实例(主文档 hook 不可达)——
285
- // 轮询同源 iframe,对其 contentWindow.speechSynthesis 同样 patch
362
+ // 轮询同源 iframe,对其 contentWindow 的原型同样 patch(S172: 原型层,防逃逸)
286
363
  " try{",
287
364
  " var _piv=function(){",
288
365
  ' var frs=document.querySelectorAll("iframe");',
@@ -290,8 +367,8 @@ function buildStealthInitScript() {
290
367
  " var cw=frs[i].contentWindow;",
291
368
  " if(!cw||!cw.speechSynthesis||cw.speechSynthesis.__xbP)continue;",
292
369
  " cw.speechSynthesis.__xbP=true;",
293
- " var _sv2=cw.speechSynthesis.getVoices.bind(cw.speechSynthesis);",
294
- " cw.speechSynthesis.getVoices=function(){var r=_sv2();if(r&&r.length>50)return r;",
370
+ " var _sv2=cw.SpeechSynthesis.prototype.getVoices;",
371
+ " cw.SpeechSynthesis.prototype.getVoices=function(){var r=_sv2.call(this);if(r&&r.length>50)return r;",
295
372
  ' return _fakeVoices.map(function(n,j){return{name:n,lang:j<2?"en-US":j<6?"en-GB":"zh-TW",localService:true,default:j===0,voiceURI:n};});};',
296
373
  " }catch(e2){}}",
297
374
  " };",
@@ -326,10 +403,21 @@ function buildStealthInitScript() {
326
403
  // 3d-2. performance.now precision clamp (d34): full-precision timestamps
327
404
  // (11 decimals) differ from site-isolation-clamped real Chrome (~100μs).
328
405
  " try{",
329
- " var _pn=performance.now.bind(performance);",
330
- " performance.now=function(){return Math.round(_pn()*10)/10;};",
406
+ " var _pn=Performance.prototype.now;",
407
+ " // S172: \u539F\u578B\u5C42\u8986\u5199\u2014\u2014\u5B9E\u4F8B\u8D4B\u503C\u53EF\u88AB Performance.prototype.now.call(performance) \u9003\u9038",
408
+ " Performance.prototype.now=function(){return Math.round(_pn.call(this)*10)/10;};",
331
409
  ' var _pto=Object.getOwnPropertyDescriptor(Performance.prototype,"timeOrigin");',
332
- ' if(_pto&&_pto.get){Object.defineProperty(performance,"timeOrigin",{get:function(){return _pto.get.call(performance);},configurable:true});}',
410
+ " if(_pto&&_pto.get){",
411
+ " // S173: timeOrigin \u771F\u4F2A\u88C5\u2014\u2014\u5360\u4F4D\u5C42\uFF08\u8FD4\u56DE\u771F\u503C\uFF09\u5347\u7EA7\u4E3A\u4F1A\u8BDD\u7EA7\u968F\u673A\u504F\u79FB\u3002",
412
+ " // \u9632 cross-page \u5BFC\u822A\u65F6\u523B\u5173\u8054\uFF1A\u4E0D\u540C\u7AD9\u70B9\u7528 timeOrigin \u7CBE\u786E\u5BF9\u9F50\u7528\u6237\u884C\u4E3A\u6863\u6848\u3002",
413
+ " // \u504F\u79FB\u7531\u672C\u9875 _seed \u6D3E\u751F\uFF08\u9875\u5185\u81EA\u6D3D\uFF0Cnow()+\u504F\u79FB\u4E00\u81F4\uFF09\uFF0C\u8DE8\u9875\u968F\u673A\uFF08\u4E0D\u53EF\u5173\u8054\uFF09\u3002",
414
+ " var _toff=Math.floor((_seed/2147483647)*600000-300000);",
415
+ " var _tog=function(){return _pto.get.call(performance)+_toff;};",
416
+ ' _tog.toString=function(){return "function get timeOrigin() { [native code] }";};',
417
+ " // S172 \u5BA1\u8BA1\uFF1A\u5B9E\u4F8B\u8986\u5199\u53EF\u88AB\u539F\u578B getter \u9003\u9038\u2014\u2014\u539F\u578B\u5C42\u8986\u5199",
418
+ ' Object.defineProperty(Performance.prototype,"timeOrigin",{get:_tog,configurable:true});',
419
+ ' Object.defineProperty(performance,"timeOrigin",{get:_tog,configurable:true});',
420
+ " }",
333
421
  " }catch(e){}",
334
422
  // 3f. getCoalescedEvents 合成(d47):真实鼠标 125Hz 采样被浏览器按帧合并,
335
423
  // 快速移动时 pointermove.getCoalescedEvents() 返回 2~6 个事件(群内
@@ -443,6 +531,9 @@ function buildStealthInitScript() {
443
531
  ' if(this===AnalyserNode.prototype.getFloatFrequencyData)return"function getFloatFrequencyData() { [native code] }";',
444
532
  ' if(this===AudioBuffer.prototype.getChannelData)return"function getChannelData() { [native code] }";',
445
533
  ' if(this===_gceH)return"function getCoalescedEvents() { [native code] }";',
534
+ ' if(this===Document.prototype.hasFocus)return"function hasFocus() { [native code] }";',
535
+ ' if(this===Performance.prototype.now)return"function now() { [native code] }";',
536
+ ' if(this===SpeechSynthesis.prototype.getVoices)return"function getVoices() { [native code] }";',
446
537
  " return _ts.call(this);",
447
538
  " };",
448
539
  // 4. onclick prototype hijack (dual-stream consistency)
@@ -1997,7 +2088,7 @@ var XBPageImpl = class _XBPageImpl {
1997
2088
  try {
1998
2089
  await this.conn.send(
1999
2090
  "Page.addScriptToEvaluateOnNewDocument",
2000
- { source: buildStealthInitScript() },
2091
+ { source: buildStealthInitScript({ ...DEFAULT_STEALTH_CONFIG, ...this._contextImpl.stealthConfig ?? {} }) },
2001
2092
  this.sessionId
2002
2093
  );
2003
2094
  } catch {
@@ -3254,6 +3345,10 @@ var XBContextImpl = class {
3254
3345
  this.options = opts;
3255
3346
  this.setupAutoAttach();
3256
3347
  }
3348
+ /** S194: 任务 tab 的 stealth 配置(UA-CH 档案等)经 context 传给 page 层 */
3349
+ get stealthConfig() {
3350
+ return this.options.stealthConfig;
3351
+ }
3257
3352
  async newPage() {
3258
3353
  if (this.closed) throw new Error("Context is closed");
3259
3354
  const { targetId } = await this._browser._createTarget(this.contextId);
@@ -3420,20 +3515,8 @@ var XBContextImpl = class {
3420
3515
 
3421
3516
  // src/cdp-driver/browser.ts
3422
3517
  var XBBrowserImpl = class {
3423
- conn;
3424
- _emitter = new EventEmitter3();
3425
- _contexts = /* @__PURE__ */ new Map();
3426
- _disconnected = false;
3427
- childProcess = null;
3428
- tmpDir;
3429
- _exitHandler = null;
3430
- /**
3431
- * Original CDP endpoint (HTTP or ws URL) used to construct this browser.
3432
- * Used by discoverContexts() as a fallback to HTTP /json/list when
3433
- * Target.getTargets doesn't return page-type targets (e.g. cdp-tunnel proxy).
3434
- */
3435
- cdpEndpoint;
3436
- constructor(conn, childProcess, tmpDir, cdpEndpoint) {
3518
+ constructor(conn, childProcess, tmpDir, cdpEndpoint, launchOpts) {
3519
+ this.launchOpts = launchOpts;
3437
3520
  this.conn = conn;
3438
3521
  this.childProcess = childProcess ?? null;
3439
3522
  this.tmpDir = tmpDir;
@@ -3461,6 +3544,20 @@ var XBBrowserImpl = class {
3461
3544
  process.on("exit", this._exitHandler);
3462
3545
  }
3463
3546
  }
3547
+ launchOpts;
3548
+ conn;
3549
+ _emitter = new EventEmitter3();
3550
+ _contexts = /* @__PURE__ */ new Map();
3551
+ _disconnected = false;
3552
+ childProcess = null;
3553
+ tmpDir;
3554
+ _exitHandler = null;
3555
+ /**
3556
+ * Original CDP endpoint (HTTP or ws URL) used to construct this browser.
3557
+ * Used by discoverContexts() as a fallback to HTTP /json/list when
3558
+ * Target.getTargets doesn't return page-type targets (e.g. cdp-tunnel proxy).
3559
+ */
3560
+ cdpEndpoint;
3464
3561
  get disconnected() {
3465
3562
  return this._disconnected;
3466
3563
  }
@@ -3488,6 +3585,9 @@ var XBBrowserImpl = class {
3488
3585
  this._emitter.emit("disconnected");
3489
3586
  }
3490
3587
  async newContext(opts = {}) {
3588
+ if (this.launchOpts?.stealthConfig && !opts.stealthConfig) {
3589
+ opts = { ...opts, stealthConfig: this.launchOpts.stealthConfig };
3590
+ }
3491
3591
  if (this._disconnected) {
3492
3592
  throw new Error("Browser is disconnected");
3493
3593
  }
@@ -3714,6 +3814,9 @@ function extractAllStrings(rawArgs) {
3714
3814
  }
3715
3815
  return strings.length > 0 ? strings.join("\n") : null;
3716
3816
  }
3817
+ function isProbeMarked(code) {
3818
+ return code.includes("/* @xb-probe */");
3819
+ }
3717
3820
 
3718
3821
  // src/cdp-interceptor/rules/dom-mutation.ts
3719
3822
  var DOM_PATTERNS = [
@@ -4276,6 +4379,7 @@ var eventSimulationRule = {
4276
4379
  evaluate(ctx) {
4277
4380
  const userCode = extractUserCode(ctx);
4278
4381
  if (!userCode) return null;
4382
+ if (isProbeMarked(userCode)) return null;
4279
4383
  for (const p of EVENT_PATTERNS) {
4280
4384
  if (p.pattern.test(userCode)) {
4281
4385
  return {
@@ -4603,6 +4707,11 @@ function createRuleEngine(customRules) {
4603
4707
  ...ctx,
4604
4708
  sessionState: getSessionState(ctx.sessionId)
4605
4709
  };
4710
+ try {
4711
+ const code = typeof ctx.params?.expression === "string" ? ctx.params.expression : JSON.stringify(ctx.params ?? "");
4712
+ if (isProbeMarked(code)) return null;
4713
+ } catch {
4714
+ }
4606
4715
  for (const rule of rules) {
4607
4716
  if (rule.canHandle && !rule.canHandle(fullCtx)) continue;
4608
4717
  const decision = rule.evaluate(fullCtx);
@@ -4924,7 +5033,13 @@ async function launch(options = {}) {
4924
5033
  const conn = new CDPConnection(wsEndpoint);
4925
5034
  await conn.ready();
4926
5035
  const httpEndpoint = options.cdpEndpoint && !options.cdpEndpoint.startsWith("ws") ? options.cdpEndpoint : void 0;
4927
- const browser = new XBBrowserImpl(conn, childProcess, tmpDir, httpEndpoint);
5036
+ const browser = new XBBrowserImpl(
5037
+ conn,
5038
+ childProcess,
5039
+ tmpDir,
5040
+ httpEndpoint,
5041
+ options.stealthConfig ? { stealthConfig: options.stealthConfig } : void 0
5042
+ );
4928
5043
  return { browser, wsEndpoint };
4929
5044
  }
4930
5045