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);