From c420987dcb778d65bd4c2c4d7bf2c6e47f4b79e6 Mon Sep 17 00:00:00 2001
From: ValueOn AG
Date: Sun, 15 Feb 2026 12:50:04 +0100
Subject: [PATCH] feat: anonymous fallback when authenticated join fails (lobby
timeout, external tenant)
Co-authored-by: Cursor
---
src/bot/orchestrator.ts | 152 ++++++++++++++++++++++++++--------------
1 file changed, 98 insertions(+), 54 deletions(-)
diff --git a/src/bot/orchestrator.ts b/src/bot/orchestrator.ts
index dd8deec..a704bb7 100644
--- a/src/bot/orchestrator.ts
+++ b/src/bot/orchestrator.ts
@@ -91,7 +91,7 @@ export class BotOrchestrator {
throw new Error(`Invalid meeting URL: ${this._meetingUrl}`);
}
- const isAuthenticated = !!(this._options.botAccountEmail && this._options.botAccountPassword);
+ let useAuthentication = !!(this._options.botAccountEmail && this._options.botAccountPassword);
try {
this._setState('launching');
@@ -99,61 +99,24 @@ export class BotOrchestrator {
// Connect to Gateway WebSocket first
await this._connectToGateway();
- // Launch browser
- await this._launchBrowser();
-
- // Authenticate with Microsoft if bot account is configured
- if (isAuthenticated) {
- const { AuthProcedure } = await import('./authProcedure');
- const authProcedure = new AuthProcedure(this._page!, this._logger);
- const authSuccess = await authProcedure.authenticateWithMicrosoft(
- this._options.botAccountEmail!,
- this._options.botAccountPassword!
- );
- if (!authSuccess) {
- this._logger.warn('Microsoft authentication failed - falling back to anonymous join');
- }
- }
-
- this._setState('navigating');
-
- // Navigate to meeting and handle launcher
- await this._joinProcedure!.startMeetingLauncherFlow(this._meetingUrl);
-
- // Set virtual background if configured (must be done on pre-join screen, before "Join now")
- if (this._options.backgroundImageUrl && this._page) {
- try {
- const { BackgroundProcedure } = await import('./backgroundProcedure');
- const bgProcedure = new BackgroundProcedure(this._page, this._logger);
- await bgProcedure.setBackgroundFromUrl(this._options.backgroundImageUrl);
- } catch (error) {
- this._logger.warn(`Background image setup failed (non-fatal): ${error}`);
- }
- }
-
- // Join the meeting (enter lobby for anonymous, direct join for authenticated)
- await this._joinProcedure!.joinMeetingLobbyFlow();
-
- // Check if we're in lobby
- const inLobby = await this._joinProcedure!.isInMeetingLobby({ waitForSeconds: 10 });
- if (inLobby) {
- this._setState('in_lobby');
- this._logger.info('Bot is in lobby, waiting to be admitted...');
- }
-
- // Wait to be admitted to the meeting
- await this._waitForMeetingAdmission();
-
- this._setState('in_meeting');
- this._logger.info('Bot joined the meeting!');
-
- // Initialize audio
- await this._audioProcedure!.initialize();
-
- // Enable and subscribe to captions
- await this._enableCaptions();
+ // Try joining (authenticated first, then anonymous fallback)
+ await this._attemptJoin(useAuthentication);
} catch (error) {
+ // If authenticated join failed, retry as anonymous
+ if (useAuthentication) {
+ this._logger.warn(`Authenticated join failed: ${(error as Error).message}. Retrying as anonymous guest...`);
+ try {
+ await this._cleanup();
+ await this._attemptJoin(false);
+ return;
+ } catch (retryError) {
+ this._logger.error('Anonymous fallback also failed:', retryError);
+ this._setState('error', (retryError as Error).message);
+ await this._takeScreenshot('error-fallback');
+ throw retryError;
+ }
+ }
this._logger.error('Error starting bot:', error);
this._setState('error', (error as Error).message);
await this._takeScreenshot('error');
@@ -161,6 +124,87 @@ export class BotOrchestrator {
}
}
+ /**
+ * Attempt to join a meeting (authenticated or anonymous).
+ */
+ private async _attemptJoin(authenticate: boolean): Promise {
+ // Launch browser
+ await this._launchBrowser();
+
+ // Authenticate with Microsoft if requested
+ if (authenticate) {
+ const { AuthProcedure } = await import('./authProcedure');
+ const authProcedure = new AuthProcedure(this._page!, this._logger);
+ const authSuccess = await authProcedure.authenticateWithMicrosoft(
+ this._options.botAccountEmail!,
+ this._options.botAccountPassword!
+ );
+ if (!authSuccess) {
+ throw new Error('Microsoft authentication failed');
+ }
+ }
+
+ // Update JoinProcedure with correct auth state
+ this._joinProcedure = new JoinProcedure(this._page!, this._logger, this._botName, authenticate);
+
+ this._setState('navigating');
+
+ // Navigate to meeting and handle launcher
+ await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl);
+
+ // Set virtual background if configured (must be done on pre-join screen, before "Join now")
+ if (this._options.backgroundImageUrl && this._page && authenticate) {
+ try {
+ const { BackgroundProcedure } = await import('./backgroundProcedure');
+ const bgProcedure = new BackgroundProcedure(this._page, this._logger);
+ await bgProcedure.setBackgroundFromUrl(this._options.backgroundImageUrl);
+ } catch (error) {
+ this._logger.warn(`Background image setup failed (non-fatal): ${error}`);
+ }
+ }
+
+ // Join the meeting
+ await this._joinProcedure.joinMeetingLobbyFlow();
+
+ // Check if we're in lobby
+ const inLobby = await this._joinProcedure.isInMeetingLobby({ waitForSeconds: 10 });
+ if (inLobby) {
+ this._setState('in_lobby');
+ this._logger.info('Bot is in lobby, waiting to be admitted...');
+ }
+
+ // Wait to be admitted to the meeting
+ await this._waitForMeetingAdmission();
+
+ this._setState('in_meeting');
+ this._logger.info(`Bot joined the meeting! (authenticated: ${authenticate})`);
+
+ // Initialize audio
+ await this._audioProcedure!.initialize();
+
+ // Enable and subscribe to captions
+ await this._enableCaptions();
+ }
+
+ /**
+ * Clean up browser for retry (close browser without full shutdown).
+ */
+ private async _cleanup(): Promise {
+ try {
+ if (this._page) await this._page.close().catch(() => {});
+ if (this._context) await this._context.close().catch(() => {});
+ if (this._browser) await this._browser.close().catch(() => {});
+ this._page = null;
+ this._context = null;
+ this._browser = null;
+ this._joinProcedure = null;
+ this._captionsProcedure = null;
+ this._audioProcedure = null;
+ } catch {
+ // Ignore cleanup errors
+ }
+ }
+
/**
* Connect to the Gateway WebSocket for this session.
*/