Summary of implemented anti-detection improvements:

New files created:
SecureRandom.ts: Crypto-secure randomization (replaces Math.random())
NaturalMouse.ts: Mouse movements with Bézier curves
AntiDetectionScripts.ts: 23 layers of client-side anti-detection
Modified files:
Browser.ts: Integration of the centralized anti-detection system
Humanizer.ts: Complete rewrite with session personalization
HumanBehavior.ts: Unique personalization per account creation
Config.ts: New ConfigAntiDetection interface
23 active anti-detection protections:
WebDriver removal
Chrome runtime mocking
Canvas noise injection
WebGL parameter spoofing
Audio fingerprint protection
WebRTC IP leak prevention
Battery API spoofing
Permissions API masking
Plugin spoofing
Network Information
Media Device Protection
Speech Synthesis Protection
Keyboard Layout Detection and Prevention
Timing Attack Prevention
Error Stack Sanitization
Console Protection
Navigator Properties Protection
Hardware Concurrency Spoofing
Memory Spoofing
Connection Information
PDF Viewer Detection
Automation Detection (Puppeteer, Selenium, Playwright)
Timezone/Locale Coherence
Behavioral Improvements:
Bezier Curves for Mouse Movement (No Straight Lines)
Simulated Natural Tremors in Movements
Overshoot and Correction Like a Real Human
Unique Personality Per Session (Typing Speed, Mouse Precision, Error Rate)
Gaussian Distribution for Delays (No Fixed Timing)
Fatigue Simulation with Performance Variation
Random Thinking Pauses
This commit is contained in:
2025-12-16 22:22:38 +01:00
parent 704c271b91
commit 9d9b391cd1
7 changed files with 1883 additions and 407 deletions

View File

