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}`);
}
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<void> {
// 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<void> {
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.
*/