import fs from 'fs' import path from 'path' import * as readline from 'readline' import type { BrowserContext, Page } from 'rebrowser-playwright' import { log } from '../util/Logger' import { DataGenerator } from './DataGenerator' import { CreatedAccount } from './types' export class AccountCreator { private page!: Page private dataGenerator: DataGenerator private referralUrl?: string private recoveryEmail?: string private autoAccept: boolean private rl: readline.Interface private rlClosed = false constructor(referralUrl?: string, recoveryEmail?: string, autoAccept = false) { this.referralUrl = referralUrl this.recoveryEmail = recoveryEmail this.autoAccept = autoAccept this.dataGenerator = new DataGenerator() this.rl = readline.createInterface({ input: process.stdin, output: process.stdout }) this.rlClosed = false } // Human-like delay helper private async humanDelay(minMs: number, maxMs: number): Promise { const delay = Math.random() * (maxMs - minMs) + minMs await this.page.waitForTimeout(Math.floor(delay)) } /** * UTILITY: Find first visible element from list of selectors * Reserved for future use - simplifies selector fallback logic * * Usage example: * const element = await this.findFirstVisible(['selector1', 'selector2'], 'CONTEXT') * if (element) await element.click() */ /* private async findFirstVisible(selectors: string[], context: string): Promise | null> { for (const selector of selectors) { try { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `[${context}] Found element: ${selector}`, 'log', 'green') return element } } catch { continue } } log(false, 'CREATOR', `[${context}] No visible element found`, 'warn', 'yellow') return null } */ /** * UTILITY: Retry an async operation with exponential backoff */ private async retryOperation( operation: () => Promise, context: string, maxRetries: number = 3, initialDelayMs: number = 1000 ): Promise { for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const result = await operation() return result } catch (error) { if (attempt < maxRetries) { const delayMs = initialDelayMs * Math.pow(2, attempt - 1) await this.page.waitForTimeout(delayMs) } else { return null } } } return null } /** * CRITICAL: Wait for dropdown to be fully closed before continuing */ private async waitForDropdownClosed(context: string, maxWaitMs: number = 5000): Promise { log(false, 'CREATOR', `[${context}] Waiting for dropdown to close...`, 'log', 'cyan') const startTime = Date.now() while (Date.now() - startTime < maxWaitMs) { // Check if any dropdown menu is visible const dropdownSelectors = [ 'div[role="listbox"]', 'ul[role="listbox"]', 'div[role="menu"]', 'ul[role="menu"]', '[class*="dropdown"][class*="open"]' ] let anyVisible = false for (const selector of dropdownSelectors) { const visible = await this.page.locator(selector).first().isVisible().catch(() => false) if (visible) { anyVisible = true break } } if (!anyVisible) { return true } await this.page.waitForTimeout(500) } return false } /** * CRITICAL: Verify input value after filling */ private async verifyInputValue( selector: string, expectedValue: string ): Promise { try { const input = this.page.locator(selector).first() const actualValue = await input.inputValue().catch(() => '') return actualValue === expectedValue } catch (error) { return false } } /** * CRITICAL: Verify no errors are displayed on the page * Returns true if no errors found, false if errors present */ private async verifyNoErrors(): Promise { const errorSelectors = [ 'div[id*="Error"]', 'div[id*="error"]', 'div[class*="error"]', 'div[role="alert"]', '[aria-invalid="true"]', 'span[class*="error"]', '.error-message', '[data-bind*="errorMessage"]' ] for (const selector of errorSelectors) { try { const errorElement = this.page.locator(selector).first() const isVisible = await errorElement.isVisible().catch(() => false) if (isVisible) { const errorText = await errorElement.textContent().catch(() => 'Unknown error') log(false, 'CREATOR', `Error detected: ${errorText}`, 'error') return false } } catch { continue } } return true } /** * CRITICAL: Verify page transition was successful * Checks that new elements appeared AND old elements disappeared * Reserved for future use - can be called for complex page transitions * * Usage example: * const success = await this.verifyPageTransition( * 'EMAIL_TO_PASSWORD', * ['input[type="password"]'], * ['input[type="email"]'] * ) * if (!success) return null */ /* private async verifyPageTransition( context: string, expectedNewSelectors: string[], expectedGoneSelectors: string[], timeoutMs: number = 15000 ): Promise { log(false, 'CREATOR', `[${context}] Verifying page transition...`, 'log', 'cyan') const startTime = Date.now() try { // STEP 1: Wait for at least ONE new element to appear log(false, 'CREATOR', `[${context}] Waiting for new page elements...`, 'log', 'cyan') let newElementFound = false for (const selector of expectedNewSelectors) { try { const element = this.page.locator(selector).first() await element.waitFor({ timeout: Math.min(5000, timeoutMs), state: 'visible' }) log(false, 'CREATOR', `[${context}] ✅ New element appeared: ${selector}`, 'log', 'green') newElementFound = true break } catch { continue } } if (!newElementFound) { log(false, 'CREATOR', `[${context}] ❌ No new elements appeared - transition likely failed`, 'error') return false } // STEP 2: Verify old elements are gone log(false, 'CREATOR', `[${context}] Verifying old elements disappeared...`, 'log', 'cyan') await this.humanDelay(1000, 2000) // Give time for old elements to disappear for (const selector of expectedGoneSelectors) { try { const element = this.page.locator(selector).first() const stillVisible = await element.isVisible().catch(() => false) if (stillVisible) { log(false, 'CREATOR', `[${context}] ⚠️ Old element still visible: ${selector}`, 'warn', 'yellow') // Don't fail immediately - element might be animating out } else { log(false, 'CREATOR', `[${context}] ✅ Old element gone: ${selector}`, 'log', 'green') } } catch { // Element not found = good, it's gone log(false, 'CREATOR', `[${context}] ✅ Old element not found: ${selector}`, 'log', 'green') } } // STEP 3: Verify no errors on new page const noErrors = await this.verifyNoErrors() if (!noErrors) { log(false, 'CREATOR', `[${context}] ❌ Errors found after transition`, 'error') return false } const elapsed = Date.now() - startTime log(false, 'CREATOR', `[${context}] ✅ Page transition verified (${elapsed}ms)`, 'log', 'green') return true } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `[${context}] ❌ Page transition verification failed: ${msg}`, 'error') return false } } */ /** * CRITICAL: Verify that a click action was successful * Checks that something changed after the click (URL, visible elements, etc.) * Reserved for future use - can be called for complex click verifications * * Usage example: * await button.click() * const success = await this.verifyClickSuccess('BUTTON_CLICK', true, ['div.new-content']) * if (!success) return null */ /* private async verifyClickSuccess( context: string, urlShouldChange: boolean = false, expectedNewSelectors: string[] = [] ): Promise { log(false, 'CREATOR', `[${context}] Verifying click was successful...`, 'log', 'cyan') const startUrl = this.page.url() // Wait a bit for changes to occur await this.humanDelay(2000, 3000) // Check 1: URL change (if expected) if (urlShouldChange) { const newUrl = this.page.url() if (newUrl === startUrl) { log(false, 'CREATOR', `[${context}] ⚠️ URL did not change (might be intentional)`, 'warn', 'yellow') } else { log(false, 'CREATOR', `[${context}] ✅ URL changed: ${startUrl} → ${newUrl}`, 'log', 'green') return true } } // Check 2: New elements appeared (if expected) if (expectedNewSelectors.length > 0) { for (const selector of expectedNewSelectors) { try { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `[${context}] ✅ New element appeared: ${selector}`, 'log', 'green') return true } } catch { continue } } log(false, 'CREATOR', `[${context}] ⚠️ No expected elements appeared`, 'warn', 'yellow') } // Check 3: No errors appeared const noErrors = await this.verifyNoErrors() if (!noErrors) { log(false, 'CREATOR', `[${context}] ❌ Errors appeared after click`, 'error') return false } log(false, 'CREATOR', `[${context}] ✅ Click appears successful`, 'log', 'green') return true } */ private async askQuestion(question: string): Promise { return new Promise((resolve) => { this.rl.question(question, (answer) => { resolve(answer.trim()) }) }) } /** * CRITICAL: Wait for page to be completely stable before continuing * Checks for loading spinners, network activity, URL stability, and JS execution */ private async waitForPageStable(context: string, maxWaitMs: number = 15000): Promise { // REDUCED: Don't log start - too verbose const startTime = Date.now() try { // STEP 1: Wait for network to be idle await this.page.waitForLoadState('networkidle', { timeout: Math.min(maxWaitMs, 10000) }) // STEP 2: Wait for DOM to be fully loaded // Silent catch justified: DOMContentLoaded may already be complete await this.page.waitForLoadState('domcontentloaded', { timeout: 3000 }).catch(() => {}) // STEP 3: REDUCED delay - pages load fast await this.humanDelay(1500, 2500) // STEP 4: Check for loading indicators const loadingSelectors = [ '.loading', '[class*="spinner"]', '[class*="loading"]', '[aria-busy="true"]' ] // Wait for loading indicators to disappear for (const selector of loadingSelectors) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { // Silent catch justified: Loading indicators may disappear before timeout, which is fine await element.waitFor({ state: 'hidden', timeout: Math.min(5000, maxWaitMs - (Date.now() - startTime)) }).catch(() => {}) } } return true } catch (error) { // Only log actual failures, not warnings const msg = error instanceof Error ? error.message : String(error) if (msg.includes('Timeout')) { // Timeout is not critical - page might still be usable return true } return false } } /** * CRITICAL: Wait for Microsoft account creation to complete * This happens AFTER CAPTCHA and can take several seconds */ private async waitForAccountCreation(): Promise { const maxWaitTime = 60000 // 60 seconds const startTime = Date.now() try { // STEP 1: Wait for any "Creating account" messages to appear AND disappear const creationMessages = [ 'text="Creating your account"', 'text="Création de votre compte"', 'text="Setting up your account"', 'text="Configuration de votre compte"', 'text="Please wait"', 'text="Veuillez patienter"' ] for (const messageSelector of creationMessages) { const element = this.page.locator(messageSelector).first() const visible = await element.isVisible().catch(() => false) if (visible) { // Wait for this message to disappear try { await element.waitFor({ state: 'hidden', timeout: 45000 }) } catch { // Continue even if message persists } } } // STEP 2: Wait for URL to stabilize or change to expected page let urlStableCount = 0 let lastUrl = this.page.url() while (Date.now() - startTime < maxWaitTime) { await this.humanDelay(1000, 1500) const currentUrl = this.page.url() if (currentUrl === lastUrl) { urlStableCount++ // URL has been stable for 3 consecutive checks if (urlStableCount >= 3) { break } } else { lastUrl = currentUrl urlStableCount = 0 } } // STEP 3: Wait for page to be fully loaded await this.waitForPageStable('ACCOUNT_CREATION', 15000) // STEP 4: Additional safety delay await this.humanDelay(3000, 5000) return true } catch (error) { return false } } /** * CRITICAL: Verify that an element exists, is visible, and is interactable */ private async verifyElementReady( selector: string, context: string, timeoutMs: number = 10000 ): Promise { try { const element = this.page.locator(selector).first() // Wait for element to exist await element.waitFor({ timeout: timeoutMs, state: 'attached' }) // Wait for element to be visible await element.waitFor({ timeout: 5000, state: 'visible' }) // Check if element is enabled (for buttons/inputs) const isEnabled = await element.isEnabled().catch(() => true) if (!isEnabled) { return false } return true } catch (error) { return false } } async create(context: BrowserContext): Promise { try { this.page = await context.newPage() log(false, 'CREATOR', '🚀 Starting account creation...', 'log', 'cyan') // Navigate to signup page await this.navigateToSignup() // Click "Create account" button await this.clickCreateAccount() // Generate email and fill it (handles suggestions automatically) const emailResult = await this.generateAndFillEmail(this.autoAccept) if (!emailResult) { log(false, 'CREATOR', 'Failed to configure email', 'error') return null } log(false, 'CREATOR', `✅ Email: ${emailResult}`, 'log', 'green') // Wait for password page and fill it const password = await this.fillPassword() if (!password) { log(false, 'CREATOR', 'Failed to generate password', 'error') return null } // Click Next button const passwordNextSuccess = await this.clickNext('password') if (!passwordNextSuccess) { log(false, 'CREATOR', '❌ Failed to proceed after password step', 'error') return null } // Extract final email from identity badge to confirm const finalEmail = await this.extractEmail() const confirmedEmail = finalEmail || emailResult // Fill birthdate const birthdate = await this.fillBirthdate() if (!birthdate) { log(false, 'CREATOR', 'Failed to fill birthdate', 'error') return null } // Click Next button const birthdateNextSuccess = await this.clickNext('birthdate') if (!birthdateNextSuccess) { log(false, 'CREATOR', '❌ Failed to proceed after birthdate step', 'error') return null } // Fill name fields const names = await this.fillNames(confirmedEmail) if (!names) { log(false, 'CREATOR', 'Failed to fill names', 'error') return null } // Click Next button const namesNextSuccess = await this.clickNext('names') if (!namesNextSuccess) { log(false, 'CREATOR', '❌ Failed to proceed after names step', 'error') return null } // Wait for CAPTCHA page const captchaDetected = await this.waitForCaptcha() if (captchaDetected) { log(false, 'CREATOR', '⚠️ CAPTCHA detected - waiting for human to solve it...', 'warn', 'yellow') log(false, 'CREATOR', 'Please solve the CAPTCHA in the browser. The script will wait...', 'log', 'yellow') await this.waitForCaptchaSolved() log(false, 'CREATOR', '✅ CAPTCHA solved! Continuing...', 'log', 'green') } // Handle post-CAPTCHA questions (Stay signed in, etc.) await this.handlePostCreationQuestions() // Navigate to Bing Rewards and verify connection await this.verifyAccountActive() // Post-setup: Recovery email & 2FA let recoveryEmailUsed: string | undefined let totpSecret: string | undefined let recoveryCode: string | undefined try { // Setup recovery email // Logic: If -r provided, use it. If -y (auto-accept), ask for it. Otherwise, interactive prompt. if (this.recoveryEmail) { // User provided -r flag with email const emailResult = await this.setupRecoveryEmail() if (emailResult) recoveryEmailUsed = emailResult } else if (this.autoAccept) { // User provided -y (auto-accept all) - prompt for recovery email log(false, 'CREATOR', '📧 Auto-accept mode: prompting for recovery email...', 'log', 'cyan') const emailResult = await this.setupRecoveryEmail() if (emailResult) recoveryEmailUsed = emailResult } else { // Interactive mode - ask user const emailResult = await this.setupRecoveryEmail() if (emailResult) recoveryEmailUsed = emailResult } // Setup 2FA // Logic: If -y (auto-accept), enable it automatically. Otherwise, ask user. if (this.autoAccept) { // User provided -y (auto-accept all) - enable 2FA automatically log(false, 'CREATOR', '🔐 Auto-accept mode: enabling 2FA...', 'log', 'cyan') const tfaResult = await this.setup2FA() if (tfaResult) { totpSecret = tfaResult.totpSecret recoveryCode = tfaResult.recoveryCode } } else { // Interactive mode - ask user const wants2FA = await this.ask2FASetup() if (wants2FA) { const tfaResult = await this.setup2FA() if (tfaResult) { totpSecret = tfaResult.totpSecret recoveryCode = tfaResult.recoveryCode } } else { log(false, 'CREATOR', 'Skipping 2FA setup', 'log', 'gray') } } } catch (error) { log(false, 'CREATOR', `Post-setup error: ${error}`, 'warn', 'yellow') } // Create account object const createdAccount: CreatedAccount = { email: confirmedEmail, password: password, birthdate: { day: birthdate.day, month: birthdate.month, year: birthdate.year }, firstName: names.firstName, lastName: names.lastName, createdAt: new Date().toISOString(), referralUrl: this.referralUrl, recoveryEmail: recoveryEmailUsed, totpSecret: totpSecret, recoveryCode: recoveryCode } // Save to file await this.saveAccount(createdAccount) log(false, 'CREATOR', `✅ Account created successfully: ${confirmedEmail}`, 'log', 'green') return createdAccount } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `Error during account creation: ${msg}`, 'error') log(false, 'CREATOR', '⚠️ Browser left open for inspection. Press Ctrl+C to exit.', 'warn', 'yellow') return null } finally { try { if (!this.rlClosed) { this.rl.close() this.rlClosed = true } } catch {/* ignore */} } } private async navigateToSignup(): Promise { if (this.referralUrl) { log(false, 'CREATOR', '🔗 Navigating to referral link...', 'log', 'cyan') await this.page.goto(this.referralUrl, { waitUntil: 'networkidle', timeout: 60000 }) await this.waitForPageStable('REFERRAL_PAGE', 10000) await this.humanDelay(1000, 2000) const joinButtonSelectors = [ 'a#start-earning-rewards-link', 'a.cta.learn-more-btn', 'a[href*="signup"]', 'button[class*="join"]' ] let clickSuccess = false for (const selector of joinButtonSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { const urlBefore = this.page.url() await button.click() // OPTIMIZED: Reduced delay after Join click await this.humanDelay(1000, 1500) // CRITICAL: Verify the click actually did something const urlAfter = this.page.url() if (urlAfter !== urlBefore || urlAfter.includes('login.live.com') || urlAfter.includes('signup')) { // OPTIMIZED: Reduced from 8000ms to 3000ms await this.waitForPageStable('AFTER_JOIN_CLICK', 3000) clickSuccess = true break } else { // OPTIMIZED: Reduced retry delay await this.humanDelay(1000, 1500) // Try clicking again await button.click() await this.humanDelay(1000, 1500) const urlRetry = this.page.url() if (urlRetry !== urlBefore) { // OPTIMIZED: Reduced from 8000ms to 3000ms await this.waitForPageStable('AFTER_JOIN_CLICK', 3000) clickSuccess = true break } } } } if (!clickSuccess) { // Navigate directly to signup await this.page.goto('https://login.live.com/', { waitUntil: 'networkidle', timeout: 30000 }) // OPTIMIZED: Reduced from 8000ms to 3000ms await this.waitForPageStable('DIRECT_LOGIN', 3000) } } else { log(false, 'CREATOR', '🌐 Navigating to Microsoft login...', 'log', 'cyan') await this.page.goto('https://login.live.com/', { waitUntil: 'networkidle', timeout: 60000 }) // OPTIMIZED: Reduced from 20000ms to 5000ms await this.waitForPageStable('LOGIN_PAGE', 5000) await this.humanDelay(1000, 1500) } } private async clickCreateAccount(): Promise { // OPTIMIZED: Page is already stable from navigateToSignup(), no need to wait again // await this.waitForPageStable('BEFORE_CREATE_ACCOUNT', 3000) // REMOVED const createAccountSelectors = [ 'a[id*="signup"]', 'a[href*="signup"]', 'span[role="button"].fui-Link', 'button[id*="signup"]', 'a[data-testid*="signup"]' ] for (const selector of createAccountSelectors) { const button = this.page.locator(selector).first() try { // OPTIMIZED: Reduced timeout from 5000ms to 2000ms await button.waitFor({ timeout: 2000 }) const urlBefore = this.page.url() await button.click() // OPTIMIZED: Reduced delay from 1500-2500ms to 500-1000ms (click is instant) await this.humanDelay(500, 1000) // CRITICAL: Verify click worked const urlAfter = this.page.url() const emailFieldAppeared = await this.page.locator('input[type="email"]').first().isVisible().catch(() => false) if (urlAfter !== urlBefore || emailFieldAppeared) { // OPTIMIZED: Reduced from 3000ms to 1000ms - email field is already visible await this.humanDelay(1000, 1500) return } else { continue } } catch { // Selector not found, try next one immediately continue } } throw new Error('Could not find working "Create account" button') } private async generateAndFillEmail(autoAccept = false): Promise { log(false, 'CREATOR', '📧 Configuring email...', 'log', 'cyan') // OPTIMIZED: Page is already stable from clickCreateAccount(), minimal wait needed await this.humanDelay(500, 1000) let email: string if (autoAccept) { // Auto mode: generate automatically email = this.dataGenerator.generateEmail() log(false, 'CREATOR', `Generated realistic email (auto mode): ${email}`, 'log', 'cyan') } else { // Interactive mode: ask user const useAutoGenerate = await this.askQuestion('Generate email automatically? (Y/n): ') if (useAutoGenerate.toLowerCase() === 'n' || useAutoGenerate.toLowerCase() === 'no') { email = await this.askQuestion('Enter your email: ') log(false, 'CREATOR', `Using custom email: ${email}`, 'log', 'cyan') } else { email = this.dataGenerator.generateEmail() log(false, 'CREATOR', `Generated realistic email: ${email}`, 'log', 'cyan') } } const emailInput = this.page.locator('input[type="email"]').first() await emailInput.waitFor({ timeout: 15000 }) // CRITICAL: Retry fill with SMART verification // Microsoft separates username from domain for outlook.com/hotmail.com addresses const emailFillSuccess = await this.retryOperation( async () => { await emailInput.clear() await this.humanDelay(800, 1500) await emailInput.fill(email) await this.humanDelay(1200, 2500) // SMART VERIFICATION: Check if Microsoft separated the domain const inputValue = await emailInput.inputValue().catch(() => '') const emailUsername = email.split('@')[0] // e.g., "sharon_jackson" const emailDomain = email.split('@')[1] // e.g., "outlook.com" // Check if input contains full email OR just username (Microsoft separated domain) if (inputValue === email) { // Full email is in input (not separated) log(false, 'CREATOR', `[EMAIL_INPUT] ✅ Input value verified: ${email}`, 'log', 'green') return true } else if (inputValue === emailUsername && (emailDomain === 'outlook.com' || emailDomain === 'hotmail.com' || emailDomain === 'outlook.fr')) { // Microsoft separated the domain - this is EXPECTED and OK log(false, 'CREATOR', `[EMAIL_INPUT] ✅ Username verified: ${emailUsername} (domain separated by Microsoft)`, 'log', 'green') return true } else { // Unexpected value log(false, 'CREATOR', `[EMAIL_INPUT] ⚠️ Unexpected value: expected "${email}" or "${emailUsername}", got "${inputValue}"`, 'warn', 'yellow') throw new Error('Email input value not verified') } }, 'EMAIL_FILL', 3, 1000 ) if (!emailFillSuccess) { log(false, 'CREATOR', 'Failed to fill email after retries', 'error') return null } log(false, 'CREATOR', 'Clicking Next button...', 'log') const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() await nextBtn.waitFor({ timeout: 10000 }) // CRITICAL: Get current URL before clicking const urlBeforeClick = this.page.url() await nextBtn.click() // OPTIMIZED: Reduced delay after clicking Next await this.humanDelay(1000, 1500) await this.waitForPageStable('AFTER_EMAIL_SUBMIT', 10000) // CRITICAL: Verify the click had an effect const urlAfterClick = this.page.url() if (urlBeforeClick === urlAfterClick) { // URL didn't change - check if there's an error or if we're on password page const onPasswordPage = await this.page.locator('input[type="password"]').first().isVisible().catch(() => false) const hasError = await this.page.locator('div[id*="Error"], div[role="alert"]').first().isVisible().catch(() => false) if (!onPasswordPage && !hasError) { log(false, 'CREATOR', '⚠️ Email submission may have failed - no password field, no error', 'warn', 'yellow') log(false, 'CREATOR', 'Waiting longer for response...', 'log', 'cyan') await this.humanDelay(5000, 7000) } } else { log(false, 'CREATOR', `✅ URL changed: ${urlBeforeClick} → ${urlAfterClick}`, 'log', 'green') } const result = await this.handleEmailErrors(email) if (!result.success) { return null } // CRITICAL: If email was accepted by handleEmailErrors, trust that result // Don't do additional error check here as it may detect false positives // (e.g., transient errors that were already handled) log(false, 'CREATOR', `✅ Email step completed successfully: ${result.email}`, 'log', 'green') return result.email } private async handleEmailErrors(originalEmail: string, retryCount = 0): Promise<{ success: boolean; email: string | null }> { await this.humanDelay(1000, 1500) // CRITICAL: Prevent infinite retry loops const MAX_EMAIL_RETRIES = 5 if (retryCount >= MAX_EMAIL_RETRIES) { log(false, 'CREATOR', `❌ Max email retries (${MAX_EMAIL_RETRIES}) reached - giving up`, 'error') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') await new Promise(() => {}) return { success: false, email: null } } const errorLocator = this.page.locator('div[id*="Error"], div[role="alert"]').first() const errorVisible = await errorLocator.isVisible().catch(() => false) if (!errorVisible) { log(false, 'CREATOR', `✅ Email accepted: ${originalEmail}`, 'log', 'green') return { success: true, email: originalEmail } } const errorText = await errorLocator.textContent().catch(() => '') || '' // IGNORE password requirements messages (not actual errors) if (errorText && (errorText.toLowerCase().includes('password') && errorText.toLowerCase().includes('characters'))) { // This is just password requirements info, not an error return { success: true, email: originalEmail } } log(false, 'CREATOR', `Email error: ${errorText} (attempt ${retryCount + 1}/${MAX_EMAIL_RETRIES})`, 'warn', 'yellow') // Check for reserved domain error if (errorText && (errorText.toLowerCase().includes('reserved') || errorText.toLowerCase().includes('réservé'))) { return await this.handleReservedDomain(originalEmail, retryCount) } // Check for email taken error if (errorText && (errorText.toLowerCase().includes('taken') || errorText.toLowerCase().includes('pris') || errorText.toLowerCase().includes('already') || errorText.toLowerCase().includes('déjà'))) { return await this.handleEmailTaken(retryCount) } log(false, 'CREATOR', 'Unknown error type, pausing for inspection', 'error') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') await new Promise(() => {}) return { success: false, email: null } } private async handleReservedDomain(originalEmail: string, retryCount = 0): Promise<{ success: boolean; email: string | null }> { log(false, 'CREATOR', `Domain blocked: ${originalEmail.split('@')[1]}`, 'warn', 'yellow') const username = originalEmail.split('@')[0] const newEmail = `${username}@outlook.com` log(false, 'CREATOR', `Retrying with: ${newEmail}`, 'log', 'cyan') const emailInput = this.page.locator('input[type="email"]').first() // CRITICAL: Retry fill with SMART verification (handles domain separation) const retryFillSuccess = await this.retryOperation( async () => { await emailInput.clear() await this.humanDelay(800, 1500) await emailInput.fill(newEmail) await this.humanDelay(1200, 2500) // SMART VERIFICATION: Microsoft may separate domain const inputValue = await emailInput.inputValue().catch(() => '') const emailUsername = newEmail.split('@')[0] const emailDomain = newEmail.split('@')[1] if (inputValue === newEmail || (inputValue === emailUsername && (emailDomain === 'outlook.com' || emailDomain === 'hotmail.com' || emailDomain === 'outlook.fr'))) { return true } else { throw new Error('Email retry input value not verified') } }, 'EMAIL_RETRY_FILL', 3, 1000 ) if (!retryFillSuccess) { log(false, 'CREATOR', 'Failed to fill retry email', 'error') return { success: false, email: null } } const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() await nextBtn.click() await this.humanDelay(2000, 3000) await this.waitForPageStable('RETRY_EMAIL', 15000) return await this.handleEmailErrors(newEmail, retryCount + 1) } private async handleEmailTaken(retryCount = 0): Promise<{ success: boolean; email: string | null }> { log(false, 'CREATOR', 'Email taken, looking for Microsoft suggestions...', 'log', 'yellow') await this.humanDelay(2000, 3000) await this.waitForPageStable('EMAIL_SUGGESTIONS', 10000) // Multiple selectors for suggestions container const suggestionSelectors = [ 'div[data-testid="suggestions"]', 'div[role="toolbar"]', 'div.fui-TagGroup', 'div[class*="suggestions"]', 'div[class*="TagGroup"]' ] let suggestionsContainer = null for (const selector of suggestionSelectors) { const container = this.page.locator(selector).first() const visible = await container.isVisible().catch(() => false) if (visible) { suggestionsContainer = container log(false, 'CREATOR', `Found suggestions with selector: ${selector}`, 'log', 'green') break } } if (!suggestionsContainer) { log(false, 'CREATOR', 'No suggestions found from Microsoft', 'warn', 'yellow') // CRITICAL FIX: Generate a new email automatically instead of freezing log(false, 'CREATOR', '🔄 Generating a new email automatically...', 'log', 'cyan') const newEmail = this.dataGenerator.generateEmail() log(false, 'CREATOR', `Generated new email: ${newEmail}`, 'log', 'cyan') // Clear and fill the email input with the new email const emailInput = this.page.locator('input[type="email"]').first() const retryFillSuccess = await this.retryOperation( async () => { await emailInput.clear() await this.humanDelay(800, 1500) await emailInput.fill(newEmail) await this.humanDelay(1200, 2500) // SMART VERIFICATION: Microsoft may separate domain const inputValue = await emailInput.inputValue().catch(() => '') const emailUsername = newEmail.split('@')[0] const emailDomain = newEmail.split('@')[1] if (inputValue === newEmail || (inputValue === emailUsername && (emailDomain === 'outlook.com' || emailDomain === 'hotmail.com' || emailDomain === 'outlook.fr'))) { return true } else { throw new Error('Email auto-retry input value not verified') } }, 'EMAIL_AUTO_RETRY_FILL', 3, 1000 ) if (!retryFillSuccess) { log(false, 'CREATOR', 'Failed to fill new email after retries', 'error') return { success: false, email: null } } // Click Next to submit the new email const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() await nextBtn.click() await this.humanDelay(2000, 3000) await this.waitForPageStable('AUTO_RETRY_EMAIL', 15000) // Recursively check the new email (with retry count incremented) return await this.handleEmailErrors(newEmail, retryCount + 1) } // Find all suggestion buttons const suggestionButtons = await suggestionsContainer.locator('button').all() log(false, 'CREATOR', `Found ${suggestionButtons.length} suggestion buttons`, 'log', 'cyan') if (suggestionButtons.length === 0) { log(false, 'CREATOR', 'Suggestions container found but no buttons inside', 'error') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') await new Promise(() => {}) return { success: false, email: null } } // Get text from first suggestion before clicking const firstButton = suggestionButtons[0] if (!firstButton) { log(false, 'CREATOR', 'First button is undefined', 'error') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') await new Promise(() => {}) return { success: false, email: null } } const suggestedEmail = await firstButton.textContent().catch(() => '') || '' let cleanEmail = suggestedEmail.trim() // If suggestion doesn't have @domain, it's just the username - add @outlook.com if (cleanEmail && !cleanEmail.includes('@')) { cleanEmail = `${cleanEmail}@outlook.com` log(false, 'CREATOR', `Suggestion is username only, adding domain: ${cleanEmail}`, 'log', 'cyan') } if (!cleanEmail) { log(false, 'CREATOR', 'Could not extract email from suggestion button', 'error') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') await new Promise(() => {}) return { success: false, email: null } } log(false, 'CREATOR', `Selecting suggestion: ${cleanEmail}`, 'log', 'cyan') // Click the suggestion await firstButton.click() await this.humanDelay(1500, 2500) // Verify the email input was updated const emailInput = this.page.locator('input[type="email"]').first() const inputValue = await emailInput.inputValue().catch(() => '') if (inputValue) { log(false, 'CREATOR', `✅ Suggestion applied: ${inputValue}`, 'log', 'green') } // Check if error is gone const errorLocator = this.page.locator('div[id*="Error"], div[role="alert"]').first() const errorStillVisible = await errorLocator.isVisible().catch(() => false) if (errorStillVisible) { log(false, 'CREATOR', 'Error still visible after clicking suggestion', 'warn', 'yellow') // Try clicking Next to submit const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() const nextEnabled = await nextBtn.isEnabled().catch(() => false) if (nextEnabled) { log(false, 'CREATOR', 'Clicking Next to submit suggestion', 'log') await nextBtn.click() await this.humanDelay(2000, 3000) // Final check const finalError = await errorLocator.isVisible().catch(() => false) if (finalError) { log(false, 'CREATOR', 'Failed to resolve error', 'error') log(false, 'CREATOR', '⚠️ Browser left open. Press Ctrl+C to exit.', 'warn', 'yellow') await new Promise(() => {}) return { success: false, email: null } } } } else { // Error is gone, click Next to continue log(false, 'CREATOR', 'Suggestion accepted, clicking Next', 'log', 'green') const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() await nextBtn.click() await this.humanDelay(2000, 3000) } log(false, 'CREATOR', `✅ Using suggested email: ${cleanEmail}`, 'log', 'green') return { success: true, email: cleanEmail } } private async clickNext(step: string): Promise { log(false, 'CREATOR', `Clicking Next button (${step})...`, 'log') // CRITICAL: Ensure page is stable before clicking await this.waitForPageStable(`BEFORE_NEXT_${step.toUpperCase()}`, 15000) // Find button by test id or type submit const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() // CRITICAL: Verify button is ready const isReady = await this.verifyElementReady( 'button[data-testid="primaryButton"], button[type="submit"]', `NEXT_BUTTON_${step.toUpperCase()}`, 15000 ) if (!isReady) { log(false, 'CREATOR', 'Next button not ready, waiting longer...', 'warn', 'yellow') await this.humanDelay(3000, 5000) } // Ensure button is enabled const isEnabled = await nextBtn.isEnabled() if (!isEnabled) { log(false, 'CREATOR', 'Waiting for Next button to be enabled...', 'warn') await this.humanDelay(3000, 5000) } // Get current URL and page state before clicking const urlBefore = this.page.url() await nextBtn.click() log(false, 'CREATOR', `✅ Clicked Next (${step})`, 'log', 'green') // CRITICAL: Wait for page to process the click await this.humanDelay(3000, 5000) // CRITICAL: Wait for page to be stable after clicking await this.waitForPageStable(`AFTER_NEXT_${step.toUpperCase()}`, 20000) // CRITICAL: Verify the click was successful const urlAfter = this.page.url() let clickSuccessful = false if (urlBefore !== urlAfter) { log(false, 'CREATOR', `✅ Navigation detected: ${urlBefore} → ${urlAfter}`, 'log', 'green') clickSuccessful = true } else { log(false, 'CREATOR', `URL unchanged after clicking Next (${step})`, 'log', 'yellow') // URL didn't change - this might be OK if content changed // Wait a bit more and check for errors await this.humanDelay(2000, 3000) const hasErrors = !(await this.verifyNoErrors()) if (hasErrors) { log(false, 'CREATOR', `❌ Errors detected after clicking Next (${step})`, 'error') return false } // No errors - assume success (some pages don't change URL) log(false, 'CREATOR', `No errors detected, assuming Next (${step}) was successful`, 'log', 'yellow') clickSuccessful = true } return clickSuccessful } private async fillPassword(): Promise { await this.page.locator('h1[data-testid="title"]').first().waitFor({ timeout: 20000 }) await this.waitForPageStable('PASSWORD_PAGE', 15000) await this.humanDelay(1000, 2000) log(false, 'CREATOR', '🔐 Generating password...', 'log', 'cyan') const password = this.dataGenerator.generatePassword() const passwordInput = this.page.locator('input[type="password"]').first() await passwordInput.waitFor({ timeout: 15000 }) // CRITICAL: Retry fill with verification const passwordFillSuccess = await this.retryOperation( async () => { await passwordInput.clear() await this.humanDelay(800, 1500) // INCREASED from 500-1000 await passwordInput.fill(password) await this.humanDelay(1200, 2500) // INCREASED from 800-2000 // Verify value was filled correctly const verified = await this.verifyInputValue('input[type="password"]', password) if (!verified) { throw new Error('Password input value not verified') } return true }, 'PASSWORD_FILL', 3, 1000 ) if (!passwordFillSuccess) { log(false, 'CREATOR', 'Failed to fill password after retries', 'error') return null } log(false, 'CREATOR', '✅ Password filled (hidden for security)', 'log', 'green') return password } private async extractEmail(): Promise { // Multiple selectors for identity badge (language-independent) const badgeSelectors = [ '#bannerText', 'div[id="identityBadge"] div', 'div[data-testid="identityBanner"] div', 'div[class*="identityBanner"]', 'span[class*="identityText"]' ] for (const selector of badgeSelectors) { try { const badge = this.page.locator(selector).first() await badge.waitFor({ timeout: 5000 }) const email = await badge.textContent() if (email && email.includes('@')) { const cleanEmail = email.trim() log(false, 'CREATOR', `✅ Email extracted: ${cleanEmail}`, 'log', 'green') return cleanEmail } } catch { // Try next selector continue } } log(false, 'CREATOR', 'Could not find identity badge (not critical)', 'warn') return null } private async fillBirthdate(): Promise<{ day: number; month: number; year: number } | null> { log(false, 'CREATOR', '🎂 Filling birthdate...', 'log', 'cyan') await this.waitForPageStable('BIRTHDATE_PAGE', 15000) const birthdate = this.dataGenerator.generateBirthdate() try { await this.humanDelay(2000, 3000) // === DAY DROPDOWN === const dayButton = this.page.locator('button[name="BirthDay"], button#BirthDayDropdown').first() await dayButton.waitFor({ timeout: 15000, state: 'visible' }) log(false, 'CREATOR', 'Clicking day dropdown...', 'log') // CRITICAL: Retry click if it fails const dayClickSuccess = await this.retryOperation( async () => { await dayButton.click({ force: true }) await this.humanDelay(1500, 2500) // INCREASED delay // Verify dropdown opened const dayOptionsContainer = this.page.locator('div[role="listbox"], ul[role="listbox"]').first() const isOpen = await dayOptionsContainer.isVisible().catch(() => false) if (!isOpen) { throw new Error('Day dropdown did not open') } return true }, 'DAY_DROPDOWN_OPEN', 3, 1000 ) if (!dayClickSuccess) { log(false, 'CREATOR', 'Failed to open day dropdown after retries', 'error') return null } log(false, 'CREATOR', '✅ Day dropdown opened', 'log', 'green') // Select day from dropdown log(false, 'CREATOR', `Selecting day: ${birthdate.day}`, 'log') const dayOption = this.page.locator(`div[role="option"]:has-text("${birthdate.day}"), li[role="option"]:has-text("${birthdate.day}")`).first() await dayOption.waitFor({ timeout: 5000, state: 'visible' }) await dayOption.click() await this.humanDelay(1500, 2500) // INCREASED delay // CRITICAL: Wait for dropdown to FULLY close await this.waitForDropdownClosed('DAY_DROPDOWN', 8000) await this.humanDelay(2000, 3000) // INCREASED safety delay // === MONTH DROPDOWN === const monthButton = this.page.locator('button[name="BirthMonth"], button#BirthMonthDropdown').first() await monthButton.waitFor({ timeout: 10000, state: 'visible' }) log(false, 'CREATOR', 'Clicking month dropdown...', 'log') // CRITICAL: Retry click if it fails const monthClickSuccess = await this.retryOperation( async () => { await monthButton.click({ force: true }) await this.humanDelay(1500, 2500) // INCREASED delay // Verify dropdown opened const monthOptionsContainer = this.page.locator('div[role="listbox"], ul[role="listbox"]').first() const isOpen = await monthOptionsContainer.isVisible().catch(() => false) if (!isOpen) { throw new Error('Month dropdown did not open') } return true }, 'MONTH_DROPDOWN_OPEN', 3, 1000 ) if (!monthClickSuccess) { log(false, 'CREATOR', 'Failed to open month dropdown after retries', 'error') return null } log(false, 'CREATOR', '✅ Month dropdown opened', 'log', 'green') // Select month by data-value attribute or by position log(false, 'CREATOR', `Selecting month: ${birthdate.month}`, 'log') const monthOption = this.page.locator(`div[role="option"][data-value="${birthdate.month}"], li[role="option"][data-value="${birthdate.month}"]`).first() // Fallback: if data-value doesn't work, try by index const monthVisible = await monthOption.isVisible().catch(() => false) if (monthVisible) { await monthOption.click() log(false, 'CREATOR', '✅ Month selected by data-value', 'log', 'green') } else { log(false, 'CREATOR', `Fallback: selecting month by nth-child(${birthdate.month})`, 'warn', 'yellow') const monthOptionByIndex = this.page.locator(`div[role="option"]:nth-child(${birthdate.month}), li[role="option"]:nth-child(${birthdate.month})`).first() await monthOptionByIndex.click() } await this.humanDelay(1500, 2500) // INCREASED delay // CRITICAL: Wait for dropdown to FULLY close await this.waitForDropdownClosed('MONTH_DROPDOWN', 8000) await this.humanDelay(2000, 3000) // INCREASED safety delay // === YEAR INPUT === const yearInput = this.page.locator('input[name="BirthYear"], input[type="number"]').first() await yearInput.waitFor({ timeout: 10000, state: 'visible' }) log(false, 'CREATOR', `Filling year: ${birthdate.year}`, 'log') // CRITICAL: Retry fill with verification const yearFillSuccess = await this.retryOperation( async () => { await yearInput.clear() await this.humanDelay(500, 1000) await yearInput.fill(birthdate.year.toString()) await this.humanDelay(1000, 2000) // Verify value was filled correctly const verified = await this.verifyInputValue( 'input[name="BirthYear"], input[type="number"]', birthdate.year.toString() ) if (!verified) { throw new Error('Year input value not verified') } return true }, 'YEAR_FILL', 3, 1000 ) if (!yearFillSuccess) { log(false, 'CREATOR', 'Failed to fill year after retries', 'error') return null } log(false, 'CREATOR', `✅ Birthdate filled: ${birthdate.day}/${birthdate.month}/${birthdate.year}`, 'log', 'green') // CRITICAL: Verify no errors appeared after filling birthdate const noErrors = await this.verifyNoErrors() if (!noErrors) { log(false, 'CREATOR', '❌ Errors detected after filling birthdate', 'error') return null } // CRITICAL: Verify Next button is enabled (indicates form is valid) await this.humanDelay(1000, 2000) const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() const nextEnabled = await nextBtn.isEnabled().catch(() => false) if (!nextEnabled) { log(false, 'CREATOR', '⚠️ Next button not enabled after filling birthdate', 'warn', 'yellow') log(false, 'CREATOR', 'Waiting for form validation...', 'log', 'cyan') await this.humanDelay(3000, 5000) const retryEnabled = await nextBtn.isEnabled().catch(() => false) if (!retryEnabled) { log(false, 'CREATOR', '❌ Next button still disabled - form may be invalid', 'error') return null } } log(false, 'CREATOR', '✅ Birthdate form validated successfully', 'log', 'green') // CRITICAL: Extra safety delay before submitting await this.humanDelay(2000, 3000) return birthdate } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `Error filling birthdate: ${msg}`, 'error') return null } } private async fillNames(email: string): Promise<{ firstName: string; lastName: string } | null> { log(false, 'CREATOR', '👤 Filling name...', 'log', 'cyan') await this.waitForPageStable('NAMES_PAGE', 15000) const names = this.dataGenerator.generateNames(email) try { await this.humanDelay(1000, 2000) const firstNameSelectors = [ 'input[id*="firstName"]', 'input[name*="firstName"]', 'input[id*="first"]', 'input[name*="first"]', 'input[aria-label*="First"]', 'input[placeholder*="First"]' ] let firstNameInput = null for (const selector of firstNameSelectors) { const input = this.page.locator(selector).first() const visible = await input.isVisible().catch(() => false) if (visible) { firstNameInput = input break } } if (!firstNameInput) { log(false, 'CREATOR', 'Could not find first name input', 'error') return null } // CRITICAL: Retry fill with verification const firstNameFillSuccess = await this.retryOperation( async () => { await firstNameInput.clear() await this.humanDelay(800, 1500) // INCREASED from 500-1000 await firstNameInput.fill(names.firstName) await this.humanDelay(1200, 2500) // INCREASED from 800-2000 return true }, 'FIRSTNAME_FILL', 3, 1000 ) if (!firstNameFillSuccess) { log(false, 'CREATOR', 'Failed to fill first name after retries', 'error') return null } // Fill last name with multiple selector fallbacks const lastNameSelectors = [ 'input[id*="lastName"]', 'input[name*="lastName"]', 'input[id*="last"]', 'input[name*="last"]', 'input[aria-label*="Last"]', 'input[placeholder*="Last"]' ] let lastNameInput = null for (const selector of lastNameSelectors) { const input = this.page.locator(selector).first() const visible = await input.isVisible().catch(() => false) if (visible) { lastNameInput = input break } } if (!lastNameInput) { log(false, 'CREATOR', 'Could not find last name input', 'error') return null } // CRITICAL: Retry fill with verification const lastNameFillSuccess = await this.retryOperation( async () => { await lastNameInput.clear() await this.humanDelay(800, 1500) // INCREASED from 500-1000 await lastNameInput.fill(names.lastName) await this.humanDelay(1200, 2500) // INCREASED from 800-2000 return true }, 'LASTNAME_FILL', 3, 1000 ) if (!lastNameFillSuccess) { log(false, 'CREATOR', 'Failed to fill last name after retries', 'error') return null } log(false, 'CREATOR', `✅ Names filled: ${names.firstName} ${names.lastName}`, 'log', 'green') // CRITICAL: Uncheck marketing opt-in checkbox (decline promotional emails) await this.uncheckMarketingOptIn() // CRITICAL: Verify no errors appeared after filling names const noErrors = await this.verifyNoErrors() if (!noErrors) { log(false, 'CREATOR', '❌ Errors detected after filling names', 'error') return null } // CRITICAL: Verify Next button is enabled (indicates form is valid) await this.humanDelay(1000, 2000) const nextBtn = this.page.locator('button[data-testid="primaryButton"], button[type="submit"]').first() const nextEnabled = await nextBtn.isEnabled().catch(() => false) if (!nextEnabled) { log(false, 'CREATOR', '⚠️ Next button not enabled after filling names', 'warn', 'yellow') log(false, 'CREATOR', 'Waiting for form validation...', 'log', 'cyan') await this.humanDelay(3000, 5000) const retryEnabled = await nextBtn.isEnabled().catch(() => false) if (!retryEnabled) { log(false, 'CREATOR', '❌ Next button still disabled - form may be invalid', 'error') return null } } log(false, 'CREATOR', '✅ Names form validated successfully', 'log', 'green') return names } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `Error filling names: ${msg}`, 'error') return null } } private async uncheckMarketingOptIn(): Promise { try { log(false, 'CREATOR', 'Checking for marketing opt-in checkbox...', 'log', 'cyan') // Multiple selectors for the marketing checkbox const checkboxSelectors = [ 'input#marketingOptIn', 'input[data-testid="marketingOptIn"]', 'input[name="marketingOptIn"]', 'input[aria-label*="information, tips, and offers"]' ] let checkbox = null for (const selector of checkboxSelectors) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { checkbox = element log(false, 'CREATOR', `Found marketing checkbox with selector: ${selector}`, 'log', 'cyan') break } } if (!checkbox) { log(false, 'CREATOR', 'No marketing checkbox found (may not exist on this page)', 'log', 'gray') return } // Check if the checkbox is already checked const isChecked = await checkbox.isChecked().catch(() => false) if (isChecked) { log(false, 'CREATOR', 'Marketing checkbox is checked, unchecking it...', 'log', 'yellow') // Click to uncheck await checkbox.click() await this.humanDelay(500, 1000) // Verify it was unchecked const stillChecked = await checkbox.isChecked().catch(() => true) if (!stillChecked) { log(false, 'CREATOR', '✅ Marketing opt-in unchecked successfully', 'log', 'green') } else { log(false, 'CREATOR', '⚠️ Failed to uncheck marketing opt-in', 'warn', 'yellow') } } else { log(false, 'CREATOR', '✅ Marketing opt-in already unchecked', 'log', 'green') } } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `Marketing opt-in handling error: ${msg}`, 'warn', 'yellow') // Don't fail the whole process for this } } private async waitForCaptcha(): Promise { try { log(false, 'CREATOR', '🔍 Checking for CAPTCHA...', 'log', 'cyan') await this.humanDelay(1500, 2500) // Check for CAPTCHA iframe (most reliable) const captchaIframe = this.page.locator('iframe[data-testid="humanCaptchaIframe"]').first() const iframeVisible = await captchaIframe.isVisible().catch(() => false) if (iframeVisible) { log(false, 'CREATOR', '🤖 CAPTCHA DETECTED via iframe - WAITING FOR HUMAN', 'warn', 'yellow') return true } // Check multiple CAPTCHA indicators const captchaIndicators = [ 'h1[data-testid="title"]', 'div[id*="captcha"]', 'div[class*="captcha"]', 'div[id*="enforcement"]', 'img[data-testid="accessibleImg"]' ] for (const selector of captchaIndicators) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { const text = await element.textContent().catch(() => '') log(false, 'CREATOR', `Found element: ${selector} = "${text?.substring(0, 50)}"`, 'log', 'gray') if (text && ( text.toLowerCase().includes('vérif') || text.toLowerCase().includes('verify') || text.toLowerCase().includes('human') || text.toLowerCase().includes('humain') || text.toLowerCase().includes('puzzle') || text.toLowerCase().includes('captcha') || text.toLowerCase().includes('prove you') )) { log(false, 'CREATOR', `🤖 CAPTCHA DETECTED: "${text.substring(0, 50)}" - WAITING FOR HUMAN`, 'warn', 'yellow') return true } } } log(false, 'CREATOR', '✅ No CAPTCHA detected', 'log', 'green') return false } catch (error) { log(false, 'CREATOR', `Error checking CAPTCHA: ${error}`, 'warn', 'yellow') return false } } private async waitForCaptchaSolved(): Promise { const maxWaitTime = 10 * 60 * 1000 const startTime = Date.now() let lastLogTime = startTime while (Date.now() - startTime < maxWaitTime) { try { if (Date.now() - lastLogTime > 30000) { const elapsed = Math.floor((Date.now() - startTime) / 1000) log(false, 'CREATOR', `⏳ Still waiting for CAPTCHA solution... (${elapsed}s)`, 'log', 'yellow') lastLogTime = Date.now() } const captchaStillPresent = await this.waitForCaptcha() if (!captchaStillPresent) { log(false, 'CREATOR', '✅ CAPTCHA SOLVED! Processing account creation...', 'log', 'green') await this.humanDelay(3000, 5000) await this.waitForAccountCreation() await this.humanDelay(2000, 3000) return } await this.page.waitForTimeout(2000) } catch (error) { log(false, 'CREATOR', `Error in CAPTCHA wait: ${error}`, 'warn', 'yellow') return } } throw new Error('CAPTCHA timeout - 10 minutes exceeded') } private async handlePostCreationQuestions(): Promise { log(false, 'CREATOR', 'Handling post-creation questions...', 'log', 'cyan') // Wait for page to stabilize (REDUCED - pages load fast) await this.waitForPageStable('POST_CREATION', 15000) await this.humanDelay(3000, 5000) // CRITICAL: Handle passkey prompt - MUST REFUSE await this.handlePasskeyPrompt() // Brief delay between prompts await this.humanDelay(2000, 3000) // Handle "Stay signed in?" (KMSI) prompt const kmsiSelectors = [ '[data-testid="kmsiVideo"]', 'div:has-text("Stay signed in?")', 'div:has-text("Rester connecté")', 'button[data-testid="primaryButton"]' ] for (let i = 0; i < 3; i++) { let found = false for (const selector of kmsiSelectors) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', 'Stay signed in prompt detected', 'log', 'yellow') // Click "Yes" button const yesButton = this.page.locator('button[data-testid="primaryButton"]').first() const yesVisible = await yesButton.isVisible().catch(() => false) if (yesVisible) { await yesButton.click() await this.humanDelay(2000, 3000) await this.waitForPageStable('AFTER_KMSI', 15000) log(false, 'CREATOR', '✅ Accepted "Stay signed in"', 'log', 'green') found = true break } } } if (!found) break await this.humanDelay(1000, 2000) } // Handle any other prompts (biometric, etc.) const genericPrompts = [ '[data-testid="biometricVideo"]', 'button[id*="close"]', 'button[aria-label*="Close"]' ] for (const selector of genericPrompts) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `Closing prompt: ${selector}`, 'log', 'yellow') // Try to close it const closeButton = this.page.locator('button[data-testid="secondaryButton"], button[id*="close"]').first() const closeVisible = await closeButton.isVisible().catch(() => false) if (closeVisible) { await closeButton.click() await this.humanDelay(1500, 2500) log(false, 'CREATOR', '✅ Closed prompt', 'log', 'green') } } } log(false, 'CREATOR', '✅ Post-creation questions handled', 'log', 'green') } private async handlePasskeyPrompt(): Promise { log(false, 'CREATOR', 'Checking for passkey setup prompt...', 'log', 'cyan') // Wait for passkey prompt to appear (REDUCED) await this.humanDelay(3000, 5000) // Ensure page is stable before checking await this.waitForPageStable('PASSKEY_CHECK', 15000) // Multiple selectors for passkey prompt detection const passkeyDetectionSelectors = [ 'div:has-text("passkey")', 'div:has-text("clé d\'accès")', 'div:has-text("Set up a passkey")', 'div:has-text("Configurer une clé")', '[data-testid*="passkey"]', 'button:has-text("Skip")', 'button:has-text("Not now")', 'button:has-text("Ignorer")', 'button:has-text("Plus tard")' ] let passkeyPromptFound = false for (const selector of passkeyDetectionSelectors) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { passkeyPromptFound = true log(false, 'CREATOR', '⚠️ Passkey setup prompt detected - REFUSING', 'warn', 'yellow') break } } if (!passkeyPromptFound) { log(false, 'CREATOR', 'No passkey prompt detected', 'log', 'green') return } // Try to click refuse/skip buttons const refuseButtonSelectors = [ 'button:has-text("Skip")', 'button:has-text("Not now")', 'button:has-text("No")', 'button:has-text("Cancel")', 'button:has-text("Ignorer")', 'button:has-text("Plus tard")', 'button:has-text("Non")', 'button:has-text("Annuler")', 'button[data-testid="secondaryButton"]', 'button[id*="cancel"]', 'button[id*="skip"]' ] for (const selector of refuseButtonSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { log(false, 'CREATOR', `Clicking refuse button: ${selector}`, 'log', 'cyan') await button.click() await this.humanDelay(2000, 3000) log(false, 'CREATOR', '✅ Passkey setup REFUSED', 'log', 'green') return } } log(false, 'CREATOR', '⚠️ Could not find refuse button for passkey prompt', 'warn', 'yellow') } private async verifyAccountActive(): Promise { log(false, 'CREATOR', 'Verifying account is active...', 'log', 'cyan') // Ensure page is stable before navigating (REDUCED) await this.waitForPageStable('PRE_VERIFICATION', 10000) await this.humanDelay(3000, 5000) // Navigate to Bing Rewards try { log(false, 'CREATOR', 'Navigating to rewards.bing.com...', 'log', 'cyan') await this.page.goto('https://rewards.bing.com/', { waitUntil: 'networkidle', timeout: 30000 }) await this.waitForPageStable('REWARDS_PAGE', 7000) await this.humanDelay(2000, 3000) log(false, 'CREATOR', '✅ On rewards.bing.com', 'log', 'green') // Clear cookies on rewards page await this.dismissCookieBanner() // Handle "Get started" popup (ReferAndEarn) await this.humanDelay(2000, 3000) await this.handleGetStartedPopup() // Referral enrollment if needed if (this.referralUrl) { await this.humanDelay(2000, 3000) await this.ensureRewardsEnrollment() } } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `Warning: Could not verify account: ${msg}`, 'warn', 'yellow') } } private async dismissCookieBanner(): Promise { try { log(false, 'CREATOR', '🍪 Checking for cookie banner...', 'log', 'cyan') const rejectButtonSelectors = [ 'button#bnp_btn_reject', 'button[id*="reject"]', 'button:has-text("Reject")', 'button:has-text("Refuser")', 'a:has-text("Reject")', 'a:has-text("Refuser")' ] for (const selector of rejectButtonSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible({ timeout: 2000 }).catch(() => false) if (visible) { log(false, 'CREATOR', '✅ Rejecting cookies', 'log', 'green') await button.click() await this.humanDelay(1000, 1500) return } } log(false, 'CREATOR', 'No cookie banner found', 'log', 'gray') } catch (error) { log(false, 'CREATOR', `Cookie banner error: ${error}`, 'log', 'gray') } } private async handleGetStartedPopup(): Promise { try { log(false, 'CREATOR', '🎯 Checking for "Get started" popup...', 'log', 'cyan') await this.humanDelay(2000, 3000) // Check for ReferAndEarn popup const popupIndicator = this.page.locator('img[src*="ReferAndEarnPopUpImgUpdated"]').first() const popupVisible = await popupIndicator.isVisible({ timeout: 3000 }).catch(() => false) if (!popupVisible) { log(false, 'CREATOR', 'No "Get started" popup found', 'log', 'gray') return } log(false, 'CREATOR', '✅ Found "Get started" popup', 'log', 'green') await this.humanDelay(1000, 2000) // Click "Get started" button const getStartedButton = this.page.locator('a#reward_pivot_earn, a.dashboardPopUpPopUpSelectButton').first() const buttonVisible = await getStartedButton.isVisible({ timeout: 2000 }).catch(() => false) if (buttonVisible) { log(false, 'CREATOR', '🎯 Clicking "Get started"', 'log', 'cyan') await getStartedButton.click() await this.humanDelay(2000, 3000) await this.waitForPageStable('AFTER_GET_STARTED', 5000) log(false, 'CREATOR', '✅ Clicked "Get started"', 'log', 'green') } } catch (error) { log(false, 'CREATOR', `Get started popup error: ${error}`, 'log', 'gray') } } // Unused - kept for future use if needed /* private async handleRewardsWelcomeTour(): Promise { await this.waitForPageStable('WELCOME_TOUR', 7000) await this.humanDelay(2000, 3000) const maxClicks = 5 for (let i = 0; i < maxClicks; i++) { // Check for welcome tour indicators const welcomeIndicators = [ 'img[src*="Get%20cool%20prizes"]', 'img[alt*="Welcome to Microsoft Rewards"]', 'div.welcome-tour', 'a#fre-next-button', 'a.welcome-tour-button.next-button', 'a.next-button.c-call-to-action' ] let tourFound = false for (const selector of welcomeIndicators) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { tourFound = true log(false, 'CREATOR', `Welcome tour detected (step ${i + 1})`, 'log', 'yellow') break } } if (!tourFound) { log(false, 'CREATOR', 'No more welcome tour steps', 'log', 'green') break } // Try to click Next button const nextButtonSelectors = [ 'a#fre-next-button', 'a.welcome-tour-button.next-button', 'a.next-button.c-call-to-action', 'button:has-text("Next")', 'a:has-text("Next")', 'button:has-text("Suivant")', 'a:has-text("Suivant")' ] let clickedNext = false for (const selector of nextButtonSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { await button.click() await this.humanDelay(1500, 2500) await this.waitForPageStable('AFTER_TOUR_NEXT', 8000) clickedNext = true break } } if (!clickedNext) { const pinButtonSelectors = [ 'a#claim-button', 'a:has-text("Pin and start earning")', 'a:has-text("Épingler et commencer")', 'a.welcome-tour-button[href*="pin=true"]' ] for (const selector of pinButtonSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { await button.click() await this.humanDelay(1500, 2500) await this.waitForPageStable('AFTER_PIN', 8000) break } } break } await this.humanDelay(1000, 1500) } } */ /* private async handleRewardsPopups(): Promise { await this.waitForPageStable('REWARDS_POPUPS', 10000) await this.humanDelay(2000, 3000) // Handle ReferAndEarn popup const referralPopupSelectors = [ 'img[src*="ReferAndEarnPopUpImgUpdated"]', 'div.dashboardPopUp', 'a.dashboardPopUpPopUpSelectButton', 'a#reward_pivot_earn' ] let referralPopupFound = false for (const selector of referralPopupSelectors) { const element = this.page.locator(selector).first() const visible = await element.isVisible().catch(() => false) if (visible) { referralPopupFound = true log(false, 'CREATOR', 'Referral popup detected', 'log', 'yellow') break } } if (referralPopupFound) { // CRITICAL: Wait longer before clicking to ensure popup is fully loaded log(false, 'CREATOR', 'Referral popup found, waiting for it to stabilize (3-5s)...', 'log', 'cyan') await this.humanDelay(3000, 5000) // INCREASED from 2-3s // Click "Get started" button const getStartedSelectors = [ 'a.dashboardPopUpPopUpSelectButton', 'a#reward_pivot_earn', 'a:has-text("Get started")', 'a:has-text("Commencer")', 'button:has-text("Get started")', 'button:has-text("Commencer")' ] for (const selector of getStartedSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { await button.click() await this.humanDelay(1500, 2500) await this.waitForPageStable('AFTER_GET_STARTED', 8000) break } } } const genericCloseSelectors = [ 'button[aria-label*="Close"]', 'button[aria-label*="Fermer"]', 'button.close', 'a.close' ] for (const selector of genericCloseSelectors) { const button = this.page.locator(selector).first() const visible = await button.isVisible().catch(() => false) if (visible) { await button.click() await this.humanDelay(1000, 1500) await this.waitForPageStable('AFTER_CLOSE_POPUP', 5000) } } } */ private async ensureRewardsEnrollment(): Promise { if (!this.referralUrl) return try { log(false, 'CREATOR', '🔗 Reloading referral URL for enrollment...', 'log', 'cyan') await this.page.goto(this.referralUrl, { waitUntil: 'networkidle', timeout: 30000 }) await this.waitForPageStable('REFERRAL_ENROLLMENT', 7000) await this.humanDelay(2000, 3000) // Click "Join Microsoft Rewards" button const joinButton = this.page.locator('a#start-earning-rewards-link').first() const joinVisible = await joinButton.isVisible({ timeout: 3000 }).catch(() => false) if (joinVisible) { log(false, 'CREATOR', '🎯 Clicking "Join Microsoft Rewards"', 'log', 'cyan') await joinButton.click() await this.humanDelay(2000, 3000) await this.waitForPageStable('AFTER_JOIN', 7000) log(false, 'CREATOR', '✅ Successfully clicked Join button', 'log', 'green') } else { log(false, 'CREATOR', '✅ Already enrolled or Join button not found', 'log', 'gray') } log(false, 'CREATOR', '✅ Enrollment process completed', 'log', 'green') } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `Warning: Could not complete enrollment: ${msg}`, 'warn', 'yellow') } } private async saveAccount(account: CreatedAccount): Promise { try { const accountsDir = path.join(process.cwd(), 'accounts-created') // Ensure directory exists if (!fs.existsSync(accountsDir)) { log(false, 'CREATOR', 'Creating accounts-created directory...', 'log', 'cyan') fs.mkdirSync(accountsDir, { recursive: true }) } // Create a unique filename for THIS account using timestamp and email const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\./g, '-') const emailPrefix = (account.email.split('@')[0] || 'account').substring(0, 20) // First 20 chars of email const filename = `account_${emailPrefix}_${timestamp}.jsonc` const filepath = path.join(accountsDir, filename) log(false, 'CREATOR', `Saving account to NEW file: ${filename}`, 'log', 'cyan') // Create account data with metadata const accountData = { ...account, savedAt: new Date().toISOString(), filename: filename } // Create output with comments const output = `// Microsoft Rewards - Account Created // Email: ${account.email} // Created: ${account.createdAt} // Saved: ${accountData.savedAt} ${JSON.stringify(accountData, null, 2)}` // Write to NEW file (never overwrites existing files) fs.writeFileSync(filepath, output, 'utf-8') // Verify the file was written correctly if (fs.existsSync(filepath)) { const verifySize = fs.statSync(filepath).size log(false, 'CREATOR', `✅ File written successfully (${verifySize} bytes)`, 'log', 'green') // Double-check we can read it back const verifyContent = fs.readFileSync(filepath, 'utf-8') const verifyJsonStartIndex = verifyContent.indexOf('{') const verifyJsonEndIndex = verifyContent.lastIndexOf('}') if (verifyJsonStartIndex !== -1 && verifyJsonEndIndex !== -1) { const verifyJsonContent = verifyContent.substring(verifyJsonStartIndex, verifyJsonEndIndex + 1) const verifyAccount = JSON.parse(verifyJsonContent) if (verifyAccount.email === account.email) { log(false, 'CREATOR', `✅ Verification passed: Account ${account.email} saved correctly`, 'log', 'green') } else { log(false, 'CREATOR', '⚠️ Verification warning: Email mismatch', 'warn', 'yellow') } } } else { log(false, 'CREATOR', '❌ File verification failed - file does not exist!', 'error') } log(false, 'CREATOR', `✅ Account saved successfully to: ${filepath}`, 'log', 'green') } catch (error) { const msg = error instanceof Error ? error.message : String(error) log(false, 'CREATOR', `❌ Error saving account: ${msg}`, 'error') // Try to save to a fallback file try { const fallbackPath = path.join(process.cwd(), `account-backup-${Date.now()}.jsonc`) fs.writeFileSync(fallbackPath, JSON.stringify(account, null, 2), 'utf-8') log(false, 'CREATOR', `⚠️ Account saved to fallback file: ${fallbackPath}`, 'warn', 'yellow') } catch (fallbackError) { const fallbackMsg = fallbackError instanceof Error ? fallbackError.message : String(fallbackError) log(false, 'CREATOR', `❌ Failed to save fallback: ${fallbackMsg}`, 'error') } } } async close(): Promise { if (!this.rlClosed) { this.rl.close() this.rlClosed = true } if (this.page && !this.page.isClosed()) { await this.page.close() } } /** * Setup recovery email for the account */ private async setupRecoveryEmail(): Promise { try { log(false, 'CREATOR', '📧 Setting up recovery email...', 'log', 'cyan') // Navigate to proofs manage page await this.page.goto('https://account.live.com/proofs/manage/', { waitUntil: 'networkidle', timeout: 30000 }) await this.humanDelay(2000, 3000) // Check if we're on the "Add security info" page const addProofTitle = await this.page.locator('#iPageTitle').textContent().catch(() => '') if (!addProofTitle || !addProofTitle.includes('protect your account')) { log(false, 'CREATOR', 'Already on security dashboard', 'log', 'gray') return undefined } log(false, 'CREATOR', '🔒 Security setup page detected', 'log', 'yellow') // Get recovery email let recoveryEmailToUse = this.recoveryEmail if (!recoveryEmailToUse && !this.autoAccept) { recoveryEmailToUse = await this.askRecoveryEmail() } if (!recoveryEmailToUse) { log(false, 'CREATOR', 'Skipping recovery email setup', 'log', 'gray') return undefined } log(false, 'CREATOR', `Using recovery email: ${recoveryEmailToUse}`, 'log', 'cyan') // Fill email input const emailInput = this.page.locator('#EmailAddress').first() await emailInput.fill(recoveryEmailToUse) await this.humanDelay(500, 1000) // Click Next const nextButton = this.page.locator('#iNext').first() await nextButton.click() log(false, 'CREATOR', '📨 Code sent to recovery email', 'log', 'green') log(false, 'CREATOR', '⏳ Please enter the code you received and click Next', 'log', 'yellow') log(false, 'CREATOR', 'Waiting for you to complete verification...', 'log', 'cyan') // Wait for URL change (user completes verification) await this.page.waitForURL((url) => !url.href.includes('/proofs/Verify'), { timeout: 300000 }) log(false, 'CREATOR', '✅ Recovery email verified!', 'log', 'green') // Click OK on "Quick note" page if present await this.humanDelay(2000, 3000) const okButton = this.page.locator('button:has-text("OK")').first() const okVisible = await okButton.isVisible({ timeout: 5000 }).catch(() => false) if (okVisible) { await okButton.click() await this.humanDelay(1000, 2000) log(false, 'CREATOR', '✅ Clicked OK on info page', 'log', 'green') } return recoveryEmailToUse } catch (error) { log(false, 'CREATOR', `Recovery email setup error: ${error}`, 'warn', 'yellow') return undefined } } /** * Ask user for recovery email (interactive) */ private async askRecoveryEmail(): Promise { return new Promise((resolve) => { this.rl.question('📧 Enter recovery email (or press Enter to skip): ', (answer) => { const email = answer.trim() if (email && email.includes('@')) { resolve(email) } else { resolve(undefined) } }) }) } /** * Ask user if they want 2FA setup */ private async ask2FASetup(): Promise { return new Promise((resolve) => { this.rl.question('🔐 Enable two-factor authentication? (y/n): ', (answer) => { resolve(answer.trim().toLowerCase() === 'y') }) }) } /** * Setup 2FA with TOTP */ private async setup2FA(): Promise<{ totpSecret: string; recoveryCode: string | undefined } | undefined> { try { log(false, 'CREATOR', '🔐 Setting up 2FA...', 'log', 'cyan') // Navigate to 2FA setup page await this.page.goto('https://account.live.com/proofs/EnableTfa', { waitUntil: 'networkidle', timeout: 30000 }) await this.humanDelay(2000, 3000) // Click Next const submitButton = this.page.locator('#EnableTfaSubmit').first() await submitButton.click() await this.humanDelay(2000, 3000) // Click "set up a different Authenticator app" const altAppLink = this.page.locator('#iSelectProofTypeAlternate').first() const altAppVisible = await altAppLink.isVisible({ timeout: 5000 }).catch(() => false) if (altAppVisible) { await altAppLink.click() await this.humanDelay(2000, 3000) } // IMPROVED: Click "I can't scan the bar code" with fallback selectors log(false, 'CREATOR', '🔍 Looking for "I can\'t scan" link...', 'log', 'cyan') const cantScanSelectors = [ '#iShowPlainLink', // Primary 'a[href*="ShowPlain"]', // Link with ShowPlain in href 'button:has-text("can\'t scan")', // Button with text 'a:has-text("can\'t scan")', // Link with text 'a:has-text("Can\'t scan")', // Capitalized 'button:has-text("I can\'t scan the bar code")', // Full text 'a:has-text("I can\'t scan the bar code")' // Full text link ] let cantScanClicked = false for (const selector of cantScanSelectors) { try { const element = this.page.locator(selector).first() const isVisible = await element.isVisible({ timeout: 2000 }).catch(() => false) if (isVisible) { log(false, 'CREATOR', `✅ Found "I can't scan" using: ${selector}`, 'log', 'green') await element.click() cantScanClicked = true break } } catch { continue } } if (!cantScanClicked) { log(false, 'CREATOR', '⚠️ Could not find "I can\'t scan" link - trying to continue anyway', 'warn', 'yellow') } await this.humanDelay(2000, 3000) // Wait for UI to update and secret to appear // IMPROVED: Extract TOTP secret with multiple strategies log(false, 'CREATOR', '🔍 Searching for TOTP secret on page...', 'log', 'cyan') // Strategy 1: Wait for common TOTP secret selectors const secretSelectors = [ '#iTOTP_Secret', // Most common '#totpSecret', // Alternative 'input[name="secret"]', // Input field 'input[id*="secret"]', // Partial ID match 'input[id*="TOTP"]', // TOTP-related input '[data-bind*="secret"]', // Data binding 'div.text-block-body', // Text block (new UI) 'pre', // Pre-formatted text 'code' // Code block ] let totpSecret = '' let foundSelector = '' // Try each selector with explicit wait for (const selector of secretSelectors) { try { const element = this.page.locator(selector).first() const isVisible = await element.isVisible({ timeout: 2000 }).catch(() => false) if (isVisible) { // Try multiple extraction methods const methods = [ () => element.inputValue().catch(() => ''), // For input fields () => element.textContent().catch(() => ''), // For text elements () => element.innerText().catch(() => ''), // Alternative text () => element.getAttribute('value').catch(() => '') // Value attribute ] for (const method of methods) { const value = await method() const cleaned = value?.trim() || '' // TOTP secrets are typically 16-32 characters, base32 encoded (A-Z, 2-7) if (cleaned && cleaned.length >= 16 && cleaned.length <= 64 && /^[A-Z2-7]+$/i.test(cleaned)) { totpSecret = cleaned.toUpperCase() foundSelector = selector log(false, 'CREATOR', `✅ Found TOTP secret using selector: ${selector}`, 'log', 'green') break } } if (totpSecret) break } } catch { continue } } // Strategy 2: If not found, scan entire page content if (!totpSecret) { log(false, 'CREATOR', '🔍 Scanning entire page for TOTP pattern...', 'log', 'yellow') const pageContent = await this.page.content().catch(() => '') // Look for base32 patterns (16-32 chars, only A-Z and 2-7) const secretPattern = /\b([A-Z2-7]{16,64})\b/g const matches = pageContent.match(secretPattern) if (matches && matches.length > 0) { // Filter out common false positives (IDs, tokens that are too long) const candidates = matches.filter(m => m.length >= 16 && m.length <= 32) if (candidates.length > 0) { totpSecret = candidates[0]! foundSelector = 'page-scan' log(false, 'CREATOR', `✅ Found TOTP secret via page scan: ${totpSecret.substring(0, 4)}...`, 'log', 'green') } } } if (!totpSecret) { log(false, 'CREATOR', '❌ Could not find TOTP secret', 'error') // Take screenshot for debugging try { const screenshotPath = path.join(process.cwd(), 'totp-secret-not-found.png') await this.page.screenshot({ path: screenshotPath, fullPage: true }) log(false, 'CREATOR', `📸 Screenshot saved to: ${screenshotPath}`, 'log', 'cyan') } catch { log(false, 'CREATOR', '⚠️ Could not save debug screenshot', 'warn') } // Log page URL for manual check log(false, 'CREATOR', `📍 Current URL: ${this.page.url()}`, 'log', 'cyan') return undefined } log(false, 'CREATOR', `🔑 TOTP Secret: ${totpSecret} (found via: ${foundSelector})`, 'log', 'green') log(false, 'CREATOR', '⚠️ SAVE THIS SECRET - You will need it to generate codes!', 'warn', 'yellow') // Click "I'll scan a bar code instead" to go back to QR code view // (Same link, but now says "I'll scan a bar code instead") log(false, 'CREATOR', '🔄 Returning to QR code view...', 'log', 'cyan') const backToQRSelectors = [ '#iShowPlainLink', // Same element, different text now 'a:has-text("I\'ll scan")', // Text-based 'a:has-text("scan a bar code instead")', // Full text 'button:has-text("bar code instead")' // Button variant ] for (const selector of backToQRSelectors) { try { const element = this.page.locator(selector).first() const isVisible = await element.isVisible({ timeout: 2000 }).catch(() => false) if (isVisible) { await element.click() log(false, 'CREATOR', '✅ Returned to QR code view', 'log', 'green') break } } catch { continue } } await this.humanDelay(1000, 2000) log(false, 'CREATOR', '📱 Please scan the QR code with Google Authenticator or similar app', 'log', 'yellow') log(false, 'CREATOR', '⏳ Then enter the 6-digit code and click Next', 'log', 'cyan') log(false, 'CREATOR', 'Waiting for you to complete setup...', 'log', 'cyan') // Wait for "Two-step verification is turned on" page await this.page.waitForSelector('#RecoveryCode', { timeout: 300000 }) log(false, 'CREATOR', '✅ 2FA enabled!', 'log', 'green') // Extract recovery code const recoveryElement = this.page.locator('#NewRecoveryCode').first() const recoveryText = await recoveryElement.textContent().catch(() => '') || '' const recoveryMatch = recoveryText.match(/([A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5}-[A-Z0-9]{5})/) const recoveryCode = recoveryMatch ? recoveryMatch[1] : '' if (recoveryCode) { log(false, 'CREATOR', `🔐 Recovery Code: ${recoveryCode}`, 'log', 'green') log(false, 'CREATOR', '⚠️ SAVE THIS CODE - You can use it to recover your account!', 'warn', 'yellow') } else { log(false, 'CREATOR', '⚠️ Could not extract recovery code', 'warn', 'yellow') } // Click Next await this.humanDelay(2000, 3000) const recoveryNextButton = this.page.locator('#iOptTfaEnabledRecoveryCodeNext').first() await recoveryNextButton.click() // Click Next again await this.humanDelay(2000, 3000) const nextButton2 = this.page.locator('#iOptTfaEnabledNext').first() const next2Visible = await nextButton2.isVisible({ timeout: 3000 }).catch(() => false) if (next2Visible) { await nextButton2.click() await this.humanDelay(2000, 3000) } // Click Finish const finishButton = this.page.locator('#EnableTfaFinish').first() const finishVisible = await finishButton.isVisible({ timeout: 3000 }).catch(() => false) if (finishVisible) { await finishButton.click() await this.humanDelay(1000, 2000) } log(false, 'CREATOR', '✅ 2FA setup complete!', 'log', 'green') if (!totpSecret) { log(false, 'CREATOR', '❌ TOTP secret missing - 2FA may not work', 'error') return undefined } return { totpSecret, recoveryCode } } catch (error) { log(false, 'CREATOR', `2FA setup error: ${error}`, 'warn', 'yellow') return undefined } } }