teams test auth bot

This commit is contained in:
ValueOn AG 2026-02-16 21:37:41 +01:00
parent b8bb5affa9
commit 8729c8056d
3 changed files with 847 additions and 0 deletions

View file

@ -12,6 +12,9 @@
},
"dependencies": {
"playwright": "1.50.0",
"rebrowser-playwright": "^1.50.0",
"playwright-extra": "^4.3.6",
"puppeteer-extra-plugin-stealth": "^2.11.2",
"ws": "^8.16.0",
"uuid": "^9.0.1",
"dotenv": "^16.4.1",

View file

@ -0,0 +1,823 @@
import { Browser, BrowserContext, Page, chromium } from 'playwright';
import { logger } from '../utils/logger';
import { AuthProcedure } from './authProcedure';
// ============================================================================
// TYPES
// ============================================================================
export interface AuthTestVariant {
id: string;
name: string;
description: string;
}
export interface AuthTestResult {
variantId: string;
variantName: string;
success: boolean;
pageType: 'v2' | 'lightMeetings' | 'error' | 'unknown';
finalUrl: string;
hasSignInLink: boolean;
hasNameInput: boolean;
hasJoinButton: boolean;
authAttempted: boolean;
authSuccess: boolean | null;
screenshot?: string;
durationMs: number;
error?: string;
detectedSignals: string[];
}
export interface AuthTestResults {
meetingUrl: string;
timestamp: string;
variants: AuthTestResult[];
recommendation: string;
}
// ============================================================================
// VARIANT DEFINITIONS
// ============================================================================
const _VARIANTS: AuthTestVariant[] = [
{
id: 'baseline',
name: 'Baseline (aktuell)',
description: 'Vanilla Playwright headless, basic stealth, anon=true',
},
{
id: 'rebrowser',
name: 'rebrowser-playwright',
description: 'rebrowser-playwright headless, CDP-Leak-Fixes, ohne anon',
},
{
id: 'playwrightExtra',
name: 'playwright-extra + stealth',
description: 'playwright-extra mit stealth plugin headless, ohne anon',
},
{
id: 'headful',
name: 'Headful',
description: 'Playwright headful (headless: false), enhanced stealth, ohne anon',
},
{
id: 'headfulAuth',
name: 'Headful + Auth',
description: 'Playwright headful mit vollstaendigem Auth-Flow (System-Bot Credentials)',
},
];
// ============================================================================
// CONSTANTS
// ============================================================================
const _VARIANT_TIMEOUT_MS = 45000;
const _SCREENSHOT_QUALITY = 50;
const _USER_AGENT = 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/131.0.0.0 Safari/537.36 Edg/131.0.0.0';
const _BROWSER_ARGS = [
'--use-fake-ui-for-media-stream',
'--use-fake-device-for-media-stream',
'--disable-web-security',
'--disable-features=IsolateOrigins,site-per-process',
'--autoplay-policy=no-user-gesture-required',
'--disable-blink-features=AutomationControlled',
];
// ============================================================================
// MAIN EXPORT
// ============================================================================
/**
* Run all 5 auth detection test variants against a Teams meeting URL.
* Does NOT join the meeting only checks which page Teams serves.
*/
export async function runAuthTests(
meetingUrl: string,
botAccountEmail?: string,
botAccountPassword?: string,
): Promise<AuthTestResults> {
logger.info(`[AuthTest] Starting auth detection tests for: ${meetingUrl}`);
const results: AuthTestResult[] = [];
for (const variant of _VARIANTS) {
// Skip auth variant if no credentials
if (variant.id === 'headfulAuth' && (!botAccountEmail || !botAccountPassword)) {
results.push(_createSkippedResult(variant, 'Keine System-Bot Credentials konfiguriert'));
continue;
}
logger.info(`[AuthTest] Running variant: ${variant.name}`);
try {
const result = await _runVariant(variant, meetingUrl, botAccountEmail, botAccountPassword);
results.push(result);
logger.info(`[AuthTest] Variant ${variant.name}: pageType=${result.pageType}, url=${result.finalUrl.substring(0, 80)}`);
} catch (err) {
logger.error(`[AuthTest] Variant ${variant.name} failed:`, err);
results.push(_createErrorResult(variant, String(err)));
}
}
const recommendation = _generateRecommendation(results);
logger.info(`[AuthTest] Recommendation: ${recommendation}`);
return {
meetingUrl,
timestamp: new Date().toISOString(),
variants: results,
recommendation,
};
}
// ============================================================================
// VARIANT RUNNER
// ============================================================================
async function _runVariant(
variant: AuthTestVariant,
meetingUrl: string,
botAccountEmail?: string,
botAccountPassword?: string,
): Promise<AuthTestResult> {
const startTime = Date.now();
let browser: Browser | null = null;
let context: BrowserContext | null = null;
try {
// Launch browser based on variant
const launchResult = await _launchBrowserForVariant(variant);
browser = launchResult.browser;
context = launchResult.context;
const page = launchResult.page;
// Resolve meeting URL (with or without anon=true)
const useAnon = variant.id === 'baseline';
const launchUrl = await _resolveMeetingUrl(meetingUrl, useAnon);
logger.info(`[AuthTest:${variant.id}] Navigating to: ${launchUrl.substring(0, 100)}`);
// Navigate to meeting
await page.goto(launchUrl, {
waitUntil: 'domcontentloaded',
timeout: 30000,
});
// Handle launcher dialog ("Continue on this browser")
await _handleLauncher(page);
// Wait for the pre-join page to settle
await page.waitForTimeout(5000);
// If this is the auth variant, attempt login
let authAttempted = false;
let authSuccess: boolean | null = null;
if (variant.id === 'headfulAuth' && botAccountEmail && botAccountPassword) {
authAttempted = true;
authSuccess = await _attemptAuth(page, botAccountEmail, botAccountPassword);
}
// Detect page type and gather signals
const detection = await _detectPageType(page);
// Take screenshot
const screenshot = await _takeScreenshot(page);
return {
variantId: variant.id,
variantName: variant.name,
success: true,
pageType: detection.pageType,
finalUrl: page.url(),
hasSignInLink: detection.hasSignInLink,
hasNameInput: detection.hasNameInput,
hasJoinButton: detection.hasJoinButton,
authAttempted,
authSuccess,
screenshot,
durationMs: Date.now() - startTime,
detectedSignals: detection.signals,
};
} catch (err) {
// Try to take an error screenshot
let screenshot: string | undefined;
try {
if (context) {
const pages = context.pages();
if (pages.length > 0) {
screenshot = await _takeScreenshot(pages[0]);
}
}
} catch {
// Ignore screenshot errors
}
return {
variantId: variant.id,
variantName: variant.name,
success: false,
pageType: 'error',
finalUrl: '',
hasSignInLink: false,
hasNameInput: false,
hasJoinButton: false,
authAttempted: false,
authSuccess: null,
screenshot,
durationMs: Date.now() - startTime,
error: String(err),
detectedSignals: [],
};
} finally {
// Clean up browser
try {
if (context) await context.close();
if (browser) await browser.close();
} catch {
// Ignore cleanup errors
}
}
}
// ============================================================================
// BROWSER LAUNCHERS
// ============================================================================
interface LaunchResult {
browser: Browser;
context: BrowserContext;
page: Page;
}
async function _launchBrowserForVariant(variant: AuthTestVariant): Promise<LaunchResult> {
switch (variant.id) {
case 'baseline':
return _launchVanilla(true, 'basic');
case 'rebrowser':
return _launchRebrowser();
case 'playwrightExtra':
return _launchPlaywrightExtra();
case 'headful':
return _launchVanilla(false, 'enhanced');
case 'headfulAuth':
return _launchVanilla(false, 'enhanced');
default:
return _launchVanilla(true, 'basic');
}
}
/**
* Launch vanilla Playwright with configurable headless mode and stealth level.
*/
async function _launchVanilla(headless: boolean, stealthLevel: 'basic' | 'enhanced'): Promise<LaunchResult> {
const browser = await chromium.launch({
headless,
args: _BROWSER_ARGS,
});
const context = await browser.newContext({
permissions: ['microphone', 'camera'],
viewport: { width: 1280, height: 720 },
userAgent: _USER_AGENT,
});
const page = await context.newPage();
if (stealthLevel === 'basic') {
await page.addInitScript(_getBasicStealthScript);
} else {
await page.addInitScript(_getEnhancedStealthScript);
}
return { browser, context, page };
}
/**
* Launch rebrowser-playwright (patched Playwright with CDP leak fixes).
* Uses dynamic require to avoid import errors if package is not installed.
*/
async function _launchRebrowser(): Promise<LaunchResult> {
let rbChromium: typeof chromium;
try {
// Dynamic require for the rebrowser-playwright package
const rb = require('rebrowser-playwright');
rbChromium = rb.chromium;
} catch {
logger.warn('[AuthTest] rebrowser-playwright not installed, falling back to vanilla');
return _launchVanilla(true, 'enhanced');
}
const browser = await rbChromium.launch({
headless: true,
args: _BROWSER_ARGS,
});
const context = await browser.newContext({
permissions: ['microphone', 'camera'],
viewport: { width: 1280, height: 720 },
userAgent: _USER_AGENT,
});
const page = await context.newPage();
// rebrowser-playwright handles most stealth automatically, but add enhanced overrides too
await page.addInitScript(_getEnhancedStealthScript);
return { browser, context, page };
}
/**
* Launch playwright-extra with the stealth plugin.
* Uses dynamic require to avoid import errors if package is not installed.
*/
async function _launchPlaywrightExtra(): Promise<LaunchResult> {
let chromiumExtra: typeof chromium;
try {
const { chromium: pweChromium } = require('playwright-extra');
const StealthPlugin = require('puppeteer-extra-plugin-stealth');
pweChromium.use(StealthPlugin());
chromiumExtra = pweChromium;
} catch {
logger.warn('[AuthTest] playwright-extra not installed, falling back to vanilla');
return _launchVanilla(true, 'enhanced');
}
const browser = await chromiumExtra.launch({
headless: true,
args: _BROWSER_ARGS,
});
const context = await browser.newContext({
permissions: ['microphone', 'camera'],
viewport: { width: 1280, height: 720 },
userAgent: _USER_AGENT,
});
const page = await context.newPage();
return { browser, context, page };
}
// ============================================================================
// STEALTH SCRIPTS
// ============================================================================
/**
* Basic stealth script matches the current orchestrator.ts implementation.
*/
function _getBasicStealthScript(): void {
// 1. Remove navigator.webdriver flag
Object.defineProperty(navigator, 'webdriver', { get: () => false });
// 2. Add realistic plugins
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
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
// @ts-ignore
if (!window.chrome) { (window as any).chrome = {}; }
// @ts-ignore
if (!(window as any).chrome.runtime) { (window as any).chrome.runtime = {}; }
}
/**
* Enhanced stealth script basic + WebGL, Canvas, screen, sec-ch-ua overrides.
*/
function _getEnhancedStealthScript(): void {
// --- Basic stealth (same as above) ---
Object.defineProperty(navigator, 'webdriver', { get: () => false });
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: '' },
],
});
Object.defineProperty(navigator, 'languages', { get: () => ['en-US', 'en', 'de'] });
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);
};
// @ts-ignore
if (!window.chrome) { (window as any).chrome = {}; }
// @ts-ignore
if (!(window as any).chrome.runtime) { (window as any).chrome.runtime = {}; }
// --- Enhanced: Screen dimensions (headless reports 0 for outer dimensions) ---
Object.defineProperty(window, 'outerWidth', { get: () => 1280 });
Object.defineProperty(window, 'outerHeight', { get: () => 720 });
Object.defineProperty(screen, 'availWidth', { get: () => 1920 });
Object.defineProperty(screen, 'availHeight', { get: () => 1040 });
Object.defineProperty(screen, 'colorDepth', { get: () => 24 });
// --- Enhanced: WebGL vendor/renderer spoofing ---
const _getParameterOriginal = WebGLRenderingContext.prototype.getParameter;
WebGLRenderingContext.prototype.getParameter = function (parameter: number) {
// UNMASKED_VENDOR_WEBGL
if (parameter === 0x9245) return 'Google Inc. (NVIDIA)';
// UNMASKED_RENDERER_WEBGL
if (parameter === 0x9246) return 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1650 Direct3D11 vs_5_0 ps_5_0, D3D11)';
return _getParameterOriginal.call(this, parameter);
};
// Also override for WebGL2
if (typeof WebGL2RenderingContext !== 'undefined') {
const _getParameter2Original = WebGL2RenderingContext.prototype.getParameter;
WebGL2RenderingContext.prototype.getParameter = function (parameter: number) {
if (parameter === 0x9245) return 'Google Inc. (NVIDIA)';
if (parameter === 0x9246) return 'ANGLE (NVIDIA, NVIDIA GeForce GTX 1650 Direct3D11 vs_5_0 ps_5_0, D3D11)';
return _getParameter2Original.call(this, parameter);
};
}
// --- Enhanced: Canvas fingerprint noise ---
const _toDataURLOriginal = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function (type?: string, quality?: any) {
const ctx = this.getContext('2d');
if (ctx && this.width > 0 && this.height > 0) {
const imageData = ctx.getImageData(0, 0, 1, 1);
// Add minimal noise to a single pixel to alter the fingerprint
imageData.data[0] = (imageData.data[0] + 1) % 256;
ctx.putImageData(imageData, 0, 0);
}
return _toDataURLOriginal.call(this, type, quality);
};
// --- Enhanced: Override navigator.connection (missing in headless) ---
if (!(navigator as any).connection) {
Object.defineProperty(navigator, 'connection', {
get: () => ({
effectiveType: '4g',
rtt: 50,
downlink: 10,
saveData: false,
}),
});
}
// --- Enhanced: Override navigator.hardwareConcurrency ---
Object.defineProperty(navigator, 'hardwareConcurrency', { get: () => 8 });
// --- Enhanced: Override navigator.deviceMemory ---
Object.defineProperty(navigator, 'deviceMemory', { get: () => 8 });
}
// ============================================================================
// URL RESOLUTION
// ============================================================================
/**
* Resolve the meeting URL, optionally adding anon=true.
*/
async function _resolveMeetingUrl(meetingUrl: string, withAnon: boolean): Promise<string> {
const trimmed = meetingUrl.trim();
try {
const response = await fetch(trimmed, { redirect: 'follow' });
const resolvedUrlStr = response.url;
const resolvedUrl = new URL(resolvedUrlStr);
resolvedUrl.searchParams.set('msLaunch', 'false');
resolvedUrl.searchParams.set('type', 'meetup-join');
resolvedUrl.searchParams.set('directDl', 'true');
resolvedUrl.searchParams.set('enableMobilePage', 'true');
resolvedUrl.searchParams.set('suppressPrompt', 'true');
if (withAnon) {
resolvedUrl.searchParams.set('anon', 'true');
}
return resolvedUrl.toString();
} catch {
// Fallback: add params directly
const separator = trimmed.includes('?') ? '&' : '?';
const anonParam = withAnon ? '&anon=true' : '';
return `${trimmed}${separator}msLaunch=false&suppressPrompt=true&directDl=true&enableMobilePage=true${anonParam}`;
}
}
// ============================================================================
// LAUNCHER HANDLING
// ============================================================================
/**
* Handle the Teams launcher dialog ("Continue on this browser").
*/
async function _handleLauncher(page: Page): Promise<void> {
const launcherSelectors = [
'button[data-tid="joinOnWeb"]',
'button:has-text("Continue on this browser")',
'button:has-text("Join on the web instead")',
'a:has-text("Continue on this browser")',
'button:has-text("Use web app instead")',
];
for (const selector of launcherSelectors) {
try {
const button = await page.waitForSelector(selector, { timeout: 10000, state: 'visible' });
if (button) {
await button.click();
logger.info(`[AuthTest] Clicked launcher: ${selector}`);
await page.waitForTimeout(3000);
return;
}
} catch {
// Continue to next selector
}
}
logger.info('[AuthTest] No launcher dialog found — may already be on pre-join page');
}
// ============================================================================
// PAGE TYPE DETECTION
// ============================================================================
interface PageDetection {
pageType: 'v2' | 'lightMeetings' | 'error' | 'unknown';
hasSignInLink: boolean;
hasNameInput: boolean;
hasJoinButton: boolean;
signals: string[];
}
/**
* Detect whether Teams served the /v2/ (authenticated) or light-meetings (anonymous) page.
*/
async function _detectPageType(page: Page): Promise<PageDetection> {
const signals: string[] = [];
const url = page.url().toLowerCase();
// URL-based detection
let pageType: 'v2' | 'lightMeetings' | 'error' | 'unknown' = 'unknown';
if (url.includes('/v2/')) {
pageType = 'v2';
signals.push('URL contains /v2/');
} else if (url.includes('light-meetings') || url.includes('lightmeetings')) {
pageType = 'lightMeetings';
signals.push('URL contains light-meetings');
} else if (url.includes('teams.microsoft.com')) {
signals.push('URL is on teams.microsoft.com');
}
if (url.includes('anon=true')) {
signals.push('URL has anon=true');
}
// DOM-based detection
const domDetection = await page.evaluate(() => {
const result = {
hasNameInput: false,
hasSignInLink: false,
hasJoinButton: false,
hasV2Indicators: false,
hasLightMeetingsIndicators: false,
bodyTextSnippet: '',
title: document.title,
};
// Name input (only present on light-meetings / anonymous page)
const nameInput = document.querySelector('input[placeholder="Type your name"]') ||
document.querySelector('input[data-tid="prejoin-display-name-input"]') ||
document.querySelector('input[placeholder*="name" i]');
result.hasNameInput = !!nameInput;
// Sign-in link (present on light-meetings, allows switching to auth)
const signInLink = document.querySelector('button[data-tid="auth-sign-in-link"]') ||
document.querySelector('a[data-tid="auth-sign-in-link"]');
result.hasSignInLink = !!signInLink;
// Join button
const joinButton = document.querySelector('#prejoin-join-button') ||
document.querySelector('button[data-tid="prejoin-join-button"]') ||
document.querySelector('button');
// Check if any button has "Join" text
const allButtons = document.querySelectorAll('button');
let hasJoinText = false;
allButtons.forEach(btn => {
if (btn.textContent?.includes('Join')) hasJoinText = true;
});
result.hasJoinButton = !!joinButton || hasJoinText;
// v2 indicators
const v2Indicators = [
document.querySelector('[data-tid="prejoin-join-button"]'),
document.querySelector('[data-tid="prejoin-screen"]'),
];
result.hasV2Indicators = v2Indicators.some(el => el !== null);
// light-meetings indicators
const bodyText = document.body?.innerText || '';
result.bodyTextSnippet = bodyText.substring(0, 300);
result.hasLightMeetingsIndicators =
bodyText.includes('Type your name') ||
bodyText.includes('Join as a guest') ||
bodyText.includes('Geben Sie Ihren Namen ein');
return result;
});
if (domDetection.hasNameInput) signals.push('Name input found (anonymous page)');
if (domDetection.hasSignInLink) signals.push('Sign-in link found');
if (domDetection.hasJoinButton) signals.push('Join button found');
if (domDetection.hasV2Indicators) signals.push('/v2/ DOM indicators found');
if (domDetection.hasLightMeetingsIndicators) signals.push('light-meetings DOM indicators found');
if (domDetection.title) signals.push(`Page title: ${domDetection.title}`);
// Refine page type based on DOM if URL detection was inconclusive
if (pageType === 'unknown') {
if (domDetection.hasLightMeetingsIndicators || domDetection.hasNameInput) {
pageType = 'lightMeetings';
} else if (domDetection.hasV2Indicators) {
pageType = 'v2';
}
}
return {
pageType,
hasSignInLink: domDetection.hasSignInLink,
hasNameInput: domDetection.hasNameInput,
hasJoinButton: domDetection.hasJoinButton,
signals,
};
}
// ============================================================================
// AUTH ATTEMPT (VARIANT 5 ONLY)
// ============================================================================
/**
* Attempt authentication using the existing AuthProcedure.
* Clicks "Sign in" on the pre-join page, then handles the hybrid auth flow.
*/
async function _attemptAuth(page: Page, email: string, password: string): Promise<boolean> {
try {
// Click "Sign in" link on the pre-join page
const signInSelectors = [
'button[data-tid="auth-sign-in-link"]',
'a[data-tid="auth-sign-in-link"]',
'button:has-text("Sign in")',
'a:has-text("Sign in")',
'button:has-text("Anmelden")',
];
let clicked = false;
for (const selector of signInSelectors) {
try {
const el = await page.$(selector);
if (el) {
await el.click();
logger.info(`[AuthTest] Clicked sign-in link: ${selector}`);
clicked = true;
await page.waitForTimeout(3000);
break;
}
} catch {
// Continue
}
}
if (!clicked) {
logger.warn('[AuthTest] No sign-in link found on page');
return false;
}
// Use AuthProcedure for the hybrid flow (inline modal → redirect → password)
const authProcedure = new AuthProcedure(page, logger);
const authResult = await authProcedure.authenticateWithMicrosoft(email, password, true);
// Wait for redirect back to Teams after auth
if (authResult) {
await page.waitForTimeout(5000);
}
return authResult;
} catch (err) {
logger.error(`[AuthTest] Auth attempt failed: ${err}`);
return false;
}
}
// ============================================================================
// SCREENSHOT
// ============================================================================
/**
* Take a JPEG screenshot and return as base64.
*/
async function _takeScreenshot(page: Page): Promise<string | undefined> {
try {
const buffer = await page.screenshot({
type: 'jpeg',
quality: _SCREENSHOT_QUALITY,
fullPage: false,
});
return buffer.toString('base64');
} catch {
return undefined;
}
}
// ============================================================================
// RESULT HELPERS
// ============================================================================
function _createSkippedResult(variant: AuthTestVariant, reason: string): AuthTestResult {
return {
variantId: variant.id,
variantName: variant.name,
success: false,
pageType: 'error',
finalUrl: '',
hasSignInLink: false,
hasNameInput: false,
hasJoinButton: false,
authAttempted: false,
authSuccess: null,
durationMs: 0,
error: `Uebersprungen: ${reason}`,
detectedSignals: [],
};
}
function _createErrorResult(variant: AuthTestVariant, error: string): AuthTestResult {
return {
variantId: variant.id,
variantName: variant.name,
success: false,
pageType: 'error',
finalUrl: '',
hasSignInLink: false,
hasNameInput: false,
hasJoinButton: false,
authAttempted: false,
authSuccess: null,
durationMs: 0,
error,
detectedSignals: [],
};
}
// ============================================================================
// RECOMMENDATION ENGINE
// ============================================================================
/**
* Generate a recommendation based on test results.
*/
function _generateRecommendation(results: AuthTestResult[]): string {
const v2Variants = results.filter(r => r.pageType === 'v2');
const authSuccess = results.find(r => r.authAttempted && r.authSuccess === true);
if (authSuccess) {
return `Variante "${authSuccess.variantName}" hat Auth erfolgreich durchgefuehrt! ` +
`Auth-Flow kann mit dieser Konfiguration aktiviert werden.`;
}
if (v2Variants.length > 0) {
const names = v2Variants.map(v => `"${v.variantName}"`).join(', ');
return `${v2Variants.length} Variante(n) haben /v2/ erhalten: ${names}. ` +
`Auth ist prinzipiell moeglich — Auth-Flow kann re-aktiviert werden.`;
}
const successVariants = results.filter(r => r.success);
if (successVariants.every(r => r.pageType === 'lightMeetings')) {
return 'Alle Varianten wurden auf light-meetings umgeleitet. ' +
'Teams blockiert den authentifizierten Join fuer alle getesteten Browser-Konfigurationen. ' +
'Weitere Optionen: Graph API / Bot Framework oder andere Stealth-Ansaetze evaluieren.';
}
return 'Test abgeschlossen. Ergebnisse pruefen und naechste Schritte planen.';
}

