This commit is contained in:
jjbrunton committed 2026-01-05 15:06:58 +00:00
1 parent 9602403f8a
commit cad1f7b842
1 file changed
+87 -4
+87 -4
View File
@@ -6,6 +6,7 @@ import fs from 'fs';
export class BrowserClient {
private context: BrowserContext | null = null;
private sessionDir: string | null = null;
constructor(
private config: Config,
@@ -14,6 +15,7 @@ export class BrowserClient {
async initialize(): Promise<BrowserContext> {
const sessionDir = path.resolve(process.cwd(), this.config.browser.sessionDir);
this.sessionDir = sessionDir;
// Ensure session directory exists
if (!fs.existsSync(sessionDir)) {
@@ -171,6 +173,26 @@ export class BrowserClient {
page.waitForURL((url) => !url.toString().includes('/login'), { timeout: 20000 }).catch(() => undefined),
]);
// Give cookies/time to settle
await page.waitForTimeout(2000);
const obstacleInfo = await this.detectLoginObstacles(page);
if (obstacleInfo.twoFactor) {
this.logger.error('Login requires 2FA. Please run "npm run login" locally to complete it once.');
await this.captureLoginDebug(page, 'login-2fa');
return false;
}
if (obstacleInfo.captcha) {
this.logger.error('Login blocked by CAPTCHA. Please run "npm run login" locally to complete it once.');
await this.captureLoginDebug(page, 'login-captcha');
return false;
}
if (obstacleInfo.errorText) {
this.logger.error(`Login failed with error: ${obstacleInfo.errorText}`);
await this.captureLoginDebug(page, 'login-error');
return false;
}
// Confirm auth via UI or API check
const isValid = await this.isSessionValid();
if (isValid) {
@@ -179,10 +201,7 @@ export class BrowserClient {
}
// Check for error messages
const errorContent = await page.evaluate(() => {
const err = document.querySelector('[role="alert"], .AnimatedForm__errorMessage');
return err ? err.textContent : null;
});
const errorContent = obstacleInfo.errorText;
if (errorContent) {
this.logger.error(`Login failed with error: ${errorContent}`);
@@ -190,6 +209,7 @@ export class BrowserClient {
this.logger.error('Login flow finished but session is invalid.');
}
await this.captureLoginDebug(page, 'login-invalid-session');
return false;
} catch (error) {
@@ -197,4 +217,67 @@ export class BrowserClient {
return false;
}
}
private async detectLoginObstacles(page: Page): Promise<{
twoFactor: boolean;
captcha: boolean;
errorText: string | null;
}> {
try {
const info = await page.evaluate(() => {
const errorCandidates = Array.from(
document.querySelectorAll('[role="alert"], .AnimatedForm__errorMessage, [data-testid*="error"], [id*="error"]')
)
.map((el) => el.textContent?.trim())
.filter((text) => text && text.length > 0) as string[];
const otpInput = document.querySelector('input[name="otp"], input[autocomplete="one-time-code"], input[type="tel"][maxlength="6"]');
const captchaElement = document.querySelector('iframe[src*="captcha"], iframe[src*="recaptcha"], [data-sitekey], #captcha, .captcha');
return {
errorText: errorCandidates[0] ?? null,
hasOtp: Boolean(otpInput),
hasCaptcha: Boolean(captchaElement),
};
});
const frameCaptcha = page.frames().some((frame) => /captcha|recaptcha/i.test(frame.url()));
return {
twoFactor: info.hasOtp,
captcha: info.hasCaptcha || frameCaptcha,
errorText: info.errorText,
};
} catch {
return { twoFactor: false, captcha: false, errorText: null };
}
}
private async captureLoginDebug(page: Page, label: string): Promise<void> {
try {
const baseDir = this.sessionDir ?? path.resolve(process.cwd(), 'data');
if (!fs.existsSync(baseDir)) {
fs.mkdirSync(baseDir, { recursive: true });
}
const safeTimestamp = new Date().toISOString().replace(/[:.]/g, '-');
const screenshotPath = path.join(baseDir, `${label}-${safeTimestamp}.png`);
const htmlPath = path.join(baseDir, `${label}-${safeTimestamp}.html`);
await page.screenshot({ path: screenshotPath, fullPage: true }).catch(() => undefined);
const html = await page.content().catch(() => '');
if (html) {
fs.writeFileSync(htmlPath, html, 'utf-8');
}
this.logger.info(`Saved login debug artifacts to ${screenshotPath} and ${htmlPath}`);
this.logger.info(`Login debug URL: ${page.url()}`);
const title = await page.title().catch(() => '');
if (title) {
this.logger.info(`Login debug title: ${title}`);
}
} catch (error) {
this.logger.warn('Failed to capture login debug artifacts:', error);
}
}
}