From d3f8457c421889f6c5303755ede0260427f77143 Mon Sep 17 00:00:00 2001 From: ValueOn AG Date: Mon, 16 Feb 2026 16:18:19 +0100 Subject: [PATCH] fix: add stealth measures to bypass Teams bot detection (webdriver, plugins, chrome.runtime) Co-authored-by: Cursor --- src/bot/orchestrator.ts | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts index fd139b5..4b3d1b8 100644 --- a/src/bot/orchestrator.ts +++ b/src/bot/orchestrator.ts @@ -545,6 +545,7 @@ export class BotOrchestrator { '--disable-web-security', '--disable-features=IsolateOrigins,site-per-process', '--autoplay-policy=no-user-gesture-required', + '--disable-blink-features=AutomationControlled', // Prevent navigator.webdriver=true ], }); @@ -556,6 +557,42 @@ export class BotOrchestrator { this._page = await this._context.newPage(); + // Stealth: Override browser properties that reveal automation. + // Teams checks these to detect headless/automated browsers and + // blocks the /v2/ authenticated experience, falling back to light-meetings. + await this._page.addInitScript(() => { + // 1. Remove navigator.webdriver flag (primary detection signal) + Object.defineProperty(navigator, 'webdriver', { get: () => false }); + + // 2. Add realistic plugins (headless has empty plugins array) + Object.defineProperty(navigator, 'plugins', { + get: () => [ + { name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' }, + { name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' }, + { name: 'Native Client', filename: 'internal-nacl-plugin', description: '' }, + ], + }); + + // 3. Add realistic languages + Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en', 'de'] }); + + // 4. Override permissions query to not reveal automation + const originalQuery = window.navigator.permissions.query.bind(window.navigator.permissions); + // @ts-ignore + window.navigator.permissions.query = (parameters: any) => { + if (parameters.name === 'notifications') { + return Promise.resolve({ state: Notification.permission } as PermissionStatus); + } + return originalQuery(parameters); + }; + + // 5. Add chrome runtime (missing in headless) + // @ts-ignore + if (!window.chrome) { window.chrome = {}; } + // @ts-ignore + if (!window.chrome.runtime) { window.chrome.runtime = {}; } + }); + // Initialize procedures const isAuthenticated = !!(this._options.botAccountEmail && this._options.botAccountPassword); this._joinProcedure = new JoinProcedure(this._page, this._logger, this._botName, isAuthenticated);