View file

@ -2,6 +2,7 @@ import express, { Express, Request, Response } from 'express';
import { Server } from 'http';
import { logger } from '../utils/logger';
import { config } from '../config';
import { runAuthTests } from '../bot/authTestProcedure';
export interface HttpServerCallbacks {
onJoinRequest: (sessionId: string, meetingUrl: string, botName?: string, instanceId?: string, gatewayWsUrl?: string, language?: string, botAccountEmail?: string, botAccountPassword?: string, backgroundImageUrl?: string) => Promise<void>;
@ -136,5 +137,25 @@ export class HttpServer {
// This would need access to the session manager
res.json({ message: 'Not implemented' });
});
// Run auth detection tests against a Teams meeting URL
this._app.post('/api/bot/test-auth', async (req: Request, res: Response) => {
try {
const { meetingUrl, botAccountEmail, botAccountPassword } = req.body;
if (!meetingUrl) {
res.status(400).json({ error: 'Missing required field: meetingUrl' });
return;
}
logger.info(`Starting auth detection tests for: ${meetingUrl}`);
const results = await runAuthTests(meetingUrl, botAccountEmail, botAccountPassword);
res.json(results);
} catch (error) {
logger.error('Error running auth tests:', error);
res.status(500).json({ error: (error as Error).message });
}
});
}
}