Files
Microsoft-Rewards-Bot/src/account-creation/AccountCreator.ts
2025-11-10 22:11:52 +01:00

2651 lines
94 KiB
TypeScript

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<void> {
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<ReturnType<Page['locator']> | 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<T>(
operation: () => Promise<T>,
context: string,
maxRetries: number = 3,
initialDelayMs: number = 1000
): Promise<T | null> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<boolean> {
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<string> {
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<boolean> {
// 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<boolean> {
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<boolean> {
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<CreatedAccount | null> {
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<void> {
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<void> {
// 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<string | null> {
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<boolean> {
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<string | null> {
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<string | null> {
// 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<void> {
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<boolean> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<void> {
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<string | undefined> {
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<string | undefined> {
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<boolean> {
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
}
}
}