feat: anonymous fallback when authenticated join fails (lobby timeout, external tenant)

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
ValueOn AG 2026-02-15 12:50:04 +01:00
parent 1db83a805e
commit c420987dcb

View file

@ -91,7 +91,7 @@ export class BotOrchestrator {
throw new Error(`Invalid meeting URL: ${this._meetingUrl}`); 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 { try {
this._setState('launching'); this._setState('launching');
@ -99,11 +99,40 @@ export class BotOrchestrator {
// Connect to Gateway WebSocket first // Connect to Gateway WebSocket first
await this._connectToGateway(); await this._connectToGateway();
// 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');
throw error;
}
}
/**
* Attempt to join a meeting (authenticated or anonymous).
*/
private async _attemptJoin(authenticate: boolean): Promise<void> {
// Launch browser // Launch browser
await this._launchBrowser(); await this._launchBrowser();
// Authenticate with Microsoft if bot account is configured // Authenticate with Microsoft if requested
if (isAuthenticated) { if (authenticate) {
const { AuthProcedure } = await import('./authProcedure'); const { AuthProcedure } = await import('./authProcedure');
const authProcedure = new AuthProcedure(this._page!, this._logger); const authProcedure = new AuthProcedure(this._page!, this._logger);
const authSuccess = await authProcedure.authenticateWithMicrosoft( const authSuccess = await authProcedure.authenticateWithMicrosoft(
@ -111,17 +140,20 @@ export class BotOrchestrator {
this._options.botAccountPassword! this._options.botAccountPassword!
); );
if (!authSuccess) { if (!authSuccess) {
this._logger.warn('Microsoft authentication failed - falling back to anonymous join'); 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'); this._setState('navigating');
// Navigate to meeting and handle launcher // Navigate to meeting and handle launcher
await this._joinProcedure!.startMeetingLauncherFlow(this._meetingUrl); await this._joinProcedure.startMeetingLauncherFlow(this._meetingUrl);
// Set virtual background if configured (must be done on pre-join screen, before "Join now") // Set virtual background if configured (must be done on pre-join screen, before "Join now")
if (this._options.backgroundImageUrl && this._page) { if (this._options.backgroundImageUrl && this._page && authenticate) {
try { try {
const { BackgroundProcedure } = await import('./backgroundProcedure'); const { BackgroundProcedure } = await import('./backgroundProcedure');
const bgProcedure = new BackgroundProcedure(this._page, this._logger); const bgProcedure = new BackgroundProcedure(this._page, this._logger);
@ -131,11 +163,11 @@ export class BotOrchestrator {
} }
} }
// Join the meeting (enter lobby for anonymous, direct join for authenticated) // Join the meeting
await this._joinProcedure!.joinMeetingLobbyFlow(); await this._joinProcedure.joinMeetingLobbyFlow();
// Check if we're in lobby // Check if we're in lobby
const inLobby = await this._joinProcedure!.isInMeetingLobby({ waitForSeconds: 10 }); const inLobby = await this._joinProcedure.isInMeetingLobby({ waitForSeconds: 10 });
if (inLobby) { if (inLobby) {
this._setState('in_lobby'); this._setState('in_lobby');
this._logger.info('Bot is in lobby, waiting to be admitted...'); this._logger.info('Bot is in lobby, waiting to be admitted...');
@ -145,19 +177,31 @@ export class BotOrchestrator {
await this._waitForMeetingAdmission(); await this._waitForMeetingAdmission();
this._setState('in_meeting'); this._setState('in_meeting');
this._logger.info('Bot joined the meeting!'); this._logger.info(`Bot joined the meeting! (authenticated: ${authenticate})`);
// Initialize audio // Initialize audio
await this._audioProcedure!.initialize(); await this._audioProcedure!.initialize();
// Enable and subscribe to captions // Enable and subscribe to captions
await this._enableCaptions(); await this._enableCaptions();
}
} catch (error) { /**
this._logger.error('Error starting bot:', error); * Clean up browser for retry (close browser without full shutdown).
this._setState('error', (error as Error).message); */
await this._takeScreenshot('error'); private async _cleanup(): Promise<void> {
throw error; 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
} }
} }