@@ -3,21 +3,88 @@
*
* CRITICAL: Microsoft detects bots by analyzing:
* 1. Typing speed (instant .fill() = bot, gradual .type() = human)
* 2. Mouse movements (no movement = bot, random moves = human)
* 2. Mouse movements (linear = bot, Bézier curves = human)
* 3. Pauses (fixed delays = bot, variable pauses = human)
* 4. Click patterns (force clicks = bot, natural clicks = human)
* 5. Session consistency (same patterns = bot farm)
*
* MAJOR IMPROVEMENTS (v3.5):
* - Crypto-secure randomness (not Math.random)
* - Bézier curve mouse movements
* - Session personality (unique behavior per account)
* - Fatigue simulation
* - Natural scroll with inertia
*
* This module ensures account creation is INDISTINGUISHABLE from manual creation.
*/
import type { Page } from 'rebrowser-playwright'
import { log } from '../util/notifications/Logger'
import { generateMousePath, generateScrollPath } from '../util/security/NaturalMouse'
import {
humanVariance,
secureGaussian,
secureRandomBool,
secureRandomFloat,
secureRandomInt,
typingDelay
} from '../util/security/SecureRandom'
/**
* Session personality - unique behavior patterns per account creation
*/
interface CreatorPersonality {
typingSpeed: number // 0.6-1.4
mousePrecision: number // 0.7-1.3
pauseTendency: number // 0.5-1.5
errorRate: number // 0-0.08 (typo probability)
burstTyping: boolean // Does this person type in bursts?
readingSpeed: number // WPM for reading
confidenceLevel: number // 0.7-1.3 (affects hesitation)
}
export class HumanBehavior {
private page: Page
private personality: CreatorPersonality
private sessionStart: number
private actionCount: number = 0
constructor(page: Page) {
this.page = page
this.sessionStart = Date.now()
// Generate unique personality for this account creation
this.personality = this.generatePersonality()
log(false, 'CREATOR', `🧠 Session personality: typing=${this.personality.typingSpeed.toFixed(2)}x, ` +
`precision=${this.personality.mousePrecision.toFixed(2)}x, ` +
`confidence=${this.personality.confidenceLevel.toFixed(2)}`, 'log', 'cyan')
}
/**
* Generate unique personality for this session
*/
private generatePersonality(): CreatorPersonality {
return {
typingSpeed: secureRandomFloat(0.6, 1.4),
mousePrecision: secureRandomFloat(0.7, 1.3),
pauseTendency: secureRandomFloat(0.5, 1.5),
errorRate: secureRandomFloat(0, 0.08),
burstTyping: secureRandomBool(0.3),
readingSpeed: secureRandomInt(180, 320),
confidenceLevel: secureRandomFloat(0.7, 1.3)
}
}
/**
* Get fatigue multiplier based on session duration
*/
private getFatigueMultiplier(): number {
const sessionDuration = Date.now() - this.sessionStart
const minutesActive = sessionDuration / 60000
// Fatigue increases over 30+ minutes
return 1 + Math.min(0.4, Math.max(0, (minutesActive - 30) * 0.01))
}
/**
@@ -29,17 +96,28 @@ export class HumanBehavior {
* @param context Description for logging (optional)
*/
async humanDelay(minMs: number, maxMs: number, context?: string): Promise<void> {
// IMPROVEMENT: Add occasional "thinking" pauses (10% chance of 2x delay)
const shouldThink = Math.random() < 0.1
const multiplier = shouldThink ? 2 : 1
// Use Gaussian distribution centered on mean
const mean = (minMs + maxMs) / 2
const stdDev = (maxMs - minMs) / 4
const delay = (Math.random() * (maxMs - minMs) + minMs) * multiplier
let delay = secureGaussian(mean, stdDev)
if (shouldThink && context) {
log(false, 'CREATOR', `[${context}] 🤔 Thinking pause (${Math.floor(delay)}ms)`, 'log', 'cyan')
// Apply personality and fatigue
delay *= this.personality.pauseTendency * this.getFatigueMultiplier()
// 10% chance of "thinking" pause (2x delay)
if (secureRandomBool(0.1)) {
delay *= 2
if (context) {
log(false, 'CREATOR', `[${context}] 🤔 Thinking pause (${Math.floor(delay)}ms)`, 'log', 'cyan')
}
}
// Clamp to reasonable bounds
delay = Math.max(minMs * 0.5, Math.min(maxMs * 2, delay))
await this.page.waitForTimeout(Math.floor(delay))
this.actionCount++
}
/**
@@ -53,68 +131,88 @@ export class HumanBehavior {
async humanType(locator: import('rebrowser-playwright').Locator, text: string, context: string): Promise<void> {
// 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
await this.humanDelay(200, 600, context)
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
// Track if we should simulate a typo
let typoMade = false
const shouldMakeTypo = secureRandomBool(this.personality.errorRate * 3) // Once per field max
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
// IMPROVED: More realistic variance based on typing personality
// Determine character delay based on type
let charDelay: number
const isFastKey = /[eatino]/i.test(char)
const isSlowKey = /[^a-z]/i.test(char) // Numbers, symbols, etc.
const hasTypo = Math.random() < errorRate // Dynamic typo rate
const isBurst = burstTyping && i > 0 && Math.random() < 0.4 // Burst typing pattern
const isFastKey = /[eatinos]/i.test(char)
const isSlowKey = /[^a-z0-9@.]/i.test(char) // Symbols, uppercase
const isBurst = this.personality.burstTyping && secureRandomBool(0.3)
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
if (isBurst && i > 0) {
// Burst typing: very fast sequence
charDelay = typingDelay(30) * this.personality.typingSpeed
} else if (isFastKey) {
charDelay = (Math.random() * 80 + 70) * typingSpeed // 70-150ms * speed
charDelay = typingDelay(60) * this.personality.typingSpeed
} else if (isSlowKey) {
charDelay = (Math.random() * 250 + 180) * typingSpeed // 180-430ms * speed
charDelay = typingDelay(150) * this.personality.typingSpeed
} else {
charDelay = (Math.random() * 120 + 100) * typingSpeed // 100-220ms * speed
charDelay = typingDelay(80) * this.personality.typingSpeed
}
// 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))
// Simulate typo (once per field, if enabled)
if (!typoMade && shouldMakeTypo && i > 2 && i < text.length - 2 && secureRandomBool(0.15)) {
typoMade = true
// Type wrong character
const wrongChar = String.fromCharCode(char.charCodeAt(0) + secureRandomInt(-1, 1))
await locator.type(wrongChar, { delay: 0 })
await this.page.waitForTimeout(secureRandomInt(200, 500))
// Pause (realize mistake)
await this.page.waitForTimeout(secureRandomInt(300, 800))
// Backspace
await this.page.keyboard.press('Backspace')
await this.page.waitForTimeout(secureRandomInt(100, 300))
log(false, 'CREATOR', `[${context}] 🔄 Typo correction`, 'log', 'gray')
}
await locator.type(char, { delay: 0 }) // Type instantly
// Type the character
await locator.type(char, { delay: 0 })
await this.page.waitForTimeout(Math.floor(charDelay))
// Occasional micro-pause (human thinking)
if (secureRandomBool(0.05) && i > 0) {
await this.page.waitForTimeout(secureRandomInt(300, 800))
}
// Burst typing: type next 2-3 chars rapidly
if (isBurst && i < text.length - 2) {
const burstLen = secureRandomInt(2, 3)
for (let j = 0; j < burstLen && i + 1 < text.length; j++) {
i++
const nextChar = text[i]
if (nextChar) {
await locator.type(nextChar, { delay: 0 })
await this.page.waitForTimeout(secureRandomInt(15, 40))
}
}
}
}
log(false, 'CREATOR', `[${context}] ✅ Typing completed`, 'log', 'green')
// IMPROVEMENT: Random pause after typing (human reviewing input)
await this.humanDelay(500, 1500, context)
// Random pause after typing (human reviewing input)
await this.humanDelay(400, 1200, context)
}
/**
* 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)
* IMPROVED: Uses Bézier curves for natural movement
*
* @param context Description for logging
*/
@@ -122,49 +220,69 @@ export class HumanBehavior {
try {
const gestureNotes: string[] = []
// IMPROVED: Variable mouse movement probability (not always 60%)
const mouseMoveProb = 0.45 + Math.random() * 0.3 // 45-75% chance
// Mouse movement probability varies by personality
const mouseMoveProb = 0.35 + this.personality.mousePrecision * 0.3
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)
if (secureRandomBool(mouseMoveProb)) {
const viewport = this.page.viewportSize()
if (viewport) {
// Get random target
const targetX = secureRandomInt(50, viewport.width - 50)
const targetY = secureRandomInt(50, viewport.height - 50)
await this.page.mouse.move(x, y, { steps }).catch(() => {
// Mouse move failed - page may be closed or unavailable
})
// Generate Bézier curve path
const startX = secureRandomInt(100, viewport.width / 2)
const startY = secureRandomInt(100, viewport.height / 2)
gestureNotes.push(`mouse→(${x},${y})`)
const path = generateMousePath(
{ x: startX, y: startY },
{ x: targetX, y: targetY },
{
speed: this.personality.mousePrecision,
overshoot: secureRandomBool(0.2)
}
)
// 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})`)
// Execute path
for (let i = 0; i < path.points.length; i++) {
const point = path.points[i]
if (point) {
await this.page.mouse.move(point.x, point.y).catch(() => { })
}
const duration = path.durations[i]
if (duration && duration > 0) {
await this.page.waitForTimeout(duration).catch(() => { })
}
}
gestureNotes.push(`mouse→(${targetX},${targetY})`)
}
}
// IMPROVED: Variable scroll probability (not always 30%)
const scrollProb = 0.2 + Math.random() * 0.25 // 20-45% chance
// Scroll probability
const scrollProb = 0.2 + this.personality.pauseTendency * 0.15
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
if (secureRandomBool(scrollProb)) {
const direction = secureRandomBool(0.65) ? 1 : -1
const distance = secureRandomInt(40, 250) * direction
await this.page.mouse.wheel(0, dy).catch(() => {
// Scroll failed - page may be closed or unavailable
})
// Natural scroll with inertia
const scrollPath = generateScrollPath(distance, { smooth: true })
gestureNotes.push(`scroll ${direction > 0 ? '↓' : '↑'} ${distance}px`)
for (let i = 0; i < scrollPath.deltas.length; i++) {
const delta = scrollPath.deltas[i]
if (delta) {
await this.page.mouse.wheel(0, delta).catch(() => { })
}
const duration = scrollPath.durations[i]
if (duration && duration > 0) {
await this.page.waitForTimeout(duration).catch(() => { })
}
}
gestureNotes.push(`scroll ${direction > 0 ? '↓' : '↑'} ${Math.abs(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}] ${gestureNotes.join(', ')}`, 'log', 'gray')
}
@@ -177,6 +295,8 @@ export class HumanBehavior {
* CRITICAL: Natural click with human behavior
* NEVER use { force: true } - it bypasses visibility checks (bot pattern)
*
* IMPROVED: Uses Bézier curve to move to element
*
* @param locator Playwright locator (button/link)
* @param context Description for logging
* @param maxRetries Max click attempts (default: 3)
@@ -189,33 +309,57 @@ export class HumanBehavior {
): Promise<boolean> {
for (let attempt = 1; attempt <= maxRetries; attempt++) {
try {
// CRITICAL: Move mouse to element first (real humans do this)
// Get element bounding box
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
// Calculate click position (not always center)
const clickX = box.x + box.width * secureRandomFloat(0.25, 0.75)
const clickY = box.y + box.height * secureRandomFloat(0.25, 0.75)
await this.page.mouse.move(
box.x + offsetX,
box.y + offsetY,
{ steps: Math.floor(Math.random() * 3) + 2 } // 2-5 steps
).catch(() => { })
// Move to element with Bézier curve
const viewport = this.page.viewportSize()
const startX = viewport ? secureRandomInt(0, viewport.width / 2) : 100
const startY = viewport ? secureRandomInt(0, viewport.height / 2) : 100
await this.humanDelay(100, 300, context) // Pause before clicking
const path = generateMousePath(
{ x: startX, y: startY },
{ x: clickX, y: clickY },
{
speed: this.personality.mousePrecision,
overshoot: secureRandomBool(0.15) // Less overshoot for clicks
}
)
// Execute path
for (let i = 0; i < path.points.length; i++) {
const point = path.points[i]
if (point) {
await this.page.mouse.move(point.x, point.y).catch(() => { })
}
const duration = path.durations[i]
if (duration && duration > 0) {
await this.page.waitForTimeout(duration).catch(() => { })
}
}
// Pre-click pause (human aims before clicking)
await this.humanDelay(50, 200, context)
}
// NATURAL CLICK: No force (respects visibility/interactability)
// Perform click
await locator.click({ force: false, timeout: 5000 })
log(false, 'CREATOR', `[${context}] ✅ Clicked successfully`, 'log', 'green')
await this.humanDelay(300, 800, context) // Pause after clicking
// Post-click pause (human waits for response)
await this.humanDelay(250, 700, context)
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)
await this.humanDelay(800, 1800, context)
} else {
const msg = error instanceof Error ? error.message : String(error)
log(false, 'CREATOR', `[${context}] ❌ Click failed after ${maxRetries} attempts: ${msg}`, 'error')
@@ -236,15 +380,16 @@ export class HumanBehavior {
async readPage(context: string): Promise<void> {
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
// Random scroll movements while reading
const scrollCount = secureRandomInt(1, 3)
for (let i = 0; i < scrollCount; i++) {
await this.microGestures(context)
await this.humanDelay(800, 2000, context)
await this.humanDelay(600, 1800, context)
}
// Final reading pause
await this.humanDelay(1500, 3500, context)
// Final reading pause (based on personality reading speed)
const readTime = (50 / this.personality.readingSpeed) * 60000 // ~50 words
await this.humanDelay(readTime * 0.5, readTime * 1.5, context)
}
/**
@@ -261,24 +406,49 @@ export class HumanBehavior {
optionLocator: import('rebrowser-playwright').Locator,
context: string
): Promise<boolean> {
// STEP 1: Click dropdown button (with human behavior)
// STEP 1: Click dropdown button
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 2: Wait for dropdown animation
await this.humanDelay(400, 1000, context)
// STEP 3: Move mouse randomly inside dropdown (human reading options)
// STEP 3: Move mouse around (reading options)
await this.microGestures(context)
await this.humanDelay(300, 800, context)
await this.humanDelay(200, 600, context)
// STEP 4: Click selected option (with human behavior)
// STEP 4: Click selected option
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)
await this.humanDelay(400, 1000, context)
return true
}
/**
* Hesitation pause - simulates uncertainty
* Used before important actions where user might reconsider
*
* @param context Description for logging
*/
async hesitate(context: string): Promise<void> {
if (secureRandomBool(0.4 / this.personality.confidenceLevel)) {
const hesitationTime = humanVariance(1500, 0.5) / this.personality.confidenceLevel
log(false, 'CREATOR', `[${context}] 🤔 Hesitating...`, 'log', 'cyan')
await this.page.waitForTimeout(Math.floor(hesitationTime))
}
}
/**
* Get session statistics
*/
getStats(): { actionCount: number; sessionDurationMs: number; personality: CreatorPersonality } {
return {
actionCount: this.actionCount,
sessionDurationMs: Date.now() - this.sessionStart,
personality: this.personality
}
}
}