@@ -104,10 +104,251 @@ export class SentienceBrowser {
104104 } ) ;
105105
106106 this . page = this . context . pages ( ) [ 0 ] || await this . context . newPage ( ) ;
107+
108+ // Apply context-level stealth patches (runs on every new page)
109+ await this . context . addInitScript ( ( ) => {
110+ // Early webdriver hiding - runs before any page script
111+ // Use multiple strategies to completely hide webdriver
112+
113+ // Strategy 1: Try to delete it first
114+ try {
115+ delete ( navigator as any ) . webdriver ;
116+ } catch ( e ) {
117+ // Property might not be deletable
118+ }
119+
120+ // Strategy 2: Redefine to return undefined and hide from enumeration
121+ Object . defineProperty ( navigator , 'webdriver' , {
122+ get : ( ) => undefined ,
123+ configurable : true ,
124+ enumerable : false ,
125+ writable : false
126+ } ) ;
127+
128+ // Strategy 3: Override 'in' operator check
129+ const originalHasOwnProperty = Object . prototype . hasOwnProperty ;
130+ Object . prototype . hasOwnProperty = function ( prop : string | number | symbol ) {
131+ if ( this === navigator && ( prop === 'webdriver' || prop === 'Webdriver' ) ) {
132+ return false ;
133+ }
134+ return originalHasOwnProperty . call ( this , prop ) ;
135+ } ;
136+ } ) ;
107137
108- // 5. Apply Stealth (Basic)
138+ // 5. Apply Comprehensive Stealth Patches
139+ // Use both CDP (earlier) and addInitScript (backup) for maximum coverage
140+
141+ // Strategy A: Use CDP to inject at the earliest possible moment
142+ const client = await this . page . context ( ) . newCDPSession ( this . page ) ;
143+ await client . send ( 'Page.addScriptToEvaluateOnNewDocument' , {
144+ source : `
145+ // Aggressive webdriver hiding - must run before ANY page script
146+ Object.defineProperty(navigator, 'webdriver', {
147+ get: () => undefined,
148+ configurable: true,
149+ enumerable: false
150+ });
151+
152+ // Override Object.getOwnPropertyDescriptor
153+ const originalGetOwnPropertyDescriptor = Object.getOwnPropertyDescriptor;
154+ Object.getOwnPropertyDescriptor = function(obj, prop) {
155+ if (obj === navigator && (prop === 'webdriver' || prop === 'Webdriver')) {
156+ return undefined;
157+ }
158+ return originalGetOwnPropertyDescriptor(obj, prop);
159+ };
160+
161+ // Override Object.keys
162+ const originalKeys = Object.keys;
163+ Object.keys = function(obj) {
164+ const keys = originalKeys(obj);
165+ if (obj === navigator) {
166+ return keys.filter(k => k !== 'webdriver' && k !== 'Webdriver');
167+ }
168+ return keys;
169+ };
170+
171+ // Override Object.getOwnPropertyNames
172+ const originalGetOwnPropertyNames = Object.getOwnPropertyNames;
173+ Object.getOwnPropertyNames = function(obj) {
174+ const names = originalGetOwnPropertyNames(obj);
175+ if (obj === navigator) {
176+ return names.filter(n => n !== 'webdriver' && n !== 'Webdriver');
177+ }
178+ return names;
179+ };
180+
181+ // Override 'in' operator check
182+ const originalHasOwnProperty = Object.prototype.hasOwnProperty;
183+ Object.prototype.hasOwnProperty = function(prop) {
184+ if (this === navigator && (prop === 'webdriver' || prop === 'Webdriver')) {
185+ return false;
186+ }
187+ return originalHasOwnProperty.call(this, prop);
188+ };
189+ `
190+ } ) ;
191+
192+ // Strategy B: Also use addInitScript as backup (runs after CDP but before page scripts)
109193 await this . page . addInitScript ( ( ) => {
110- Object . defineProperty ( navigator , 'webdriver' , { get : ( ) => false } ) ;
194+ // 1. Hide navigator.webdriver (comprehensive approach for advanced detection)
195+ // Advanced detection checks for property descriptor, so we need multiple strategies
196+ try {
197+ // Strategy 1: Try to delete the property
198+ delete ( navigator as any ) . webdriver ;
199+ } catch ( e ) {
200+ // Property might not be deletable, continue with redefine
201+ }
202+
203+ // Strategy 2: Redefine to return undefined (better than false)
204+ // Also set enumerable: false to hide from Object.keys() checks
205+ Object . defineProperty ( navigator , 'webdriver' , {
206+ get : ( ) => undefined ,
207+ configurable : true ,
208+ enumerable : false
209+ } ) ;
210+
211+ // Strategy 3: Override Object.getOwnPropertyDescriptor only for navigator.webdriver
212+ // This prevents advanced detection that checks the property descriptor
213+ const originalGetOwnPropertyDescriptor = Object . getOwnPropertyDescriptor ;
214+ Object . getOwnPropertyDescriptor = function ( obj : any , prop : string | symbol ) {
215+ if ( obj === navigator && ( prop === 'webdriver' || prop === 'Webdriver' ) ) {
216+ return undefined ;
217+ }
218+ return originalGetOwnPropertyDescriptor ( obj , prop ) ;
219+ } as any ;
220+
221+ // Strategy 4: Hide from Object.keys() and Object.getOwnPropertyNames()
222+ const originalKeys = Object . keys ;
223+ Object . keys = function ( obj : any ) {
224+ const keys = originalKeys ( obj ) ;
225+ if ( obj === navigator ) {
226+ return keys . filter ( k => k !== 'webdriver' && k !== 'Webdriver' ) ;
227+ }
228+ return keys ;
229+ } as any ;
230+
231+ // Strategy 5: Hide from Object.getOwnPropertyNames()
232+ const originalGetOwnPropertyNames = Object . getOwnPropertyNames ;
233+ Object . getOwnPropertyNames = function ( obj : any ) {
234+ const names = originalGetOwnPropertyNames ( obj ) ;
235+ if ( obj === navigator ) {
236+ return names . filter ( n => n !== 'webdriver' && n !== 'Webdriver' ) ;
237+ }
238+ return names ;
239+ } as any ;
240+
241+ // Strategy 6: Override hasOwnProperty to hide from 'in' operator checks
242+ const originalHasOwnProperty = Object . prototype . hasOwnProperty ;
243+ Object . prototype . hasOwnProperty = function ( prop : string | number | symbol ) {
244+ if ( this === navigator && ( prop === 'webdriver' || prop === 'Webdriver' ) ) {
245+ return false ;
246+ }
247+ return originalHasOwnProperty . call ( this , prop ) ;
248+ } ;
249+
250+ // 2. Inject window.chrome object (required for Chrome detection)
251+ if ( typeof ( window as any ) . chrome === 'undefined' ) {
252+ ( window as any ) . chrome = {
253+ runtime : { } ,
254+ loadTimes : function ( ) { } ,
255+ csi : function ( ) { } ,
256+ app : { }
257+ } ;
258+ }
259+
260+ // 3. Patch navigator.plugins (should have length > 0)
261+ // Only patch if plugins array is empty (headless mode issue)
262+ const originalPlugins = navigator . plugins ;
263+ if ( originalPlugins . length === 0 ) {
264+ // Create a PluginArray-like object with minimal plugins
265+ const fakePlugins = [
266+ {
267+ name : 'Chrome PDF Plugin' ,
268+ filename : 'internal-pdf-viewer' ,
269+ description : 'Portable Document Format' ,
270+ length : 1 ,
271+ item : function ( ) { return null ; } ,
272+ namedItem : function ( ) { return null ; }
273+ } ,
274+ {
275+ name : 'Chrome PDF Viewer' ,
276+ filename : 'mhjfbmdgcfjbbpaeojofohoefgiehjai' ,
277+ description : '' ,
278+ length : 0 ,
279+ item : function ( ) { return null ; } ,
280+ namedItem : function ( ) { return null ; }
281+ } ,
282+ {
283+ name : 'Native Client' ,
284+ filename : 'internal-nacl-plugin' ,
285+ description : '' ,
286+ length : 0 ,
287+ item : function ( ) { return null ; } ,
288+ namedItem : function ( ) { return null ; }
289+ }
290+ ] ;
291+
292+ // Create PluginArray-like object (array-like but not a real array)
293+ // This needs to behave like the real PluginArray for detection to pass
294+ const pluginArray : any = { } ;
295+ fakePlugins . forEach ( ( plugin , index ) => {
296+ Object . defineProperty ( pluginArray , index . toString ( ) , {
297+ value : plugin ,
298+ enumerable : true ,
299+ configurable : true
300+ } ) ;
301+ } ) ;
302+
303+ Object . defineProperty ( pluginArray , 'length' , {
304+ value : fakePlugins . length ,
305+ enumerable : false ,
306+ configurable : false
307+ } ) ;
308+
309+ pluginArray . item = function ( index : number ) {
310+ return this [ index ] || null ;
311+ } ;
312+ pluginArray . namedItem = function ( name : string ) {
313+ for ( let i = 0 ; i < this . length ; i ++ ) {
314+ if ( this [ i ] && this [ i ] . name === name ) return this [ i ] ;
315+ }
316+ return null ;
317+ } ;
318+
319+ // Make it iterable (for for...of loops)
320+ pluginArray [ Symbol . iterator ] = function * ( ) {
321+ for ( let i = 0 ; i < this . length ; i ++ ) {
322+ yield this [ i ] ;
323+ }
324+ } ;
325+
326+ // Make it array-like for Array.from() and spread
327+ Object . setPrototypeOf ( pluginArray , Object . create ( null ) ) ;
328+
329+ Object . defineProperty ( navigator , 'plugins' , {
330+ get : ( ) => pluginArray ,
331+ configurable : true ,
332+ enumerable : true
333+ } ) ;
334+ }
335+
336+ // 4. Ensure navigator.languages exists and has values
337+ if ( ! navigator . languages || navigator . languages . length === 0 ) {
338+ Object . defineProperty ( navigator , 'languages' , {
339+ get : ( ) => [ 'en-US' , 'en' ] ,
340+ configurable : true
341+ } ) ;
342+ }
343+
344+ // 5. Patch permissions API (should exist)
345+ if ( ! navigator . permissions ) {
346+ ( navigator as any ) . permissions = {
347+ query : async ( parameters : PermissionDescriptor ) => {
348+ return { state : 'granted' , onchange : null } as PermissionStatus ;
349+ }
350+ } ;
351+ }
111352 } ) ;
112353
113354 // Inject API Key if present
0 commit comments