From 476fff7116733334f54dee73a6df93444107df1f Mon Sep 17 00:00:00 2001 From: LightZirconite Date: Sun, 16 Nov 2025 19:00:27 +0100 Subject: [PATCH] fix: enhance human-like behavior in account creation process with variable delays and improved randomness --- src/account-creation/AccountCreator.ts | 449 ++++++++++++++++--------- src/account-creation/DataGenerator.ts | 56 +-- src/account-creation/HumanBehavior.ts | 74 ++-- 3 files changed, 380 insertions(+), 199 deletions(-) diff --git a/src/account-creation/AccountCreator.ts b/src/account-creation/AccountCreator.ts index e7255c4..7c70987 100644 --- a/src/account-creation/AccountCreator.ts +++ b/src/account-creation/AccountCreator.ts @@ -77,7 +77,8 @@ export class AccountCreator { */ /** - * UTILITY: Retry an async operation with exponential backoff + * UTILITY: Retry an async operation with HUMAN-LIKE random delays + * IMPROVED: Avoid exponential backoff (too predictable = bot pattern) */ private async retryOperation( operation: () => Promise, @@ -91,8 +92,19 @@ export class AccountCreator { return result } catch (error) { if (attempt < maxRetries) { - const delayMs = initialDelayMs * Math.pow(2, attempt - 1) - await this.page.waitForTimeout(delayMs) + // IMPROVED: Human-like variable delays (not exponential) + // Real humans retry at inconsistent intervals + const baseDelay = initialDelayMs + Math.random() * 500 + const variance = Math.random() * 1000 - 500 // ±500ms random jitter + const humanDelay = baseDelay + (attempt * 800) + variance // Gradual increase with randomness + + log(false, 'CREATOR', `[${context}] Retry ${attempt}/${maxRetries} after ${Math.floor(humanDelay)}ms`, 'warn', 'yellow') + await this.page.waitForTimeout(Math.floor(humanDelay)) + + // IMPROVED: Random micro-gesture during retry (human frustration pattern) + if (Math.random() < 0.4) { + await this.human.microGestures(`${context}_RETRY_${attempt}`) + } } else { return null } @@ -104,6 +116,7 @@ export class AccountCreator { /** * CRITICAL: Wait for dropdown to be fully closed before continuing + * IMPROVED: Detect Fluent UI dropdown states reliably */ private async waitForDropdownClosed(context: string, maxWaitMs: number = 5000): Promise { log(false, 'CREATOR', `[${context}] Waiting for dropdown to close...`, 'log', 'cyan') @@ -111,13 +124,16 @@ export class AccountCreator { const startTime = Date.now() while (Date.now() - startTime < maxWaitMs) { - // Check if any dropdown menu is visible + // UPDATED: Check for Fluent UI dropdown containers (new classes) const dropdownSelectors = [ 'div[role="listbox"]', 'ul[role="listbox"]', 'div[role="menu"]', 'ul[role="menu"]', - '[class*="dropdown"][class*="open"]' + 'div.fui-Listbox', // NEW: Fluent UI specific + 'div.fui-Menu', // NEW: Fluent UI specific + '[class*="dropdown"][class*="open"]', + '[aria-expanded="true"]' // NEW: Better detection ] let anyVisible = false @@ -130,12 +146,18 @@ export class AccountCreator { } if (!anyVisible) { - return true + // IMPROVED: Extra verification - check aria-expanded on buttons + const expandedButtons = await this.page.locator('button[aria-expanded="true"]').count().catch(() => 0) + if (expandedButtons === 0) { + log(false, 'CREATOR', `[${context}] ✅ Dropdown confirmed closed`, 'log', 'green') + return true + } } await this.page.waitForTimeout(500) } + log(false, 'CREATOR', `[${context}] ⚠️ Dropdown still visible after ${maxWaitMs}ms`, 'warn', 'yellow') return false } @@ -628,15 +650,27 @@ export class AccountCreator { // Navigate to signup page await this.navigateToSignup() - // CRITICAL: Simulate human reading the signup page + // IMPROVED: Random gestures NOT always followed by actions await this.human.microGestures('SIGNUP_PAGE') await this.humanDelay(500, 1500) + // IMPROVED: Sometimes extra gesture without action (human browsing) + if (Math.random() < 0.3) { + await this.human.microGestures('SIGNUP_PAGE_READING') + await this.humanDelay(1200, 2500) + } + // Click "Create account" button await this.clickCreateAccount() - // CRITICAL: Simulate human inspecting the email field - await this.human.microGestures('EMAIL_FIELD') + // IMPROVED: Variable delay before inspecting email (not always immediate) + const preEmailDelay: [number, number] = Math.random() < 0.5 ? [800, 1500] : [300, 800] + await this.humanDelay(preEmailDelay[0], preEmailDelay[1]) + + // CRITICAL: Sometimes NO gesture (humans don't always move mouse) + if (Math.random() < 0.7) { + await this.human.microGestures('EMAIL_FIELD') + } await this.humanDelay(300, 800) // Generate email and fill it (handles suggestions automatically) @@ -648,8 +682,10 @@ export class AccountCreator { log(false, 'CREATOR', `✅ Email: ${emailResult}`, 'log', 'green') - // CRITICAL: Simulate human reading password requirements - await this.human.microGestures('PASSWORD_PAGE') + // IMPROVED: Variable behavior before password (not always gesture) + if (Math.random() < 0.6) { + await this.human.microGestures('PASSWORD_PAGE') + } await this.humanDelay(500, 1200) // Wait for password page and fill it @@ -670,8 +706,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') + // IMPROVED: Random reading pattern (not always gesture) + if (Math.random() < 0.65) { + await this.human.microGestures('BIRTHDATE_PAGE') + } await this.humanDelay(400, 1000) // Fill birthdate @@ -688,9 +726,14 @@ export class AccountCreator { return null } - // CRITICAL: Simulate human inspecting name fields - await this.human.microGestures('NAMES_PAGE') - await this.humanDelay(400, 1000) + // IMPROVED: Variable inspection behavior + if (Math.random() < 0.55) { + await this.human.microGestures('NAMES_PAGE') + await this.humanDelay(400, 1000) + } else { + // Sometimes just pause without gesture + await this.humanDelay(800, 1500) + } // Fill name fields const names = await this.fillNames(confirmedEmail) @@ -1469,141 +1512,40 @@ export class AccountCreator { 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' }) + // CRITICAL: Microsoft changed order - MONTH must be filled BEFORE DAY + // Detect order by checking which dropdown appears first in DOM + const monthFirst = await this.page.locator('button#BirthMonthDropdown').first().boundingBox().then(box => box?.y ?? 0).catch(() => 0) + const dayFirst = await this.page.locator('button#BirthDayDropdown').first().boundingBox().then(box => box?.y ?? 0).catch(() => 0) - log(false, 'CREATOR', 'Clicking day dropdown...', 'log') + const monthBeforeDay = monthFirst > 0 && monthFirst < dayFirst - // CRITICAL: Retry click if it fails - const dayClickSuccess = await this.retryOperation( - async () => { - // 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 - 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 () => { - // 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 - 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') + if (monthBeforeDay) { + log(false, 'CREATOR', '🔄 Detected MONTH-FIRST layout (new Microsoft UI)', 'log', 'cyan') } 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() + log(false, 'CREATOR', '📅 Detected DAY-FIRST layout (old Microsoft UI)', 'log', 'cyan') } - 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 + // === FILL IN CORRECT ORDER === + if (monthBeforeDay) { + // NEW ORDER: MONTH → DAY → YEAR + const monthResult = await this.fillMonthDropdown(birthdate.month) + if (!monthResult) return null - // === YEAR INPUT === - const yearInput = this.page.locator('input[name="BirthYear"], input[type="number"]').first() - await yearInput.waitFor({ timeout: 10000, state: 'visible' }) + const dayResult = await this.fillDayDropdown(birthdate.day) + if (!dayResult) return null + } else { + // OLD ORDER: DAY → MONTH → YEAR + const dayResult = await this.fillDayDropdown(birthdate.day) + if (!dayResult) return null - log(false, 'CREATOR', `Filling year: ${birthdate.year}`, 'log') - - // CRITICAL: Retry fill with verification - const yearFillSuccess = await this.retryOperation( - async () => { - // 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( - '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 + const monthResult = await this.fillMonthDropdown(birthdate.month) + if (!monthResult) return null } + // === YEAR INPUT (always last) === + const yearResult = await this.fillYearInput(birthdate.year) + if (!yearResult) return null + log(false, 'CREATOR', `✅ Birthdate filled: ${birthdate.day}/${birthdate.month}/${birthdate.year}`, 'log', 'green') // CRITICAL: Verify no errors appeared after filling birthdate @@ -1644,6 +1586,205 @@ export class AccountCreator { } } + /** + * EXTRACTED: Fill day dropdown (reusable for both orders) + */ + private async fillDayDropdown(day: number): Promise { + try { + // === DAY DROPDOWN === + // UPDATED: Microsoft changed HTML - new Fluent UI classes (___w2njya0, etc.) + const dayButton = this.page.locator('button#BirthDayDropdown, button[name="BirthDay"], button.fui-Dropdown__button[aria-label*="day"]').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 () => { + // 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 + // UPDATED: Check for Fluent UI dropdown container + const dayOptionsContainer = this.page.locator('div[role="listbox"], ul[role="listbox"], div.fui-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 false + } + + log(false, 'CREATOR', '✅ Day dropdown opened', 'log', 'green') + + // Select day from dropdown + log(false, 'CREATOR', `Selecting day: ${day}`, 'log') + // UPDATED: Fluent UI uses div[role="option"] with exact text matching + const dayOption = this.page.locator(`div[role="option"]:text-is("${day}"), div[role="option"]:has-text("${day}"), li[role="option"]:has-text("${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(3500, 5500) // IMPROVED: Longer delay (humans take time between dropdowns) + + // CRITICAL: Verify page is interactive (not animating) + await this.waitForPageStable('AFTER_DAY_DROPDOWN', 5000) + await this.humanDelay(1500, 2500) // Additional reading pause + + return true + } catch (error) { + const msg = error instanceof Error ? error.message : String(error) + log(false, 'CREATOR', `Error filling day dropdown: ${msg}`, 'error') + return false + } + } + + /** + * EXTRACTED: Fill month dropdown (reusable for both orders) + */ + private async fillMonthDropdown(month: number): Promise { + try { + // === MONTH DROPDOWN === + // UPDATED: Microsoft changed HTML - new Fluent UI classes + const monthButton = this.page.locator('button#BirthMonthDropdown, button[name="BirthMonth"], button.fui-Dropdown__button[aria-label*="month"]').first() + await monthButton.waitFor({ timeout: 10000, state: 'visible' }) + + // CRITICAL: Verify button is actually clickable (not disabled, not covered) + const monthEnabled = await monthButton.isEnabled().catch(() => false) + if (!monthEnabled) { + log(false, 'CREATOR', 'Month button not enabled yet, waiting...', 'warn', 'yellow') + await this.humanDelay(3000, 5000) + } + + log(false, 'CREATOR', 'Clicking month dropdown...', 'log') + + // CRITICAL: Retry click with RANDOMIZED delays (avoid pattern detection) + const monthClickSuccess = await this.retryOperation( + async () => { + // IMPROVED: Random micro-gesture before click (human-like) + await this.human.microGestures('MONTH_DROPDOWN_PRE_CLICK') + await this.humanDelay(500, 1200) + + // CRITICAL FIX: Use normal click (no force) to avoid bot detection + await monthButton.click({ timeout: 5000 }) + + // IMPROVED: Variable delay after click (avoid predictability) + const postClickDelay: [number, number] = Math.random() < 0.3 ? [2500, 4000] : [1500, 2500] + await this.humanDelay(postClickDelay[0], postClickDelay[1]) + + // Verify dropdown opened + // UPDATED: Fluent UI listbox detection + const monthOptionsContainer = this.page.locator('div[role="listbox"], ul[role="listbox"], div.fui-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 false + } + + log(false, 'CREATOR', '✅ Month dropdown opened', 'log', 'green') + + // Select month by data-value attribute or by position + log(false, 'CREATOR', `Selecting month: ${month}`, 'log') + // UPDATED: Try multiple strategies for Fluent UI month selection + const monthOption = this.page.locator(`div[role="option"][data-value="${month}"], div[role="option"]:nth-child(${month}), li[role="option"][data-value="${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', 'log', 'green') + } else { + log(false, 'CREATOR', `Fallback: selecting month by nth-child(${month})`, 'warn', 'yellow') + const monthOptionByIndex = this.page.locator(`div[role="option"]:nth-child(${month}), li[role="option"]:nth-child(${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 + + return true + } catch (error) { + const msg = error instanceof Error ? error.message : String(error) + log(false, 'CREATOR', `Error filling month dropdown: ${msg}`, 'error') + return false + } + } + + /** + * EXTRACTED: Fill year input (always last) + */ + private async fillYearInput(year: number): Promise { + try { + // === YEAR INPUT === + // UPDATED: Fluent UI year input (class fui-Input__input) + const yearInput = this.page.locator('input[name="BirthYear"], input[type="number"], input.fui-Input__input[aria-label*="year"]').first() + await yearInput.waitFor({ timeout: 10000, state: 'visible' }) + + log(false, 'CREATOR', `Filling year: ${year}`, 'log') + + // CRITICAL: Retry fill with verification + const yearFillSuccess = await this.retryOperation( + async () => { + // CRITICAL FIX: Use humanType() instead of .fill() to avoid detection + await this.human.humanType(yearInput, year.toString(), 'YEAR_INPUT') + + // Verify value was filled correctly + const verified = await this.verifyInputValue( + 'input[name="BirthYear"], input[type="number"], input.fui-Input__input[aria-label*="year"]', + 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 false + } + + return true + } catch (error) { + const msg = error instanceof Error ? error.message : String(error) + log(false, 'CREATOR', `Error filling year input: ${msg}`, 'error') + return false + } + } + private async fillNames(email: string): Promise<{ firstName: string; lastName: string } | null> { log(false, 'CREATOR', '👤 Filling name...', 'log', 'cyan') @@ -1872,10 +2013,14 @@ export class AccountCreator { // IMPROVED: Try multiple click strategies for Fluent UI checkboxes let unchecked = false - // Strategy 1: Normal Playwright click + // Strategy 1: Normal Playwright click (HUMAN-LIKE: no force) try { + // IMPROVED: Random micro-gesture before click + await this.human.microGestures('CHECKBOX_PRE_CLICK') + await this.humanDelay(300, 700) + await checkbox.click({ force: false }) - await this.humanDelay(500, 800) + await this.humanDelay(500, 1200) // IMPROVED: Variable delay const stillChecked1 = await this.isCheckboxChecked(checkbox) if (!stillChecked1) { unchecked = true @@ -1885,20 +2030,8 @@ export class AccountCreator { // Continue to next strategy } - // Strategy 2: Force click (bypass visibility checks) - if (!unchecked) { - try { - await checkbox.click({ force: true }) - await this.humanDelay(500, 800) - const stillChecked2 = await this.isCheckboxChecked(checkbox) - if (!stillChecked2) { - unchecked = true - log(false, 'CREATOR', '✅ Unchecked via force click', 'log', 'green') - } - } catch { - // Continue to next strategy - } - } + // Strategy 2: REMOVED FORCE CLICK (detectable as bot behavior) + // Strategy 2 is now Label click (Fluent UI native pattern) // Strategy 3: Click the label instead (Fluent UI pattern) if (!unchecked) { diff --git a/src/account-creation/DataGenerator.ts b/src/account-creation/DataGenerator.ts index 67100b2..c0eb91b 100644 --- a/src/account-creation/DataGenerator.ts +++ b/src/account-creation/DataGenerator.ts @@ -1,18 +1,22 @@ import { getRandomFirstName, getRandomLastName } from './nameDatabase' export class DataGenerator { - + generateEmail(customFirstName?: string, customLastName?: string): string { const firstName = customFirstName || getRandomFirstName() const lastName = customLastName || getRandomLastName() - + const cleanFirst = firstName.toLowerCase().replace(/[^a-z]/g, '') const cleanLast = lastName.toLowerCase().replace(/[^a-z]/g, '') - - // More realistic patterns + + // IMPROVED: More variations to avoid pattern detection const randomNum = Math.floor(Math.random() * 9999) const randomYear = 1985 + Math.floor(Math.random() * 20) - + const randomShortNum = Math.floor(Math.random() * 99) + 1 + const firstInitial = cleanFirst.charAt(0) + const lastInitial = cleanLast.charAt(0) + + // IMPROVED: 20 patterns instead of 8 (harder to detect) const patterns = [ `${cleanFirst}.${cleanLast}`, `${cleanFirst}${cleanLast}`, @@ -21,9 +25,21 @@ export class DataGenerator { `${cleanFirst}${randomNum}`, `${cleanLast}${cleanFirst}`, `${cleanFirst}.${cleanLast}${randomYear}`, - `${cleanFirst}${randomYear}` + `${cleanFirst}${randomYear}`, + `${firstInitial}${cleanLast}${randomShortNum}`, + `${cleanFirst}${lastInitial}${randomYear}`, + `${firstInitial}.${cleanLast}`, + `${cleanFirst}-${cleanLast}`, + `${cleanLast}.${cleanFirst}`, + `${cleanFirst}${randomShortNum}${cleanLast}`, + `${firstInitial}${lastInitial}${randomNum}`, + `${cleanLast}${randomYear}`, + `${cleanFirst}.${randomYear}`, + `${cleanFirst}_${randomNum}`, + `${lastInitial}${cleanFirst}${randomShortNum}`, + `${cleanFirst}${cleanLast}${randomShortNum}` ] - + const username = patterns[Math.floor(Math.random() * patterns.length)] return `${username}@outlook.com` } @@ -33,52 +49,52 @@ export class DataGenerator { const lowercase = 'abcdefghijklmnopqrstuvwxyz' const numbers = '0123456789' const symbols = '!@#$%^&*' - + let password = '' - + // Ensure at least one of each required type password += uppercase[Math.floor(Math.random() * uppercase.length)] password += lowercase[Math.floor(Math.random() * lowercase.length)] password += numbers[Math.floor(Math.random() * numbers.length)] password += symbols[Math.floor(Math.random() * symbols.length)] - + // Fill the rest (total length: 14-18 chars for better security) const allChars = uppercase + lowercase + numbers + symbols const targetLength = 14 + Math.floor(Math.random() * 5) - + for (let i = password.length; i < targetLength; i++) { password += allChars[Math.floor(Math.random() * allChars.length)] } - + // Shuffle to mix required characters password = password.split('').sort(() => Math.random() - 0.5).join('') - + return password } generateBirthdate(): { day: number; month: number; year: number } { const currentYear = new Date().getFullYear() - + // Age between 20 and 45 years old (safer range) const minAge = 20 const maxAge = 45 - + const age = minAge + Math.floor(Math.random() * (maxAge - minAge + 1)) const year = currentYear - age - + const month = 1 + Math.floor(Math.random() * 12) const daysInMonth = new Date(year, month, 0).getDate() const day = 1 + Math.floor(Math.random() * daysInMonth) - + return { day, month, year } } generateNames(email: string): { firstName: string; lastName: string } { const username = email.split('@')[0] || 'user' - + // Split on numbers, dots, underscores, hyphens const parts = username.split(/[0-9._-]+/).filter(p => p.length > 1) - + if (parts.length >= 2) { return { firstName: this.capitalize(parts[0] || getRandomFirstName()), @@ -90,7 +106,7 @@ export class DataGenerator { lastName: getRandomLastName() } } - + return { firstName: getRandomFirstName(), lastName: getRandomLastName() diff --git a/src/account-creation/HumanBehavior.ts b/src/account-creation/HumanBehavior.ts index ecf682e..d2ee99f 100644 --- a/src/account-creation/HumanBehavior.ts +++ b/src/account-creation/HumanBehavior.ts @@ -61,30 +61,43 @@ export class HumanBehavior { log(false, 'CREATOR', `[${context}] ⌨️ Typing: "${text.substring(0, 20)}${text.length > 20 ? '...' : ''}"`, 'log', 'cyan') + // IMPROVED: Generate per-session typing personality (consistent across field) + const typingSpeed = 0.7 + Math.random() * 0.6 // 0.7-1.3x speed multiplier + const errorRate = Math.random() * 0.08 // 0-8% error rate + const burstTyping = Math.random() < 0.3 // 30% chance of burst typing + 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) - + // IMPROVED: More realistic variance based on typing personality 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 + const hasTypo = Math.random() < errorRate // Dynamic typo rate + const isBurst = burstTyping && i > 0 && Math.random() < 0.4 // Burst typing pattern - if (hasTyro) { - charDelay = Math.random() * 400 + 300 // 300-700ms (correcting typo) + if (hasTypo) { + // Typo: pause, backspace, retype + charDelay = Math.random() * 500 + 400 // 400-900ms (correcting) + log(false, 'CREATOR', `[${context}] 🔄 Typo correction simulation`, 'log', 'gray') + } else if (isBurst) { + // Burst typing: very fast + charDelay = (Math.random() * 50 + 40) * typingSpeed // 40-90ms * speed } else if (isFastKey) { - charDelay = Math.random() * 70 + 80 // 80-150ms + charDelay = (Math.random() * 80 + 70) * typingSpeed // 70-150ms * speed } else if (isSlowKey) { - charDelay = Math.random() * 200 + 200 // 200-400ms + charDelay = (Math.random() * 250 + 180) * typingSpeed // 180-430ms * speed } else { - charDelay = Math.random() * 100 + 120 // 120-220ms + charDelay = (Math.random() * 120 + 100) * typingSpeed // 100-220ms * speed + } + + // IMPROVED: Random micro-pauses (thinking) + if (Math.random() < 0.05 && i > 0) { + const thinkPause = Math.random() * 800 + 500 // 500-1300ms + await this.page.waitForTimeout(Math.floor(thinkPause)) } await locator.type(char, { delay: 0 }) // Type instantly @@ -101,29 +114,45 @@ export class HumanBehavior { * CRITICAL: Simulate micro mouse movements and scrolls * Real humans constantly move mouse and scroll while reading/thinking * + * IMPROVED: Natural variance per session (not every gesture is identical) + * * @param context Description for logging */ async microGestures(context: string): Promise { try { const gestureNotes: string[] = [] - // 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) + // IMPROVED: Variable mouse movement probability (not always 60%) + const mouseMoveProb = 0.45 + Math.random() * 0.3 // 45-75% chance + + if (Math.random() < mouseMoveProb) { + // IMPROVED: Wider movement range (more natural) + const x = Math.floor(Math.random() * 400) + 30 // Random x: 30-430px + const y = Math.floor(Math.random() * 300) + 20 // Random y: 20-320px + const steps = Math.floor(Math.random() * 8) + 2 // 2-10 steps (variable smoothness) await this.page.mouse.move(x, y, { steps }).catch(() => { // Mouse move failed - page may be closed or unavailable }) gestureNotes.push(`mouse→(${x},${y})`) + + // IMPROVED: Sometimes double-move (human overshoots then corrects) + if (Math.random() < 0.15) { + await this.humanDelay(100, 300, context) + const x2 = x + (Math.random() * 40 - 20) // ±20px correction + const y2 = y + (Math.random() * 40 - 20) + await this.page.mouse.move(x2, y2, { steps: 2 }).catch(() => { }) + gestureNotes.push(`correct→(${x2},${y2})`) + } } - // 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 + // IMPROVED: Variable scroll probability (not always 30%) + const scrollProb = 0.2 + Math.random() * 0.25 // 20-45% chance + + if (Math.random() < scrollProb) { + const direction = Math.random() < 0.65 ? 1 : -1 // 65% down, 35% up + const distance = Math.floor(Math.random() * 300) + 40 // 40-340px (more variance) const dy = direction * distance await this.page.mouse.wheel(0, dy).catch(() => { @@ -133,8 +162,11 @@ export class HumanBehavior { gestureNotes.push(`scroll ${direction > 0 ? '↓' : '↑'} ${distance}px`) } + // IMPROVED: Sometimes NO gesture at all (humans sometimes just stare) + // Already handled by caller's random probability + if (gestureNotes.length > 0) { - log(false, 'CREATOR', `[${context}] micro gestures: ${gestureNotes.join(', ')}`, 'log', 'gray') + log(false, 'CREATOR', `[${context}] ${gestureNotes.join(', ')}`, 'log', 'gray') } } catch { // Gesture execution failed - not critical for operation