Skip to main content
Glama
auth-manager.ts38.8 kB
/** * Authentication Manager for NotebookLM * * Handles: * - Interactive login (headful browser for setup) * - Auto-login with credentials (email/password from ENV) * - Browser state persistence (cookies + localStorage + sessionStorage) * - Cookie expiry validation * - State expiry checks (cookie-based + 7-day file age fallback) * - Hard reset for clean start * * Based on the Python implementation from auth.py */ import type { BrowserContext, Page, ElementHandle } from 'patchright'; import fs from 'fs/promises'; import { existsSync } from 'fs'; import path from 'path'; import { CONFIG, NOTEBOOKLM_AUTH_URL } from '../config.js'; import { log } from '../utils/logger.js'; import { humanType, randomDelay, realisticClick, randomMouseMovement, } from '../utils/stealth-utils.js'; import type { ProgressCallback } from '../types.js'; /** * Critical cookie names for Google authentication */ const CRITICAL_COOKIE_NAMES = [ 'SID', 'HSID', 'SSID', // Google session 'APISID', 'SAPISID', // API auth 'OSID', '__Secure-OSID', // NotebookLM-specific '__Secure-1PSID', '__Secure-3PSID', // Secure variants ]; export class AuthManager { private stateFilePath: string; private sessionFilePath: string; constructor() { this.stateFilePath = path.join(CONFIG.browserStateDir, 'state.json'); this.sessionFilePath = path.join(CONFIG.browserStateDir, 'session.json'); } // ============================================================================ // Browser State Management // ============================================================================ /** * Save entire browser state (cookies + localStorage) */ async saveBrowserState(context: BrowserContext, page?: Page): Promise<boolean> { try { // Save storage state (cookies + localStorage + IndexedDB) await context.storageState({ path: this.stateFilePath }); // Also save sessionStorage if page is provided if (page) { try { const sessionStorageData: string = await page.evaluate((): string => { // Properly extract sessionStorage as a plain object const storage: Record<string, string> = {}; // @ts-expect-error - sessionStorage exists in browser context for (let i = 0; i < sessionStorage.length; i++) { // @ts-expect-error - sessionStorage exists in browser context const key = sessionStorage.key(i); if (key) { // @ts-expect-error - sessionStorage exists in browser context storage[key] = sessionStorage.getItem(key) || ''; } } return JSON.stringify(storage); }); await fs.writeFile(this.sessionFilePath, sessionStorageData, { encoding: 'utf-8', }); const entries = Object.keys(JSON.parse(sessionStorageData)).length; log.success(`✅ Browser state saved (incl. sessionStorage: ${entries} entries)`); } catch (error) { log.warning(`⚠️ State saved, but sessionStorage failed: ${error}`); } } else { log.success('✅ Browser state saved'); } return true; } catch (error) { log.error(`❌ Failed to save browser state: ${error}`); return false; } } /** * Check if saved browser state exists */ async hasSavedState(): Promise<boolean> { try { await fs.access(this.stateFilePath); return true; } catch { return false; } } /** * Get path to saved browser state */ getStatePath(): string | null { // Synchronous check using imported existsSync if (existsSync(this.stateFilePath)) { return this.stateFilePath; } return null; } /** * Get valid state path (checks expiry) */ async getValidStatePath(): Promise<string | null> { const statePath = this.getStatePath(); if (!statePath) { return null; } if (await this.isStateExpired()) { log.warning('⚠️ Saved state is expired (cookies invalid or file too old)'); log.info('💡 Run setup_auth tool to re-authenticate'); return null; } return statePath; } /** * Load sessionStorage from file */ async loadSessionStorage(): Promise<Record<string, string> | null> { try { const data = await fs.readFile(this.sessionFilePath, { encoding: 'utf-8' }); const sessionData = JSON.parse(data); log.success(`✅ Loaded sessionStorage (${Object.keys(sessionData).length} entries)`); return sessionData; } catch (error) { log.warning(`⚠️ Failed to load sessionStorage: ${error}`); return null; } } // ============================================================================ // Cookie Validation // ============================================================================ /** * Validate if saved state is still valid */ async validateState(context: BrowserContext): Promise<boolean> { try { const cookies = await context.cookies(); if (cookies.length === 0) { log.warning('⚠️ No cookies found in state'); return false; } // Check for Google auth cookies const googleCookies = cookies.filter((c) => c.domain.includes('google.com')); if (googleCookies.length === 0) { log.warning('⚠️ No Google cookies found'); return false; } // Check if important cookies are expired const currentTime = Date.now() / 1000; for (const cookie of googleCookies) { const expires = cookie.expires ?? -1; if (expires !== -1 && expires < currentTime) { log.warning(`⚠️ Cookie '${cookie.name}' has expired`); return false; } } log.success('✅ State validation passed'); return true; } catch (error) { log.warning(`⚠️ State validation failed: ${error}`); return false; } } /** * Validate if critical authentication cookies are still valid */ async validateCookiesExpiry(context: BrowserContext): Promise<boolean> { try { const cookies = await context.cookies(); if (cookies.length === 0) { log.warning('⚠️ No cookies found'); return false; } // Find critical cookies const criticalCookies = cookies.filter((c) => CRITICAL_COOKIE_NAMES.includes(c.name)); if (criticalCookies.length === 0) { log.warning('⚠️ No critical auth cookies found'); return false; } // Check expiration for each critical cookie const currentTime = Date.now() / 1000; const expiredCookies: string[] = []; for (const cookie of criticalCookies) { const expires = cookie.expires ?? -1; // -1 means session cookie (valid until browser closes) if (expires === -1) { continue; } // Check if cookie is expired if (expires < currentTime) { expiredCookies.push(cookie.name); } } if (expiredCookies.length > 0) { log.warning(`⚠️ Expired cookies: ${expiredCookies.join(', ')}`); return false; } log.success(`✅ All ${criticalCookies.length} critical cookies are valid`); return true; } catch (error) { log.warning(`⚠️ Cookie validation failed: ${error}`); return false; } } /** * Validate cookies directly from the state.json file (without browser context) * Returns true if critical cookies exist and are not expired */ async validateCookiesFromFile(): Promise<boolean> { try { const stateData = await fs.readFile(this.stateFilePath, { encoding: 'utf-8' }); const state = JSON.parse(stateData); if (!state.cookies || state.cookies.length === 0) { log.warning('⚠️ No cookies found in state file'); return false; } // Find critical cookies const criticalCookies = state.cookies.filter((c: { name: string }) => CRITICAL_COOKIE_NAMES.includes(c.name) ); if (criticalCookies.length === 0) { log.warning('⚠️ No critical auth cookies found in state file'); return false; } // Check expiration for each critical cookie const currentTime = Date.now() / 1000; const expiredCookies: string[] = []; for (const cookie of criticalCookies) { const expires = cookie.expires ?? -1; // -1 means session cookie (valid until browser closes) if (expires === -1) { continue; } // Check if cookie is expired if (expires < currentTime) { expiredCookies.push(cookie.name); } } if (expiredCookies.length > 0) { log.warning(`⚠️ Expired cookies in state file: ${expiredCookies.join(', ')}`); return false; } log.success(`✅ State file cookies valid (${criticalCookies.length} critical cookies)`); return true; } catch (error) { log.warning(`⚠️ Failed to validate cookies from file: ${error}`); return false; } } /** * Check if the saved state is expired * Primary: validates cookies are not expired * Secondary: checks file age as safety fallback (7 days max) */ async isStateExpired(): Promise<boolean> { try { // First, check if file exists const stats = await fs.stat(this.stateFilePath); // Primary check: validate cookies in the file are not expired const cookiesValid = await this.validateCookiesFromFile(); if (!cookiesValid) { log.warning('⚠️ State expired: cookies are invalid or expired'); return true; } // Secondary check: file age as safety fallback (7 days max) const fileAgeSeconds = (Date.now() - stats.mtimeMs) / 1000; const maxAgeSeconds = 7 * 24 * 60 * 60; // 7 days (increased from 24h) if (fileAgeSeconds > maxAgeSeconds) { const daysOld = fileAgeSeconds / (24 * 3600); log.warning(`⚠️ State file is ${daysOld.toFixed(1)} days old (max: 7 days)`); return true; } return false; } catch { return true; // File doesn't exist = expired } } // ============================================================================ // Interactive Login // ============================================================================ /** * Perform interactive login * User will see a browser window and login manually * * SIMPLE & RELIABLE: Just wait for URL to change to notebooklm.google.com */ async performLogin(page: Page, sendProgress?: ProgressCallback): Promise<boolean> { try { log.info('🌐 Opening Google login page...'); log.warning('📝 Please login to your Google account'); log.warning('⏳ Browser will close automatically once you reach NotebookLM'); log.info(''); // Progress: Navigating await sendProgress?.('Navigating to Google login...', 3, 10); // Navigate to Google login (redirects to NotebookLM after auth) await page.goto(NOTEBOOKLM_AUTH_URL, { timeout: 60000 }); // Progress: Waiting for login await sendProgress?.('Waiting for manual login (up to 10 minutes)...', 4, 10); // Wait for user to complete login log.warning('⏳ Waiting for login (up to 10 minutes)...'); const checkIntervalMs = 1000; // Check every 1 second const maxAttempts = 600; // 10 minutes total let lastProgressUpdate = 0; for (let attempt = 0; attempt < maxAttempts; attempt++) { try { const currentUrl = page.url(); const elapsedSeconds = Math.floor(attempt * (checkIntervalMs / 1000)); // Send progress every 10 seconds if (elapsedSeconds - lastProgressUpdate >= 10) { lastProgressUpdate = elapsedSeconds; const progressStep = Math.min(8, 4 + Math.floor(elapsedSeconds / 60)); await sendProgress?.( `Waiting for login... (${elapsedSeconds}s elapsed)`, progressStep, 10 ); } // ✅ SIMPLE: Check if we're on NotebookLM (any path!) if (currentUrl.startsWith('https://notebooklm.google.com/')) { await sendProgress?.('Login successful! NotebookLM detected!', 9, 10); log.success('✅ Login successful! NotebookLM URL detected.'); log.success(`✅ Current URL: ${currentUrl}`); // ✅ CRITICAL: Wait for page to fully load before saving cookies // NotebookLM needs time to: // 1. Complete the OAuth redirect // 2. Generate session cookies (__Secure-OSID, etc.) // 3. Load the application and establish session log.info('⏳ Waiting for NotebookLM to fully load (10 seconds)...'); await sendProgress?.('Waiting for page to fully load...', 9, 10); // Wait for network to be idle (no more requests for 500ms) try { await page.waitForLoadState('networkidle', { timeout: 15000 }); log.success('✅ Page network is idle'); } catch { log.warning('⚠️ Network idle timeout - continuing anyway'); } // Additional buffer to ensure all cookies are set await page.waitForTimeout(5000); log.success('✅ Page fully loaded, cookies should be set'); return true; } // Still on accounts.google.com - log periodically if (currentUrl.includes('accounts.google.com') && attempt % 30 === 0 && attempt > 0) { log.warning(`⏳ Still waiting... (${elapsedSeconds}s elapsed)`); } await page.waitForTimeout(checkIntervalMs); } catch { await page.waitForTimeout(checkIntervalMs); continue; } } // Timeout reached - final check const currentUrl = page.url(); if (currentUrl.startsWith('https://notebooklm.google.com/')) { await sendProgress?.('Login successful (detected on timeout check)!', 9, 10); log.success('✅ Login successful (detected on timeout check)'); return true; } log.error('❌ Login verification failed - timeout reached'); log.warning(`Current URL: ${currentUrl}`); return false; } catch (error) { log.error(`❌ Login failed: ${error}`); return false; } } // ============================================================================ // Auto-Login with Credentials // ============================================================================ /** * Attempt to authenticate using configured credentials */ async loginWithCredentials( context: BrowserContext, page: Page, email: string, password: string ): Promise<boolean> { const maskedEmail = this.maskEmail(email); log.warning(`🔁 Attempting automatic login for ${maskedEmail}...`); // Log browser visibility if (!CONFIG.headless) { log.info(' 👁️ Browser is VISIBLE for debugging'); } else { log.info(' 🙈 Browser is HEADLESS (invisible)'); } log.info(` 🌐 Navigating to Google login...`); try { await page.goto(NOTEBOOKLM_AUTH_URL, { waitUntil: 'domcontentloaded', timeout: CONFIG.browserTimeout, }); log.success(` ✅ Page loaded: ${page.url().slice(0, 80)}...`); } catch { log.warning(` ⚠️ Page load timeout (continuing anyway)`); } const deadline = Date.now() + CONFIG.autoLoginTimeoutMs; log.info(` ⏰ Auto-login timeout: ${CONFIG.autoLoginTimeoutMs / 1000}s`); // Already on NotebookLM? log.info(' 🔍 Checking if already authenticated...'); if (await this.waitForNotebook(page, CONFIG.autoLoginTimeoutMs)) { log.success('✅ Already authenticated'); await this.saveBrowserState(context, page); return true; } log.warning(' ❌ Not authenticated yet, proceeding with login...'); // Handle possible account chooser log.info(' 🔍 Checking for account chooser...'); if (await this.handleAccountChooser(page, email)) { log.success(' ✅ Account selected from chooser'); if (await this.waitForNotebook(page, CONFIG.autoLoginTimeoutMs)) { log.success('✅ Automatic login successful'); await this.saveBrowserState(context, page); return true; } } // Email step log.info(' 📧 Entering email address...'); if (!(await this.fillIdentifier(page, email))) { if (await this.waitForNotebook(page, CONFIG.autoLoginTimeoutMs)) { log.success('✅ Automatic login successful'); await this.saveBrowserState(context, page); return true; } log.warning('⚠️ Email input not detected'); } // Password step (wait until visible) let waitAttempts = 0; log.warning(' ⏳ Waiting for password page to load...'); while (Date.now() < deadline && !(await this.fillPassword(page, password))) { waitAttempts++; // Log every 10 seconds (20 attempts * 0.5s) if (waitAttempts % 20 === 0) { const secondsWaited = waitAttempts * 0.5; const secondsRemaining = (deadline - Date.now()) / 1000; log.warning( ` ⏳ Still waiting for password field... (${secondsWaited}s elapsed, ${secondsRemaining.toFixed(0)}s remaining)` ); log.info(` 📍 Current URL: ${page.url().slice(0, 100)}`); } if (page.url().includes('challenge')) { log.warning('⚠️ Additional verification required (Google challenge page).'); return false; } await page.waitForTimeout(500); } // Wait for Google redirect after login log.info(' 🔄 Waiting for Google redirect to NotebookLM...'); if (await this.waitForRedirectAfterLogin(page, deadline)) { log.success('✅ Automatic login successful'); await this.saveBrowserState(context, page); return true; } // Login failed - diagnose log.error('❌ Automatic login timed out'); // Take screenshot for debugging try { const screenshotPath = path.join(CONFIG.dataDir, `login_failed_${Date.now()}.png`); await page.screenshot({ path: screenshotPath }); log.info(` 📸 Screenshot saved: ${screenshotPath}`); } catch (error) { log.warning(` ⚠️ Could not save screenshot: ${error}`); } // Diagnose specific failure reason const currentUrl = page.url(); log.warning(' 🔍 Diagnosing failure...'); if (currentUrl.includes('accounts.google.com')) { if (currentUrl.includes('/signin/identifier')) { log.error(' ❌ Still on email page - email input might have failed'); log.info(' 💡 Check if email is correct in .env'); } else if (currentUrl.includes('/challenge')) { log.error(' ❌ Google requires additional verification (2FA, CAPTCHA, suspicious login)'); log.info(' 💡 Try logging in manually first: use setup_auth tool'); } else if (currentUrl.includes('/pwd') || currentUrl.includes('/password')) { log.error(' ❌ Still on password page - password input might have failed'); log.info(' 💡 Check if password is correct in .env'); } else { log.error(` ❌ Stuck on Google accounts page: ${currentUrl.slice(0, 80)}...`); } } else if (currentUrl.includes('notebooklm.google.com')) { log.warning(" ⚠️ Reached NotebookLM but couldn't detect successful login"); log.info(' 💡 This might be a timing issue - try again'); } else { log.error(` ❌ Unexpected page: ${currentUrl.slice(0, 80)}...`); } return false; } // ============================================================================ // Helper Methods // ============================================================================ /** * Wait for Google to redirect to NotebookLM after successful login (SIMPLE & RELIABLE) * * Just checks if URL changes to notebooklm.google.com - no complex UI element searching! * Matches the simplified approach used in performLogin(). */ private async waitForRedirectAfterLogin(page: Page, deadline: number): Promise<boolean> { log.info(' ⏳ Waiting for redirect to NotebookLM...'); while (Date.now() < deadline) { try { const currentUrl = page.url(); // Simple check: Are we on NotebookLM? if (currentUrl.startsWith('https://notebooklm.google.com/')) { log.success(' ✅ NotebookLM URL detected!'); // Short wait to ensure page is loaded await page.waitForTimeout(2000); return true; } } catch { // Ignore errors } await page.waitForTimeout(500); } log.error(' ❌ Redirect timeout - NotebookLM URL not reached'); return false; } /** * Wait for NotebookLM to load (SIMPLE & RELIABLE) * * Just checks if URL starts with notebooklm.google.com - no complex UI element searching! * Matches the simplified approach used in performLogin(). */ private async waitForNotebook(page: Page, timeoutMs: number): Promise<boolean> { const endTime = Date.now() + timeoutMs; while (Date.now() < endTime) { try { const currentUrl = page.url(); // Simple check: Are we on NotebookLM? if (currentUrl.startsWith('https://notebooklm.google.com/')) { log.success(' ✅ NotebookLM URL detected'); return true; } } catch { // Ignore errors } await page.waitForTimeout(1000); } return false; } /** * Handle possible account chooser */ private async handleAccountChooser(page: Page, email: string): Promise<boolean> { try { const chooser = await page.$$('div[data-identifier], li[data-identifier]'); if (chooser.length > 0) { for (const item of chooser) { const identifier = (await item.getAttribute('data-identifier'))?.toLowerCase() || ''; if (identifier === email.toLowerCase()) { await item.click(); await randomDelay(150, 320); await page.waitForTimeout(500); return true; } } // Click "Use another account" await this.clickText(page, [ 'Use another account', 'Weiteres Konto hinzufügen', 'Anderes Konto verwenden', ]); await randomDelay(150, 320); return false; } return false; } catch { return false; } } /** * Fill email identifier field with human-like typing */ private async fillIdentifier(page: Page, email: string): Promise<boolean> { log.info(' 📧 Looking for email field...'); const emailSelectors = [ 'input#identifierId', "input[name='identifier']", "input[type='email']", ]; let emailSelector: string | null = null; let emailField: ElementHandle | null = null; for (const selector of emailSelectors) { try { const candidate = await page.waitForSelector(selector, { state: 'attached', timeout: 3000, }); if (!candidate) continue; try { if (!(await candidate.isVisible())) { continue; // Hidden field } } catch { continue; } emailField = candidate; emailSelector = selector; log.success(` ✅ Email field visible: ${selector}`); break; } catch { continue; } } if (!emailField || !emailSelector) { log.warning(' ℹ️ No visible email field found (likely pre-filled)'); log.info(` 📍 Current URL: ${page.url().slice(0, 100)}`); return false; } // Human-like mouse movement to field try { const box = await emailField.boundingBox(); if (box) { const targetX = box.x + box.width / 2; const targetY = box.y + box.height / 2; await randomMouseMovement(page, targetX, targetY); await randomDelay(200, 500); } } catch { // Ignore errors } // Click to focus try { await realisticClick(page, emailSelector, false); } catch (error) { log.warning(` ⚠️ Could not click email field (${error}); trying direct focus`); try { await emailField.focus(); } catch { log.error(' ❌ Failed to focus email field'); return false; } } // ✅ FASTER: Programmer typing speed (90-120 WPM from config) log.info(` ⌨️ Typing email: ${this.maskEmail(email)}`); try { const wpm = CONFIG.typingWpmMin + Math.floor(Math.random() * (CONFIG.typingWpmMax - CONFIG.typingWpmMin + 1)); await humanType(page, emailSelector, email, { wpm, withTypos: false }); log.success(' ✅ Email typed successfully'); } catch (error) { log.error(` ❌ Typing failed: ${error}`); try { await page.fill(emailSelector, email); log.success(' ✅ Filled email using fallback'); } catch { return false; } } // Human "thinking" pause before clicking Next await randomDelay(400, 1200); // Click Next button log.info(' 🔘 Looking for Next button...'); const nextSelectors = [ "button:has-text('Next')", "button:has-text('Weiter')", '#identifierNext', ]; let nextClicked = false; for (const selector of nextSelectors) { try { const button = await page.locator(selector); if ((await button.count()) > 0) { await realisticClick(page, selector, true); log.success(` ✅ Next button clicked: ${selector}`); nextClicked = true; break; } } catch { continue; } } if (!nextClicked) { log.warning(' ⚠️ Button not found, pressing Enter'); await emailField.press('Enter'); } // Variable delay await randomDelay(800, 1500); log.success(' ✅ Email step complete'); return true; } /** * Fill password field with human-like typing */ private async fillPassword(page: Page, password: string): Promise<boolean> { log.info(' 🔐 Looking for password field...'); const passwordSelectors = ["input[name='Passwd']", "input[type='password']"]; let passwordSelector: string | null = null; let passwordField: ElementHandle | null = null; for (const selector of passwordSelectors) { try { passwordField = await page.$(selector); if (passwordField) { passwordSelector = selector; log.success(` ✅ Password field found: ${selector}`); break; } } catch { continue; } } if (!passwordField) { // Not found yet, but don't fail - this is called in a loop return false; } // Human-like mouse movement to field try { const box = await passwordField.boundingBox(); if (box) { const targetX = box.x + box.width / 2; const targetY = box.y + box.height / 2; await randomMouseMovement(page, targetX, targetY); await randomDelay(300, 700); } } catch { // Ignore errors } // Click to focus if (passwordSelector) { await realisticClick(page, passwordSelector, false); } // ✅ FASTER: Programmer typing speed (90-120 WPM from config) log.info(' ⌨️ Typing password...'); try { const wpm = CONFIG.typingWpmMin + Math.floor(Math.random() * (CONFIG.typingWpmMax - CONFIG.typingWpmMin + 1)); if (passwordSelector) { await humanType(page, passwordSelector, password, { wpm, withTypos: false }); } log.success(' ✅ Password typed successfully'); } catch (error) { log.error(` ❌ Typing failed: ${error}`); return false; } // Human "review" pause before submitting password await randomDelay(300, 1000); // Click Next button log.info(' 🔘 Looking for Next button...'); const pwdNextSelectors = [ "button:has-text('Next')", "button:has-text('Weiter')", '#passwordNext', ]; let pwdNextClicked = false; for (const selector of pwdNextSelectors) { try { const button = await page.locator(selector); if ((await button.count()) > 0) { await realisticClick(page, selector, true); log.success(` ✅ Next button clicked: ${selector}`); pwdNextClicked = true; break; } } catch { continue; } } if (!pwdNextClicked) { log.warning(' ⚠️ Button not found, pressing Enter'); await passwordField.press('Enter'); } // Variable delay await randomDelay(800, 1500); log.success(' ✅ Password step complete'); return true; } /** * Click text element */ private async clickText(page: Page, texts: string[]): Promise<boolean> { for (const text of texts) { const selector = `text="${text}"`; try { const locator = page.locator(selector); if ((await locator.count()) > 0) { await realisticClick(page, selector, true); await randomDelay(120, 260); return true; } } catch { continue; } } return false; } /** * Mask email for logging */ private maskEmail(email: string): string { if (!email.includes('@')) { return '***'; } const [name, domain] = email.split('@'); if (name.length <= 2) { return `${'*'.repeat(name.length)}@${domain}`; } return `${name[0]}${'*'.repeat(name.length - 2)}${name[name.length - 1]}@${domain}`; } // ============================================================================ // Additional Helper Methods // ============================================================================ /** * Load authentication state from a specific file path */ async loadAuthState(context: BrowserContext, statePath: string): Promise<boolean> { try { // Read state.json const stateData = await fs.readFile(statePath, { encoding: 'utf-8' }); const state = JSON.parse(stateData); // Add cookies to context if (state.cookies) { await context.addCookies(state.cookies); log.success(`✅ Loaded ${state.cookies.length} cookies from ${statePath}`); return true; } log.warning(`⚠️ No cookies found in state file`); return false; } catch (error) { log.error(`❌ Failed to load auth state: ${error}`); return false; } } /** * Perform interactive setup (for setup_auth tool) * Opens a PERSISTENT browser for manual login * * CRITICAL: Uses the SAME persistent context as runtime! * This ensures cookies are automatically saved to the Chrome profile. * * Benefits over temporary browser: * - Session cookies persist correctly (Playwright bug workaround) * - Same fingerprint as runtime * - No need for addCookies() workarounds * - Automatic cookie persistence via Chrome profile * * @param sendProgress Optional progress callback * @param overrideHeadless Optional override for headless mode (true = visible, false = headless) * If not provided, defaults to true (visible) for setup */ async performSetup( sendProgress?: ProgressCallback, overrideHeadless?: boolean ): Promise<boolean> { const { chromium } = await import('patchright'); // Determine headless mode: override or default to true (visible for setup) // overrideHeadless contains show_browser value (true = show, false = hide) const shouldShowBrowser = overrideHeadless !== undefined ? overrideHeadless : true; try { // Check if already authenticated const statePath = await this.getValidStatePath(); const isAuthenticated = statePath !== null; if (isAuthenticated) { log.info('✅ Already authenticated, skipping setup'); log.info(" Use 're_auth' tool to switch accounts or re-authenticate"); await sendProgress?.('Already authenticated!', 10, 10); return true; } log.info('🔄 Preparing for first-time authentication...'); await sendProgress?.('Preparing browser...', 1, 10); log.info('🚀 Launching persistent browser for interactive setup...'); log.info(` 📍 Profile: ${CONFIG.chromeProfileDir}`); await sendProgress?.('Launching persistent browser...', 2, 10); // ✅ CRITICAL FIX: Use launchPersistentContext (same as runtime!) // This ensures session cookies persist correctly const context = await chromium.launchPersistentContext(CONFIG.chromeProfileDir, { headless: !shouldShowBrowser, // Use override or default to visible for setup channel: 'chrome' as const, viewport: CONFIG.viewport, locale: 'en-US', timezoneId: 'Europe/Berlin', args: [ '--disable-blink-features=AutomationControlled', '--disable-dev-shm-usage', '--no-first-run', '--no-default-browser-check', ], }); // Get or create a page const pages = context.pages(); const page = pages.length > 0 ? pages[0] : await context.newPage(); // Perform login with progress updates const loginSuccess = await this.performLogin(page, sendProgress); if (loginSuccess) { // ✅ Save browser state to state.json (for validation & backup) // Chrome ALSO saves everything to the persistent profile automatically! await sendProgress?.('Saving authentication state...', 9, 10); await this.saveBrowserState(context, page); // ✅ CRITICAL FIX: Wait for Chrome to flush profile to disk // Windows needs time to write persistent data (cookies, cache, session storage, etc.) // Without this delay, chrome_profile/ folder remains empty! log.info('⏳ Waiting for Chrome to finalize profile writes (5 seconds)...'); await page.waitForTimeout(5000); // 5 seconds buffer for Windows filesystem log.success('✅ Setup complete - authentication saved to:'); log.success(` 📄 State file: ${this.stateFilePath}`); log.success(` 📁 Chrome profile: ${CONFIG.chromeProfileDir}`); log.info('💡 Session cookies will now persist across restarts!'); } // Close persistent context await context.close(); return loginSuccess; } catch (error) { log.error(`❌ Setup failed: ${error}`); return false; } } // ============================================================================ // Cleanup // ============================================================================ /** * Clear ALL authentication data for account switching * * CRITICAL: This deletes EVERYTHING to ensure only ONE account is active: * - All state.json files (cookies, localStorage) * - sessionStorage files * - Chrome profile directory (browser fingerprint, cache, etc.) * * Use this BEFORE authenticating a new account! */ async clearAllAuthData(): Promise<void> { log.warning('🗑️ Clearing ALL authentication data for account switch...'); let deletedCount = 0; // 1. Delete all state files in browser_state_dir try { const files = await fs.readdir(CONFIG.browserStateDir); for (const file of files) { if (file.endsWith('.json')) { await fs.unlink(path.join(CONFIG.browserStateDir, file)); log.info(` ✅ Deleted: ${file}`); deletedCount++; } } } catch (error) { log.warning(` ⚠️ Could not delete state files: ${error}`); } // 2. Delete Chrome profile (THE KEY for account switching!) // This removes ALL browser data: cookies, cache, fingerprint, etc. try { const chromeProfileDir = CONFIG.chromeProfileDir; if (existsSync(chromeProfileDir)) { await fs.rm(chromeProfileDir, { recursive: true, force: true }); log.success(` ✅ Deleted Chrome profile: ${chromeProfileDir}`); deletedCount++; } } catch (error) { log.warning(` ⚠️ Could not delete Chrome profile: ${error}`); } if (deletedCount === 0) { log.info(' ℹ️ No old auth data found (already clean)'); } else { log.success(`✅ All auth data cleared (${deletedCount} items) - ready for new account!`); } } /** * Clear all saved authentication state */ async clearState(): Promise<boolean> { try { try { await fs.unlink(this.stateFilePath); } catch { // File doesn't exist } try { await fs.unlink(this.sessionFilePath); } catch { // File doesn't exist } log.success('✅ Authentication state cleared'); return true; } catch (error) { log.error(`❌ Failed to clear state: ${error}`); return false; } } /** * HARD RESET: Completely delete ALL authentication state */ async hardResetState(): Promise<boolean> { try { log.warning('🧹 Performing HARD RESET of all authentication state...'); let deletedCount = 0; // Delete state file try { await fs.unlink(this.stateFilePath); log.info(` 🗑️ Deleted: ${this.stateFilePath}`); deletedCount++; } catch { // File doesn't exist } // Delete session file try { await fs.unlink(this.sessionFilePath); log.info(` 🗑️ Deleted: ${this.sessionFilePath}`); deletedCount++; } catch { // File doesn't exist } // Delete entire browser_state_dir try { const files = await fs.readdir(CONFIG.browserStateDir); for (const file of files) { await fs.unlink(path.join(CONFIG.browserStateDir, file)); deletedCount++; } log.info(` 🗑️ Deleted: ${CONFIG.browserStateDir}/ (${files.length} files)`); } catch { // Directory doesn't exist or empty } if (deletedCount === 0) { log.info(' ℹ️ No state to delete (already clean)'); } else { log.success(`✅ Hard reset complete: ${deletedCount} items deleted`); } return true; } catch (error) { log.error(`❌ Hard reset failed: ${error}`); return false; } } }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/roomi-fields/notebooklm-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server