diff --git a/src/account-creation/AccountCreator.ts b/src/account-creation/AccountCreator.ts index 049ebc6..e7255c4 100644 --- a/src/account-creation/AccountCreator.ts +++ b/src/account-creation/AccountCreator.ts @@ -4,10 +4,12 @@ import * as readline from 'readline' import type { BrowserContext, Page } from 'rebrowser-playwright' import { log } from '../util/notifications/Logger' import { DataGenerator } from './DataGenerator' +import { HumanBehavior } from './HumanBehavior' import { CreatedAccount } from './types' export class AccountCreator { private page!: Page + private human!: HumanBehavior private dataGenerator: DataGenerator private referralUrl?: string private recoveryEmail?: string @@ -27,10 +29,10 @@ export class AccountCreator { this.rlClosed = false } - // Human-like delay helper + // Human-like delay helper (DEPRECATED - use this.human.humanDelay() instead) + // Kept for backward compatibility during migration private async humanDelay(minMs: number, maxMs: number): Promise { - const delay = Math.random() * (maxMs - minMs) + minMs - await this.page.waitForTimeout(Math.floor(delay)) + await this.human.humanDelay(minMs, maxMs) } /** @@ -618,14 +620,25 @@ export class AccountCreator { try { this.page = await context.newPage() - log(false, 'CREATOR', '🚀 Starting account creation...', 'log', 'cyan') + // CRITICAL: Initialize human behavior simulator + this.human = new HumanBehavior(this.page) + + log(false, 'CREATOR', '🚀 Starting account creation with enhanced anti-detection...', 'log', 'cyan') // Navigate to signup page await this.navigateToSignup() + // CRITICAL: Simulate human reading the signup page + await this.human.microGestures('SIGNUP_PAGE') + await this.humanDelay(500, 1500) + // Click "Create account" button await this.clickCreateAccount() + // CRITICAL: Simulate human inspecting the email field + await this.human.microGestures('EMAIL_FIELD') + await this.humanDelay(300, 800) + // Generate email and fill it (handles suggestions automatically) const emailResult = await this.generateAndFillEmail(this.autoAccept) if (!emailResult) { @@ -635,6 +648,10 @@ export class AccountCreator { log(false, 'CREATOR', `✅ Email: ${emailResult}`, 'log', 'green') + // CRITICAL: Simulate human reading password requirements + await this.human.microGestures('PASSWORD_PAGE') + await this.humanDelay(500, 1200) + // Wait for password page and fill it const password = await this.fillPassword() if (!password) { @@ -653,6 +670,10 @@ export class AccountCreator { const finalEmail = await this.extractEmail() const confirmedEmail = finalEmail || emailResult + // CRITICAL: Simulate human inspecting birthdate fields + await this.human.microGestures('BIRTHDATE_PAGE') + await this.humanDelay(400, 1000) + // Fill birthdate const birthdate = await this.fillBirthdate() if (!birthdate) { @@ -667,6 +688,10 @@ export class AccountCreator { return null } + // CRITICAL: Simulate human inspecting name fields + await this.human.microGestures('NAMES_PAGE') + await this.humanDelay(400, 1000) + // Fill name fields const names = await this.fillNames(confirmedEmail) if (!names) { @@ -942,10 +967,8 @@ export class AccountCreator { // 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) + // CRITICAL FIX: Use humanType() instead of .fill() to avoid detection + await this.human.humanType(emailInput, email, 'EMAIL_INPUT') // SMART VERIFICATION: Check if Microsoft separated the domain const inputValue = await emailInput.inputValue().catch(() => '') @@ -1080,10 +1103,8 @@ export class AccountCreator { // 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) + // CRITICAL FIX: Use humanType() instead of .fill() to avoid detection + await this.human.humanType(emailInput, newEmail, 'EMAIL_RETRY') // SMART VERIFICATION: Microsoft may separate domain for managed email providers const inputValue = await emailInput.inputValue().catch(() => '') @@ -1158,10 +1179,8 @@ export class AccountCreator { const retryFillSuccess = await this.retryOperation( async () => { - await emailInput.clear() - await this.humanDelay(800, 1500) - await emailInput.fill(newEmail) - await this.humanDelay(1200, 2500) + // CRITICAL FIX: Use humanType() instead of .fill() to avoid detection + await this.human.humanType(emailInput, newEmail, 'EMAIL_AUTO_RETRY') // SMART VERIFICATION: Microsoft may separate domain for managed email providers const inputValue = await emailInput.inputValue().catch(() => '') @@ -1380,10 +1399,8 @@ export class AccountCreator { // 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 + // CRITICAL FIX: Use humanType() instead of .fill() to avoid detection + await this.human.humanType(passwordInput, password, 'PASSWORD_INPUT') // Verify value was filled correctly const verified = await this.verifyInputValue('input[type="password"]', password) @@ -1461,7 +1478,8 @@ export class AccountCreator { // CRITICAL: Retry click if it fails const dayClickSuccess = await this.retryOperation( async () => { - await dayButton.click({ force: true }) + // CRITICAL FIX: Use normal click (no force) to avoid bot detection + await dayButton.click({ timeout: 5000 }) await this.humanDelay(1500, 2500) // INCREASED delay // Verify dropdown opened @@ -1506,7 +1524,8 @@ export class AccountCreator { // CRITICAL: Retry click if it fails const monthClickSuccess = await this.retryOperation( async () => { - await monthButton.click({ force: true }) + // CRITICAL FIX: Use normal click (no force) to avoid bot detection + await monthButton.click({ timeout: 5000 }) await this.humanDelay(1500, 2500) // INCREASED delay // Verify dropdown opened @@ -1560,10 +1579,8 @@ export class AccountCreator { // 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) + // CRITICAL FIX: Use humanType() instead of .fill() to avoid detection + await this.human.humanType(yearInput, birthdate.year.toString(), 'YEAR_INPUT') // Verify value was filled correctly const verified = await this.verifyInputValue( @@ -1667,10 +1684,8 @@ export class AccountCreator { // 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 + // CRITICAL FIX: Use humanType() instead of .fill() to avoid detection + await this.human.humanType(firstNameInput, names.firstName, 'FIRSTNAME_INPUT') return true }, @@ -1712,10 +1727,8 @@ export class AccountCreator { // 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 + // CRITICAL FIX: Use humanType() instead of .fill() to avoid detection + await this.human.humanType(lastNameInput, names.lastName, 'LASTNAME_INPUT') return true }, @@ -2701,8 +2714,7 @@ ${JSON.stringify(accountData, null, 2)}` // Fill email input const emailInput = this.page.locator('#EmailAddress').first() - await emailInput.fill(recoveryEmailToUse) - await this.humanDelay(500, 1000) + await this.human.humanType(emailInput, recoveryEmailToUse, 'RECOVERY_EMAIL') // Click Next const nextButton = this.page.locator('#iNext').first() diff --git a/src/account-creation/HumanBehavior.ts b/src/account-creation/HumanBehavior.ts new file mode 100644 index 0000000..e1bc170 --- /dev/null +++ b/src/account-creation/HumanBehavior.ts @@ -0,0 +1,248 @@ +/** + * Human Behavior Simulator for Account Creation + * + * CRITICAL: Microsoft detects bots by analyzing: + * 1. Typing speed (instant .fill() = bot, gradual .type() = human) + * 2. Mouse movements (no movement = bot, random moves = human) + * 3. Pauses (fixed delays = bot, variable pauses = human) + * 4. Click patterns (force clicks = bot, natural clicks = human) + * + * This module ensures account creation is INDISTINGUISHABLE from manual creation. + */ + +import type { Page } from 'rebrowser-playwright' +import { log } from '../util/notifications/Logger' + +export class HumanBehavior { + private page: Page + + constructor(page: Page) { + this.page = page + } + + /** + * Human-like delay with natural variance + * Unlike fixed delays, humans vary greatly in timing + * + * @param minMs Minimum delay + * @param maxMs Maximum delay + * @param context Description for logging (optional) + */ + async humanDelay(minMs: number, maxMs: number, context?: string): Promise { + // IMPROVEMENT: Add occasional "thinking" pauses (10% chance of 2x delay) + const shouldThink = Math.random() < 0.1 + const multiplier = shouldThink ? 2 : 1 + + const delay = (Math.random() * (maxMs - minMs) + minMs) * multiplier + + if (shouldThink && context) { + log(false, 'CREATOR', `[${context}] 🤔 Thinking pause (${Math.floor(delay)}ms)`, 'log', 'cyan') + } + + await this.page.waitForTimeout(Math.floor(delay)) + } + + /** + * CRITICAL: Type text naturally like a human + * NEVER use .fill() - it's instant and detectable + * + * @param locator Playwright locator (input field) + * @param text Text to type + * @param context Description for logging + */ + async humanType(locator: import('rebrowser-playwright').Locator, text: string, context: string): Promise { + // CRITICAL: Clear field first (human would select all + delete) + await locator.clear() + await this.humanDelay(300, 800, context) + + // CRITICAL: Type character by character with VARIABLE delays + // Real humans type at 40-80 WPM = ~150-300ms per character + // But with natural variation: some characters faster, some slower + + log(false, 'CREATOR', `[${context}] ⌨️ Typing: "${text.substring(0, 20)}${text.length > 20 ? '...' : ''}"`, 'log', 'cyan') + + for (let i = 0; i < text.length; i++) { + const char: string = text[i] as string + + // CRITICAL: Skip if char is somehow undefined (defensive programming) + if (!char) continue + + // NATURAL VARIANCE: + // - Fast keys: common letters (e, a, t, i, o, n) = 80-150ms + // - Slow keys: numbers, symbols, shift combos = 200-400ms + // - Occasional typos: 5% chance of longer pause (user correcting) + + let charDelay: number + const isFastKey = /[eatino]/i.test(char) + const isSlowKey = /[^a-z]/i.test(char) // Numbers, symbols, etc. + const hasTyro = Math.random() < 0.05 // 5% typo simulation + + if (hasTyro) { + charDelay = Math.random() * 400 + 300 // 300-700ms (correcting typo) + } else if (isFastKey) { + charDelay = Math.random() * 70 + 80 // 80-150ms + } else if (isSlowKey) { + charDelay = Math.random() * 200 + 200 // 200-400ms + } else { + charDelay = Math.random() * 100 + 120 // 120-220ms + } + + await locator.type(char, { delay: 0 }) // Type instantly + await this.page.waitForTimeout(Math.floor(charDelay)) + } + + log(false, 'CREATOR', `[${context}] ✅ Typing completed`, 'log', 'green') + + // IMPROVEMENT: Random pause after typing (human reviewing input) + await this.humanDelay(500, 1500, context) + } + + /** + * CRITICAL: Simulate micro mouse movements and scrolls + * Real humans constantly move mouse and scroll while reading/thinking + * + * @param context Description for logging + */ + async microGestures(context: string): Promise { + try { + // 60% chance of mouse movement (humans move mouse A LOT) + if (Math.random() < 0.6) { + const x = Math.floor(Math.random() * 200) + 50 // Random x: 50-250px + const y = Math.floor(Math.random() * 150) + 30 // Random y: 30-180px + const steps = Math.floor(Math.random() * 5) + 3 // 3-8 steps (smooth movement) + + await this.page.mouse.move(x, y, { steps }).catch(() => { + // Mouse move failed - page may be closed or unavailable + }) + + // VERBOSE logging disabled - too noisy + // log(false, 'CREATOR', `[${context}] 🖱️ Mouse moved to (${x}, ${y})`, 'log', 'gray') + } + + // 30% chance of scroll (humans scroll to read content) + if (Math.random() < 0.3) { + const direction = Math.random() < 0.7 ? 1 : -1 // 70% down, 30% up + const distance = Math.floor(Math.random() * 200) + 50 // 50-250px + const dy = direction * distance + + await this.page.mouse.wheel(0, dy).catch(() => { + // Scroll failed - page may be closed or unavailable + }) + + // VERBOSE logging disabled - too noisy + // log(false, 'CREATOR', `[${context}] 📜 Scrolled ${direction > 0 ? 'down' : 'up'} ${distance}px`, 'log', 'gray') + } + } catch { + // Gesture execution failed - not critical for operation + } + } + + /** + * CRITICAL: Natural click with human behavior + * NEVER use { force: true } - it bypasses visibility checks (bot pattern) + * + * @param locator Playwright locator (button/link) + * @param context Description for logging + * @param maxRetries Max click attempts (default: 3) + * @returns true if click succeeded, false otherwise + */ + async humanClick( + locator: import('rebrowser-playwright').Locator, + context: string, + maxRetries: number = 3 + ): Promise { + for (let attempt = 1; attempt <= maxRetries; attempt++) { + try { + // CRITICAL: Move mouse to element first (real humans do this) + const box = await locator.boundingBox().catch(() => null) + if (box) { + // Click at random position within element (not always center) + const offsetX = Math.random() * box.width * 0.6 + box.width * 0.2 // 20-80% of width + const offsetY = Math.random() * box.height * 0.6 + box.height * 0.2 // 20-80% of height + + await this.page.mouse.move( + box.x + offsetX, + box.y + offsetY, + { steps: Math.floor(Math.random() * 3) + 2 } // 2-5 steps + ).catch(() => { }) + + await this.humanDelay(100, 300, context) // Pause before clicking + } + + // NATURAL CLICK: No force (respects visibility/interactability) + await locator.click({ force: false, timeout: 5000 }) + + log(false, 'CREATOR', `[${context}] ✅ Clicked successfully`, 'log', 'green') + await this.humanDelay(300, 800, context) // Pause after clicking + return true + + } catch (error) { + if (attempt < maxRetries) { + log(false, 'CREATOR', `[${context}] ⚠️ Click failed (attempt ${attempt}/${maxRetries}), retrying...`, 'warn', 'yellow') + await this.humanDelay(1000, 2000, context) + } else { + const msg = error instanceof Error ? error.message : String(error) + log(false, 'CREATOR', `[${context}] ❌ Click failed after ${maxRetries} attempts: ${msg}`, 'error') + return false + } + } + } + + return false + } + + /** + * CRITICAL: Simulate human "reading" the page + * Real humans pause to read content before interacting + * + * @param context Description for logging + */ + async readPage(context: string): Promise { + log(false, 'CREATOR', `[${context}] 👀 Reading page...`, 'log', 'cyan') + + // Random scroll movements (humans scroll while reading) + const scrollCount = Math.floor(Math.random() * 3) + 1 // 1-3 scrolls + for (let i = 0; i < scrollCount; i++) { + await this.microGestures(context) + await this.humanDelay(800, 2000, context) + } + + // Final reading pause + await this.humanDelay(1500, 3500, context) + } + + /** + * CRITICAL: Simulate dropdown interaction (more complex than simple clicks) + * Real humans: move mouse → hover → click → wait → select option + * + * @param buttonLocator Dropdown button locator + * @param optionLocator Option to select locator + * @param context Description for logging + * @returns true if interaction succeeded, false otherwise + */ + async humanDropdownSelect( + buttonLocator: import('rebrowser-playwright').Locator, + optionLocator: import('rebrowser-playwright').Locator, + context: string + ): Promise { + // STEP 1: Click dropdown button (with human behavior) + const openSuccess = await this.humanClick(buttonLocator, `${context}_OPEN`) + if (!openSuccess) return false + + // STEP 2: Wait for dropdown to open (visual feedback) + await this.humanDelay(500, 1200, context) + + // STEP 3: Move mouse randomly inside dropdown (human reading options) + await this.microGestures(context) + await this.humanDelay(300, 800, context) + + // STEP 4: Click selected option (with human behavior) + const selectSuccess = await this.humanClick(optionLocator, `${context}_SELECT`) + if (!selectSuccess) return false + + // STEP 5: Wait for dropdown to close + await this.humanDelay(500, 1200, context) + + return true + } +}