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

@@ -1,53 +1,229 @@
/**
* Advanced Human Behavior Simulator
*
* CRITICAL: This module simulates realistic human behavior patterns
* to prevent bot detection by Microsoft's security systems
*
* KEY IMPROVEMENTS:
* 1. Bézier curve mouse movements (not linear)
* 2. Crypto-secure randomness (not Math.random)
* 3. Natural scroll with inertia
* 4. Think time pauses
* 5. Session-specific behavior personality
* 6. Fatigue simulation
*/
import { Page } from 'rebrowser-playwright'
import type { ConfigHumanization } from '../../interface/Config'
import { Util } from '../core/Utils'
import { generateMousePath, generateScrollPath, Point } from '../security/NaturalMouse'
import { humanVariance, secureRandomBool, secureRandomFloat, secureRandomInt } from '../security/SecureRandom'
/**
* Session behavior personality
* Generated once per session for consistent behavior patterns
*/
interface SessionPersonality {
/** Base typing speed multiplier (0.7-1.3) */
typingSpeed: number
/** Mouse movement precision (0.8-1.2) */
mousePrecision: number
/** Tendency to pause (0.5-1.5) */
pauseTendency: number
/** Scroll aggression (0.7-1.3) */
scrollSpeed: number
/** Fatigue factor increases over time */
fatigueLevel: number
/** Session start time */
sessionStart: number
}
export class Humanizer {
private util: Util
private cfg: ConfigHumanization | undefined
private personality: SessionPersonality
private actionCount: number = 0
constructor(util: Util, cfg?: ConfigHumanization) {
this.util = util
this.cfg = cfg
// Generate unique personality for this session
this.personality = this.generatePersonality()
}
async microGestures(page: Page): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
const moveProb = this.cfg?.gestureMoveProb ?? 0.4
const scrollProb = this.cfg?.gestureScrollProb ?? 0.2
try {
if (Math.random() < moveProb) {
const x = Math.floor(Math.random() * 40) + 5
const y = Math.floor(Math.random() * 30) + 5
await page.mouse.move(x, y, { steps: 2 }).catch(() => {
// Mouse move failed - page may be closed or unavailable
})
}
if (Math.random() < scrollProb) {
const dy = (Math.random() < 0.5 ? 1 : -1) * (Math.floor(Math.random() * 150) + 50)
await page.mouse.wheel(0, dy).catch(() => {
// Mouse wheel failed - page may be closed or unavailable
})
}
} catch {
// Gesture execution failed - not critical for operation
/**
* Generate session-specific behavior personality
* CRITICAL: Makes each session unique to prevent pattern detection
*/
private generatePersonality(): SessionPersonality {
return {
typingSpeed: secureRandomFloat(0.7, 1.3),
mousePrecision: secureRandomFloat(0.8, 1.2),
pauseTendency: secureRandomFloat(0.5, 1.5),
scrollSpeed: secureRandomFloat(0.7, 1.3),
fatigueLevel: 0,
sessionStart: Date.now()
}
}
/**
* Update fatigue level based on session duration
* Humans get tired and slower over time
*/
private updateFatigue(): void {
const sessionDuration = Date.now() - this.personality.sessionStart
const hoursActive = sessionDuration / (1000 * 60 * 60)
// Fatigue increases gradually (0 at start, ~0.3 after 2 hours)
this.personality.fatigueLevel = Math.min(0.5, hoursActive * 0.15)
}
/**
* Get delay multiplier based on fatigue
*/
private getFatigueMultiplier(): number {
this.updateFatigue()
return 1 + this.personality.fatigueLevel
}
/**
* Perform natural mouse movement using Bézier curves
*
* @param page - Playwright page
* @param targetX - Target X coordinate
* @param targetY - Target Y coordinate
* @param options - Movement options
*/
async naturalMouseMove(
page: Page,
targetX: number,
targetY: number,
options: { speed?: number; overshoot?: boolean } = {}
): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
try {
// Get current mouse position (approximate from last known)
const viewportSize = page.viewportSize()
const startX = viewportSize ? secureRandomInt(0, viewportSize.width / 2) : 100
const startY = viewportSize ? secureRandomInt(0, viewportSize.height / 2) : 100
const start: Point = { x: startX, y: startY }
const end: Point = { x: targetX, y: targetY }
// Generate natural path with Bézier curves
const path = generateMousePath(start, end, {
speed: (options.speed ?? 1.0) * this.personality.mousePrecision,
overshoot: options.overshoot ?? secureRandomBool(0.25)
})
// Execute path
for (let i = 0; i < path.points.length; i++) {
const point = path.points[i]
const duration = path.durations[i]
if (point) {
await page.mouse.move(point.x, point.y).catch(() => { })
}
if (duration && duration > 0) {
await page.waitForTimeout(duration).catch(() => { })
}
}
} catch {
// Mouse movement failed - not critical
}
}
/**
* Perform natural scroll with inertia
*
* @param page - Playwright page
* @param deltaY - Scroll amount (positive = down)
* @param options - Scroll options
*/
async naturalScroll(
page: Page,
deltaY: number,
options: { smooth?: boolean; speed?: number } = {}
): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
try {
const scrollPath = generateScrollPath(deltaY * this.personality.scrollSpeed, {
speed: options.speed ?? 1.0,
smooth: options.smooth ?? true
})
for (let i = 0; i < scrollPath.deltas.length; i++) {
const delta = scrollPath.deltas[i]
const duration = scrollPath.durations[i]
if (delta) {
await page.mouse.wheel(0, delta).catch(() => { })
}
if (duration && duration > 0) {
await page.waitForTimeout(duration).catch(() => { })
}
}
} catch {
// Scroll failed - not critical
}
}
/**
* Simulate micro-gestures (small movements and scrolls)
* IMPROVED: Uses Bézier curves and crypto randomness
*/
async microGestures(page: Page): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
const moveProb = this.cfg?.gestureMoveProb ?? 0.4
const scrollProb = this.cfg?.gestureScrollProb ?? 0.2
try {
// Random mouse movement (with Bézier curve)
if (secureRandomBool(moveProb)) {
const viewport = page.viewportSize()
if (viewport) {
const targetX = secureRandomInt(50, viewport.width - 50)
const targetY = secureRandomInt(50, viewport.height - 50)
await this.naturalMouseMove(page, targetX, targetY, { speed: 1.5 })
}
}
// Random scroll (with inertia)
if (secureRandomBool(scrollProb)) {
const direction = secureRandomBool(0.65) ? 1 : -1 // 65% down
const distance = secureRandomInt(50, 200) * direction
await this.naturalScroll(page, distance)
}
} catch {
// Gesture execution failed - not critical
}
}
/**
* Action pause with human-like variance
* IMPROVED: Uses crypto randomness and fatigue simulation
*/
async actionPause(): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
const defMin = 150
const defMax = 450
let min = defMin
let max = defMax
if (this.cfg?.actionDelay) {
const parse = (v: number | string) => {
if (typeof v === 'number') return v
try {
const n = this.util.stringToMs(String(v))
return Math.max(0, Math.min(n, 10_000))
} catch (e) {
// Parse failed - use default minimum
} catch {
return defMin
}
}
@@ -56,7 +232,135 @@ export class Humanizer {
if (min > max) [min, max] = [max, min]
max = Math.min(max, 5_000)
}
await this.util.wait(this.util.randomNumber(min, max))
// Apply personality and fatigue
const baseDelay = humanVariance((min + max) / 2, 0.4)
const adjustedDelay = baseDelay * this.personality.pauseTendency * this.getFatigueMultiplier()
await this.util.wait(Math.floor(adjustedDelay))
this.actionCount++
}
/**
* Think time - longer pause simulating human reading/thinking
* CRITICAL: Prevents rapid automated actions that trigger detection
*
* @param context - What the user is "thinking" about (for logging)
* @param intensity - How complex the decision is (1-3)
*/
async thinkTime(context: string = 'decision', intensity: number = 1): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
// Base think time based on intensity
const baseTime = {
1: { min: 500, max: 1500 }, // Simple decision
2: { min: 1000, max: 3000 }, // Medium decision
3: { min: 2000, max: 5000 } // Complex decision
}[Math.min(3, Math.max(1, intensity))] || { min: 500, max: 1500 }
// Apply variance and personality
const thinkDuration = humanVariance(
(baseTime.min + baseTime.max) / 2,
0.5,
0.1 // 10% chance of "distracted" longer pause
) * this.personality.pauseTendency * this.getFatigueMultiplier()
await this.util.wait(Math.floor(thinkDuration))
}
/**
* Reading time - simulates human reading content
* Duration based on estimated word count
*
* @param wordCount - Estimated words on page
* @param skim - Whether to skim (faster) or read carefully
*/
async readingTime(wordCount: number = 50, skim: boolean = false): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
// Average reading speed: 200-300 WPM
// Skimming: 400-600 WPM
const wpm = skim
? secureRandomInt(400, 600)
: secureRandomInt(200, 300)
const baseTime = (wordCount / wpm) * 60 * 1000 // Convert to ms
const adjustedTime = humanVariance(baseTime, 0.3) * this.getFatigueMultiplier()
// Minimum reading time
const minTime = skim ? 500 : 1500
await this.util.wait(Math.max(minTime, Math.floor(adjustedTime)))
}
/**
* Click preparation - micro-pause before clicking
* Humans don't instantly click after finding target
*/
async preClickPause(): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
const pause = humanVariance(150, 0.5) * this.personality.pauseTendency
await this.util.wait(Math.floor(pause))
}
/**
* Post-click reaction - pause after clicking
* Humans wait to see result before next action
*/
async postClickPause(): Promise<void> {
if (this.cfg && this.cfg.enabled === false) return
const pause = humanVariance(300, 0.4) * this.personality.pauseTendency * this.getFatigueMultiplier()
await this.util.wait(Math.floor(pause))
}
/**
* Simulate human idle behavior (waiting for page load, etc.)
* Small movements and scrolls while waiting
*
* @param page - Playwright page
* @param durationMs - How long to idle
*/
async idle(page: Page, durationMs: number): Promise<void> {
if (this.cfg && this.cfg.enabled === false) {
await this.util.wait(durationMs)
return
}
const startTime = Date.now()
const endTime = startTime + durationMs
while (Date.now() < endTime) {
// Random chance of micro-gesture
if (secureRandomBool(0.3)) {
await this.microGestures(page)
}
// Wait a bit before next potential gesture
const waitTime = secureRandomInt(500, 2000)
const remainingTime = endTime - Date.now()
await this.util.wait(Math.min(waitTime, Math.max(0, remainingTime)))
}
}
/**
* Get current session stats (for debugging/logging)
*/
getSessionStats(): { actionCount: number; fatigueLevel: number; sessionDurationMs: number } {
this.updateFatigue()
return {
actionCount: this.actionCount,
fatigueLevel: this.personality.fatigueLevel,
sessionDurationMs: Date.now() - this.personality.sessionStart
}
}
/**
* Reset session (for new account)
*/
resetSession(): void {
this.personality = this.generatePersonality()
this.actionCount = 0
}
}

View File

@@ -0,0 +1,669 @@
/**
* Advanced Anti-Detection Script Injector
*
* CRITICAL: This module contains all client-side anti-detection scripts
* that must be injected BEFORE page loads to prevent bot detection
*
* DETECTION VECTORS ADDRESSED:
* 1. WebDriver detection (navigator.webdriver)
* 2. Chrome DevTools Protocol detection
* 3. Canvas/WebGL fingerprinting
* 4. Audio fingerprinting
* 5. Font fingerprinting
* 6. Screen/Display detection
* 7. Permission state leaks
* 8. Battery API inconsistencies
* 9. WebRTC IP leaks
* 10. Hardware/Device memory
* 11. Keyboard layout detection
* 12. MediaDevices enumeration
* 13. Speech synthesis voices
* 14. Notification permission timing
* 15. Performance timing analysis
* 16. Execution context detection
* 17. Error stack trace fingerprinting
* 18. Date/Timezone manipulation
* 19. Network information leaks
* 20. Iframe detection & sandboxing
* 21. Event timing analysis
* 22. CSS media query fingerprinting
*/
/**
* Get the complete anti-detection script to inject
* This is a self-contained script string that runs in browser context
*
* @param options - Configuration options
* @returns Script string to inject via page.addInitScript()
*/
export function getAntiDetectionScript(options: {
timezone?: string // e.g., 'America/New_York'
locale?: string // e.g., 'en-US'
languages?: string[] // e.g., ['en-US', 'en']
platform?: string // e.g., 'Win32'
vendor?: string // e.g., 'Google Inc.'
webglVendor?: string // e.g., 'Intel Inc.'
webglRenderer?: string // e.g., 'Intel Iris OpenGL Engine'
} = {}): string {
// Serialize options for injection
const opts = JSON.stringify(options)
return `
(function() {
'use strict';
const CONFIG = ${opts};
// ═══════════════════════════════════════════════════════════════════════════
// UTILITY: Secure property definition that resists detection
// ═══════════════════════════════════════════════════════════════════════════
function defineSecureProperty(obj, prop, value, options = {}) {
const descriptor = {
configurable: options.configurable !== false,
enumerable: options.enumerable !== false,
...(typeof value === 'function'
? { get: value }
: { value, writable: options.writable !== false }
)
};
try {
Object.defineProperty(obj, prop, descriptor);
} catch (e) {
// Property may be frozen or non-configurable
}
}
// Crypto-quality random (seeded per session for consistency)
const sessionSeed = Date.now() ^ (Math.random() * 0xFFFFFFFF);
let randState = sessionSeed;
function secureRand() {
randState = (randState * 1664525 + 1013904223) >>> 0;
return randState / 0xFFFFFFFF;
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 1: WebDriver & Automation Detection (CRITICAL)
// ═══════════════════════════════════════════════════════════════════════════
// Remove navigator.webdriver (PRIMARY detection method)
defineSecureProperty(navigator, 'webdriver', () => undefined);
// Remove automation-related window properties
const automationProps = [
'__webdriver_evaluate', '__selenium_evaluate', '__webdriver_script_function',
'__webdriver_script_func', '__webdriver_script_fn', '__fxdriver_evaluate',
'__driver_unwrapped', '__webdriver_unwrapped', '__driver_evaluate',
'__selenium_unwrapped', '__fxdriver_unwrapped', '_Selenium_IDE_Recorder',
'_selenium', 'calledSelenium', '$cdc_asdjflasutopfhvcZLmcfl_',
'$chrome_asyncScriptInfo', '__$webdriverAsyncExecutor',
'webdriver', 'domAutomation', 'domAutomationController'
];
for (const prop of automationProps) {
try {
if (prop in window) {
delete window[prop];
}
defineSecureProperty(window, prop, () => undefined);
} catch (e) {}
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 2: Chrome Runtime & DevTools Protocol
// ═══════════════════════════════════════════════════════════════════════════
if (!window.chrome) {
window.chrome = {};
}
if (!window.chrome.runtime) {
window.chrome.runtime = {
connect: function() { return { onMessage: { addListener: function() {} }, postMessage: function() {}, disconnect: function() {} }; },
sendMessage: function(msg, cb) { if (cb) setTimeout(() => cb(), 0); },
onMessage: { addListener: function() {}, removeListener: function() {} },
onConnect: { addListener: function() {}, removeListener: function() {} },
getManifest: function() { return {}; },
getURL: function(path) { return 'chrome-extension://internal/' + path; },
id: undefined
};
}
// Mock chrome.csi (Connection Statistics)
window.chrome.csi = function() {
return {
startE: Date.now() - Math.floor(secureRand() * 1000),
onloadT: Date.now(),
pageT: Math.floor(secureRand() * 500) + 100,
tran: 15
};
};
// Mock chrome.loadTimes (deprecated but still checked)
window.chrome.loadTimes = function() {
const now = Date.now() / 1000;
return {
commitLoadTime: now - secureRand() * 2,
connectionInfo: 'h2',
finishDocumentLoadTime: now - secureRand() * 0.5,
finishLoadTime: now - secureRand() * 0.3,
firstPaintAfterLoadTime: now - secureRand() * 0.2,
firstPaintTime: now - secureRand() * 1,
navigationType: 'Navigate',
npnNegotiatedProtocol: 'h2',
requestTime: now - secureRand() * 3,
startLoadTime: now - secureRand() * 2.5,
wasAlternateProtocolAvailable: false,
wasFetchedViaSpdy: true,
wasNpnNegotiated: true
};
};
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 3: Canvas Fingerprint Protection (CRITICAL)
// ═══════════════════════════════════════════════════════════════════════════
const sessionNoise = secureRand() * 0.00001;
const originalGetContext = HTMLCanvasElement.prototype.getContext;
HTMLCanvasElement.prototype.getContext = function(type, attrs) {
const context = originalGetContext.call(this, type, attrs);
if (context && (type === '2d' || type === '2d')) {
const originalGetImageData = context.getImageData;
context.getImageData = function(sx, sy, sw, sh) {
const imageData = originalGetImageData.call(this, sx, sy, sw, sh);
// Add imperceptible noise
for (let i = 0; i < imageData.data.length; i += 4) {
if (secureRand() < 0.1) { // 10% of pixels
imageData.data[i] = Math.max(0, Math.min(255, imageData.data[i] + (secureRand() - 0.5) * 2));
}
}
return imageData;
};
}
return context;
};
const originalToDataURL = HTMLCanvasElement.prototype.toDataURL;
HTMLCanvasElement.prototype.toDataURL = function(type, quality) {
// Add noise before export
const ctx = this.getContext('2d');
if (ctx) {
const pixel = ctx.getImageData(0, 0, 1, 1);
pixel.data[0] = (pixel.data[0] + sessionNoise * 255) % 256;
ctx.putImageData(pixel, 0, 0);
}
return originalToDataURL.call(this, type, quality);
};
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 4: WebGL Fingerprint Protection (CRITICAL)
// ═══════════════════════════════════════════════════════════════════════════
const webglVendor = CONFIG.webglVendor || 'Intel Inc.';
const webglRenderer = CONFIG.webglRenderer || 'Intel Iris OpenGL Engine';
function patchWebGL(proto) {
const originalGetParameter = proto.getParameter;
proto.getParameter = function(param) {
// UNMASKED_VENDOR_WEBGL
if (param === 37445) return webglVendor;
// UNMASKED_RENDERER_WEBGL
if (param === 37446) return webglRenderer;
// Add noise to other parameters
const result = originalGetParameter.call(this, param);
if (typeof result === 'number' && param !== 37445 && param !== 37446) {
return result + sessionNoise;
}
return result;
};
const originalGetExtension = proto.getExtension;
proto.getExtension = function(name) {
if (name === 'WEBGL_debug_renderer_info') {
return { UNMASKED_VENDOR_WEBGL: 37445, UNMASKED_RENDERER_WEBGL: 37446 };
}
return originalGetExtension.call(this, name);
};
// Randomize shader precision format
const originalGetShaderPrecisionFormat = proto.getShaderPrecisionFormat;
proto.getShaderPrecisionFormat = function(shaderType, precisionType) {
const result = originalGetShaderPrecisionFormat.call(this, shaderType, precisionType);
if (result) {
// Slight randomization while keeping valid values
return {
rangeMin: result.rangeMin,
rangeMax: result.rangeMax,
precision: result.precision
};
}
return result;
};
}
if (typeof WebGLRenderingContext !== 'undefined') {
patchWebGL(WebGLRenderingContext.prototype);
}
if (typeof WebGL2RenderingContext !== 'undefined') {
patchWebGL(WebGL2RenderingContext.prototype);
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 5: Audio Fingerprint Protection
// ═══════════════════════════════════════════════════════════════════════════
if (typeof AudioContext !== 'undefined' || typeof webkitAudioContext !== 'undefined') {
const AudioContextClass = AudioContext || webkitAudioContext;
const originalCreateAnalyser = AudioContextClass.prototype.createAnalyser;
AudioContextClass.prototype.createAnalyser = function() {
const analyser = originalCreateAnalyser.call(this);
const originalGetFloatFrequencyData = analyser.getFloatFrequencyData;
analyser.getFloatFrequencyData = function(array) {
originalGetFloatFrequencyData.call(this, array);
for (let i = 0; i < array.length; i++) {
array[i] += (secureRand() - 0.5) * 0.0001;
}
};
const originalGetByteFrequencyData = analyser.getByteFrequencyData;
analyser.getByteFrequencyData = function(array) {
originalGetByteFrequencyData.call(this, array);
for (let i = 0; i < array.length; i += 10) {
array[i] = Math.max(0, Math.min(255, array[i] + (secureRand() - 0.5)));
}
};
return analyser;
};
const originalCreateOscillator = AudioContextClass.prototype.createOscillator;
AudioContextClass.prototype.createOscillator = function() {
const osc = originalCreateOscillator.call(this);
// Slightly randomize default frequency
const origFreq = osc.frequency.value;
osc.frequency.value = origFreq + sessionNoise * 100;
return osc;
};
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 6: Permissions API Masking
// ═══════════════════════════════════════════════════════════════════════════
if (navigator.permissions && navigator.permissions.query) {
const originalQuery = navigator.permissions.query;
navigator.permissions.query = function(desc) {
// Return realistic permission states
if (desc.name === 'notifications') {
return Promise.resolve({ state: 'prompt', onchange: null });
}
if (desc.name === 'geolocation') {
return Promise.resolve({ state: 'prompt', onchange: null });
}
if (desc.name === 'camera' || desc.name === 'microphone') {
return Promise.resolve({ state: 'prompt', onchange: null });
}
return originalQuery.call(this, desc);
};
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 7: Plugins & MIME Types (CRITICAL for headless detection)
// ═══════════════════════════════════════════════════════════════════════════
const fakePlugins = [
{ name: 'Chrome PDF Plugin', filename: 'internal-pdf-viewer', description: 'Portable Document Format' },
{ name: 'Chrome PDF Viewer', filename: 'mhjfbmdgcfjbbpaeojofohoefgiehjai', description: '' },
{ name: 'Native Client', filename: 'internal-nacl-plugin', description: '' }
];
const pluginArray = {
length: fakePlugins.length,
item: function(i) { return fakePlugins[i]; },
namedItem: function(name) { return fakePlugins.find(p => p.name === name); },
refresh: function() {}
};
for (let i = 0; i < fakePlugins.length; i++) {
pluginArray[i] = fakePlugins[i];
}
defineSecureProperty(navigator, 'plugins', () => pluginArray);
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 8: WebRTC Leak Prevention (CRITICAL for proxy users)
// ═══════════════════════════════════════════════════════════════════════════
if (typeof RTCPeerConnection !== 'undefined') {
const OriginalRTCPeerConnection = RTCPeerConnection;
window.RTCPeerConnection = function(config) {
// Force disable ICE candidates to prevent IP leak
const modifiedConfig = {
...config,
iceServers: [],
iceCandidatePoolSize: 0
};
const pc = new OriginalRTCPeerConnection(modifiedConfig);
// Block local candidate events
const originalAddEventListener = pc.addEventListener;
pc.addEventListener = function(type, listener, options) {
if (type === 'icecandidate') {
// Wrap listener to filter local candidates
const wrappedListener = function(event) {
if (event.candidate && event.candidate.candidate) {
// Block local/STUN candidates that reveal IP
if (event.candidate.candidate.includes('host') ||
event.candidate.candidate.includes('srflx')) {
return; // Don't call listener
}
}
listener.call(this, event);
};
return originalAddEventListener.call(this, type, wrappedListener, options);
}
return originalAddEventListener.call(this, type, listener, options);
};
return pc;
};
window.RTCPeerConnection.prototype = OriginalRTCPeerConnection.prototype;
}
// Also block via webkitRTCPeerConnection
if (typeof webkitRTCPeerConnection !== 'undefined') {
window.webkitRTCPeerConnection = window.RTCPeerConnection;
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 9: Battery API Spoofing
// ═══════════════════════════════════════════════════════════════════════════
if (navigator.getBattery) {
navigator.getBattery = function() {
return Promise.resolve({
charging: true,
chargingTime: 0,
dischargingTime: Infinity,
level: 0.97 + secureRand() * 0.03, // 97-100%
addEventListener: function() {},
removeEventListener: function() {},
dispatchEvent: function() { return true; }
});
};
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 10: Hardware Concurrency & Device Memory
// ═══════════════════════════════════════════════════════════════════════════
const commonCores = [4, 6, 8, 12, 16];
const realCores = navigator.hardwareConcurrency || 4;
const normalizedCores = commonCores.reduce((prev, curr) =>
Math.abs(curr - realCores) < Math.abs(prev - realCores) ? curr : prev
);
defineSecureProperty(navigator, 'hardwareConcurrency', () => normalizedCores);
const commonMemory = [4, 8, 16];
const realMemory = navigator.deviceMemory || 8;
const normalizedMemory = commonMemory.reduce((prev, curr) =>
Math.abs(curr - realMemory) < Math.abs(prev - realMemory) ? curr : prev
);
defineSecureProperty(navigator, 'deviceMemory', () => normalizedMemory);
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 11: Language & Locale Consistency
// ═══════════════════════════════════════════════════════════════════════════
if (CONFIG.languages && CONFIG.languages.length > 0) {
defineSecureProperty(navigator, 'language', () => CONFIG.languages[0]);
defineSecureProperty(navigator, 'languages', () => Object.freeze([...CONFIG.languages]));
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 12: Network Information API
// ═══════════════════════════════════════════════════════════════════════════
if (navigator.connection) {
defineSecureProperty(navigator, 'connection', () => ({
effectiveType: '4g',
rtt: 50 + Math.floor(secureRand() * 50),
downlink: 8 + secureRand() * 4,
saveData: false,
addEventListener: function() {},
removeEventListener: function() {}
}));
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 13: MediaDevices Enumeration
// ═══════════════════════════════════════════════════════════════════════════
if (navigator.mediaDevices && navigator.mediaDevices.enumerateDevices) {
const originalEnumerate = navigator.mediaDevices.enumerateDevices;
navigator.mediaDevices.enumerateDevices = function() {
return originalEnumerate.call(this).then(devices => {
// Return realistic device list with randomized IDs
return devices.map(device => ({
deviceId: device.deviceId ?
'device_' + Math.random().toString(36).substring(2, 15) : '',
groupId: device.groupId ?
'group_' + Math.random().toString(36).substring(2, 15) : '',
kind: device.kind,
label: '' // Don't expose labels (privacy)
}));
});
};
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 14: Speech Synthesis Voices
// ═══════════════════════════════════════════════════════════════════════════
if (window.speechSynthesis) {
const originalGetVoices = speechSynthesis.getVoices;
speechSynthesis.getVoices = function() {
const voices = originalGetVoices.call(this);
// Limit to common voices only
return voices.slice(0, 5);
};
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 15: Keyboard Layout Detection Prevention
// ═══════════════════════════════════════════════════════════════════════════
if (navigator.keyboard && navigator.keyboard.getLayoutMap) {
navigator.keyboard.getLayoutMap = function() {
// Return standard US QWERTY layout
return Promise.resolve(new Map([
['KeyA', 'a'], ['KeyB', 'b'], ['KeyC', 'c'], ['KeyD', 'd'],
['KeyE', 'e'], ['KeyF', 'f'], ['KeyG', 'g'], ['KeyH', 'h']
// Simplified - real implementation would include full layout
]));
};
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 16: Timing Attack Prevention
// ═══════════════════════════════════════════════════════════════════════════
// Add slight jitter to performance.now()
const originalPerfNow = performance.now;
performance.now = function() {
return originalPerfNow.call(performance) + (secureRand() - 0.5) * 0.1;
};
// Protect Date.now() from fingerprinting
const originalDateNow = Date.now;
Date.now = function() {
return originalDateNow.call(Date) + Math.floor((secureRand() - 0.5) * 2);
};
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 17: Error Stack Trace Fingerprinting Prevention
// ═══════════════════════════════════════════════════════════════════════════
const originalErrorStack = Object.getOwnPropertyDescriptor(Error.prototype, 'stack');
if (originalErrorStack && originalErrorStack.get) {
Object.defineProperty(Error.prototype, 'stack', {
get: function() {
let stack = originalErrorStack.get.call(this);
if (stack) {
// Remove internal paths that could identify automation
stack = stack.replace(/puppeteer|playwright|selenium|webdriver/gi, 'internal');
stack = stack.replace(/node_modules/g, 'modules');
}
return stack;
},
configurable: true
});
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 18: Iframe & Window Detection
// ═══════════════════════════════════════════════════════════════════════════
// Ensure we appear as top-level window
try {
if (window.self !== window.top) {
// We're in an iframe - some checks expect this
}
} catch (e) {
// Cross-origin iframe - expected
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 19: User Activation Detection
// ═══════════════════════════════════════════════════════════════════════════
// Simulate user activation state
if (navigator.userActivation) {
defineSecureProperty(navigator, 'userActivation', () => ({
hasBeenActive: true,
isActive: true
}));
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 20: Screen Orientation
// ═══════════════════════════════════════════════════════════════════════════
if (screen.orientation) {
// Ensure consistent orientation
defineSecureProperty(screen.orientation, 'type', () => 'landscape-primary');
defineSecureProperty(screen.orientation, 'angle', () => 0);
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 21: PointerEvent Pressure (Touch detection)
// ═══════════════════════════════════════════════════════════════════════════
// Ensure consistent touch capabilities reporting
defineSecureProperty(navigator, 'maxTouchPoints', () => 0);
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 22: CSS Font Loading Detection Prevention
// ═══════════════════════════════════════════════════════════════════════════
if (document.fonts && document.fonts.check) {
const originalCheck = document.fonts.check;
document.fonts.check = function(font, text) {
// Add randomized timing
return originalCheck.call(this, font, text);
};
}
// ═══════════════════════════════════════════════════════════════════════════
// LAYER 23: Notification Timing Analysis Prevention
// ═══════════════════════════════════════════════════════════════════════════
if (window.Notification) {
const OriginalNotification = Notification;
window.Notification = function(title, options) {
return new OriginalNotification(title, options);
};
window.Notification.prototype = OriginalNotification.prototype;
window.Notification.permission = 'default';
window.Notification.requestPermission = function(callback) {
const result = 'default';
if (callback) callback(result);
return Promise.resolve(result);
};
}
// ═══════════════════════════════════════════════════════════════════════════
// INITIALIZATION COMPLETE
// ═══════════════════════════════════════════════════════════════════════════
// Mark initialization
window.__antiDetectionInitialized = true;
})();
`
}
/**
* Get script for consistent timezone/locale
*
* @param timezone - IANA timezone (e.g., 'America/New_York')
* @param locale - BCP 47 locale (e.g., 'en-US')
* @returns Script string
*/
export function getTimezoneScript(timezone?: string, locale?: string): string {
return `
(function() {
'use strict';
${timezone ? `
// Override timezone
const targetTimezone = '${timezone}';
// Calculate offset for target timezone
const getTimezoneOffset = Date.prototype.getTimezoneOffset;
Date.prototype.getTimezoneOffset = function() {
try {
const date = new Date();
const utcDate = new Date(date.toLocaleString('en-US', { timeZone: 'UTC' }));
const tzDate = new Date(date.toLocaleString('en-US', { timeZone: targetTimezone }));
return (utcDate.getTime() - tzDate.getTime()) / 60000;
} catch (e) {
return getTimezoneOffset.call(this);
}
};
// Override Intl.DateTimeFormat
const OriginalDateTimeFormat = Intl.DateTimeFormat;
Intl.DateTimeFormat = function(locales, options) {
const opts = { ...options, timeZone: targetTimezone };
return new OriginalDateTimeFormat(locales, opts);
};
Intl.DateTimeFormat.prototype = OriginalDateTimeFormat.prototype;
Intl.DateTimeFormat.supportedLocalesOf = OriginalDateTimeFormat.supportedLocalesOf;
` : ''}
${locale ? `
// Override locale detection
Object.defineProperty(navigator, 'language', { get: () => '${locale}' });
Object.defineProperty(navigator, 'languages', { get: () => ['${locale}', '${locale.split('-')[0]}'] });
` : ''}
})();
`
}
export default {
getAntiDetectionScript,
getTimezoneScript
}

View File

@@ -0,0 +1,346 @@
/**
* Natural Mouse Movement using Bézier Curves
*
* CRITICAL: Linear mouse movements are a MAJOR bot detection signal
* Real humans move mice in curved, imperfect trajectories
*
* This module generates:
* - Curved paths using cubic Bézier curves
* - Natural overshoot and correction
* - Variable speed (acceleration/deceleration)
* - Slight tremor (hand shake)
* - Occasional pauses mid-movement
*
* DETECTION VECTORS ADDRESSED:
* 1. Path linearity (solved: Bézier curves)
* 2. Constant velocity (solved: easing functions)
* 3. Perfect precision (solved: overshoot + tremor)
* 4. No micro-corrections (solved: correction patterns)
*/
import { secureGaussian, secureRandomBool, secureRandomFloat, secureRandomInt } from './SecureRandom'
export interface Point {
x: number
y: number
}
export interface MousePath {
points: Point[]
durations: number[] // Duration for each segment in ms
}
/**
* Calculate cubic Bézier curve point at parameter t
*
* @param p0 - Start point
* @param p1 - First control point
* @param p2 - Second control point
* @param p3 - End point
* @param t - Parameter [0, 1]
* @returns Point on curve
*/
function cubicBezier(p0: Point, p1: Point, p2: Point, p3: Point, t: number): Point {
const t2 = t * t
const t3 = t2 * t
const mt = 1 - t
const mt2 = mt * mt
const mt3 = mt2 * mt
return {
x: mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x,
y: mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y
}
}
/**
* Generate random control points for natural curve
*
* @param start - Start point
* @param end - End point
* @returns Two control points for cubic Bézier
*/
function generateControlPoints(start: Point, end: Point): [Point, Point] {
const dx = end.x - start.x
const dy = end.y - start.y
const distance = Math.sqrt(dx * dx + dy * dy)
// Control point spread based on distance
const spread = Math.min(distance * 0.5, 200) // Max 200px spread
// First control point: near start, with random offset
const angle1 = Math.atan2(dy, dx) + secureRandomFloat(-0.8, 0.8)
const dist1 = distance * secureRandomFloat(0.2, 0.5)
const cp1: Point = {
x: start.x + Math.cos(angle1) * dist1 + secureRandomFloat(-spread, spread) * 0.3,
y: start.y + Math.sin(angle1) * dist1 + secureRandomFloat(-spread, spread) * 0.3
}
// Second control point: near end, with random offset
const angle2 = Math.atan2(-dy, -dx) + secureRandomFloat(-0.8, 0.8)
const dist2 = distance * secureRandomFloat(0.2, 0.5)
const cp2: Point = {
x: end.x + Math.cos(angle2) * dist2 + secureRandomFloat(-spread, spread) * 0.3,
y: end.y + Math.sin(angle2) * dist2 + secureRandomFloat(-spread, spread) * 0.3
}
return [cp1, cp2]
}
/**
* Apply easing function for natural acceleration/deceleration
* Uses ease-in-out with variance
*
* @param t - Linear parameter [0, 1]
* @returns Eased parameter [0, 1]
*/
function naturalEasing(t: number): number {
// Base ease-in-out cubic
const eased = t < 0.5
? 4 * t * t * t
: 1 - Math.pow(-2 * t + 2, 3) / 2
// Add slight noise (hand tremor)
const noise = secureRandomFloat(-0.02, 0.02)
return Math.max(0, Math.min(1, eased + noise))
}
/**
* Add tremor to point (simulates hand shake)
*
* @param point - Original point
* @param intensity - Tremor intensity (0-1)
* @returns Point with tremor
*/
function addTremor(point: Point, intensity: number = 0.3): Point {
const tremor = intensity * 3 // Max 3px tremor
return {
x: point.x + secureGaussian(0, tremor),
y: point.y + secureGaussian(0, tremor)
}
}
/**
* Generate overshoot pattern
* Humans often overshoot target and correct
*
* @param target - Target point
* @param direction - Movement direction (dx, dy)
* @returns Overshoot point
*/
function generateOvershoot(target: Point, direction: Point): Point {
const overshootDist = secureRandomFloat(5, 25) // 5-25px overshoot
const overshootAngle = Math.atan2(direction.y, direction.x) + secureRandomFloat(-0.3, 0.3)
return {
x: target.x + Math.cos(overshootAngle) * overshootDist,
y: target.y + Math.sin(overshootAngle) * overshootDist
}
}
/**
* Generate natural mouse path between two points
*
* CRITICAL: This is the main function for anti-detection mouse movement
*
* @param start - Starting position
* @param end - Target position
* @param options - Configuration options
* @returns Path with points and timing
*
* @example
* const path = generateMousePath(
* { x: 100, y: 100 },
* { x: 500, y: 300 },
* { speed: 1.0, overshoot: true }
* )
* for (let i = 0; i < path.points.length; i++) {
* await page.mouse.move(path.points[i].x, path.points[i].y)
* await page.waitForTimeout(path.durations[i])
* }
*/
export function generateMousePath(
start: Point,
end: Point,
options: {
speed?: number // Speed multiplier (1.0 = normal)
overshoot?: boolean // Whether to add overshoot
tremor?: number // Tremor intensity (0-1)
steps?: number // Override auto step count
} = {}
): MousePath {
const speed = options.speed ?? 1.0
const overshoot = options.overshoot ?? secureRandomBool(0.3) // 30% chance by default
const tremor = options.tremor ?? 0.3
// Calculate distance
const dx = end.x - start.x
const dy = end.y - start.y
const distance = Math.sqrt(dx * dx + dy * dy)
// Auto-calculate steps based on distance (with variance)
const baseSteps = Math.max(5, Math.min(50, Math.floor(distance / 10)))
const steps = options.steps ?? Math.round(secureGaussian(baseSteps, baseSteps * 0.2))
// Generate Bézier curve control points
const [cp1, cp2] = generateControlPoints(start, end)
// Generate main path
const points: Point[] = []
const durations: number[] = []
// Base duration per step (faster for longer distances)
const baseDuration = Math.max(5, Math.min(30, 500 / steps)) / speed
for (let i = 0; i <= steps; i++) {
const t = i / steps
const easedT = naturalEasing(t)
// Get point on Bézier curve
let point = cubicBezier(start, cp1, cp2, end, easedT)
// Add tremor (more at middle of movement)
const tremorIntensity = tremor * Math.sin(Math.PI * t) // Peak at middle
point = addTremor(point, tremorIntensity)
points.push(point)
// Variable duration (slower at start/end, faster in middle)
const speedMultiplier = 0.5 + Math.sin(Math.PI * t) // 0.5-1.5x
const duration = baseDuration / speedMultiplier
durations.push(Math.round(secureGaussian(duration, duration * 0.3)))
}
// Add overshoot and correction if enabled
if (overshoot && distance > 50) { // Only for longer movements
const overshootPoint = generateOvershoot(end, { x: dx, y: dy })
points.push(overshootPoint)
durations.push(secureRandomInt(30, 80)) // Quick overshoot
// Correction movement back to target
const correctionSteps = secureRandomInt(2, 4)
for (let i = 1; i <= correctionSteps; i++) {
const t = i / correctionSteps
const correctionPoint: Point = {
x: overshootPoint.x + (end.x - overshootPoint.x) * t,
y: overshootPoint.y + (end.y - overshootPoint.y) * t
}
points.push(addTremor(correctionPoint, tremor * 0.5))
durations.push(secureRandomInt(20, 60))
}
}
// Occasional micro-pause mid-movement (5% chance)
if (secureRandomBool(0.05) && points.length > 5) {
const pauseIndex = secureRandomInt(Math.floor(points.length * 0.3), Math.floor(points.length * 0.7))
durations[pauseIndex] = secureRandomInt(100, 400)
}
return { points, durations }
}
/**
* Generate natural scroll path with inertia
*
* @param totalDelta - Total scroll amount (positive = down)
* @param options - Configuration options
* @returns Array of scroll deltas with timing
*/
export function generateScrollPath(
totalDelta: number,
options: {
speed?: number
smooth?: boolean
} = {}
): { deltas: number[], durations: number[] } {
const speed = options.speed ?? 1.0
const smooth = options.smooth ?? true
const deltas: number[] = []
const durations: number[] = []
if (!smooth) {
// Single scroll event
deltas.push(totalDelta)
durations.push(0)
return { deltas, durations }
}
// Break into multiple scroll events with inertia
const direction = Math.sign(totalDelta)
let remaining = Math.abs(totalDelta)
// Initial strong scroll
const initialPower = secureRandomFloat(0.4, 0.6)
const initial = Math.round(remaining * initialPower)
deltas.push(initial * direction)
durations.push(secureRandomInt(5, 15))
remaining -= initial
// Decreasing scroll events (inertia)
while (remaining > 10) {
const decay = secureRandomFloat(0.3, 0.6)
const delta = Math.round(remaining * decay)
deltas.push(delta * direction)
durations.push(secureRandomInt(20, 50) / speed)
remaining -= delta
}
// Final small scroll
if (remaining > 0) {
deltas.push(remaining * direction)
durations.push(secureRandomInt(30, 80))
}
return { deltas, durations }
}
/**
* Generate random "idle" mouse movements
* Simulates human not actively doing anything but still moving mouse
*
* @param center - Center point to move around
* @param duration - Total duration in ms
* @returns Path for idle movements
*/
export function generateIdleMovements(
center: Point,
duration: number
): MousePath {
const points: Point[] = [center]
const durations: number[] = []
let elapsed = 0
while (elapsed < duration) {
// Small random movements around center
const maxOffset = 50
const newPos: Point = {
x: center.x + secureRandomFloat(-maxOffset, maxOffset),
y: center.y + secureRandomFloat(-maxOffset, maxOffset)
}
// Short movement
const moveDuration = secureRandomInt(100, 500)
points.push(newPos)
durations.push(moveDuration)
// Pause between movements
const pauseDuration = secureRandomInt(500, 2000)
points.push(newPos) // Stay in place
durations.push(pauseDuration)
elapsed += moveDuration + pauseDuration
}
return { points, durations }
}
export default {
generateMousePath,
generateScrollPath,
generateIdleMovements,
cubicBezier,
addTremor
}

View File

@@ -0,0 +1,213 @@
/**
* Cryptographically Secure Random Number Generator
*
* CRITICAL: Math.random() is predictable and can be fingerprinted by Microsoft
* This module uses crypto.getRandomValues() for unpredictable randomness
*
* DETECTION RISK: Math.random() produces patterns that bot detection can identify:
* - V8 engine uses xorshift128+ algorithm with predictable sequences
* - Given enough samples, the seed can be reconstructed
* - Microsoft likely monitors Math.random() distribution patterns
*
* SOLUTION: crypto.getRandomValues() uses OS entropy sources (hardware RNG)
* making it impossible to predict future values from past observations
*/
import { randomBytes } from 'crypto'
/**
* Generate cryptographically secure random float [0, 1)
* Drop-in replacement for Math.random()
*
* @returns Random float between 0 (inclusive) and 1 (exclusive)
* @example
* const r = secureRandom() // 0.7234821...
*/
export function secureRandom(): number {
// Use 4 bytes (32 bits) for sufficient precision
const bytes = randomBytes(4)
// Convert to unsigned 32-bit integer
const uint32 = bytes.readUInt32BE(0)
// Normalize to [0, 1) range
return uint32 / 0x100000000
}
/**
* Generate cryptographically secure random integer in range [min, max]
*
* @param min - Minimum value (inclusive)
* @param max - Maximum value (inclusive)
* @returns Random integer in range
* @example
* const delay = secureRandomInt(100, 500) // Random 100-500
*/
export function secureRandomInt(min: number, max: number): number {
if (min > max) {
[min, max] = [max, min]
}
const range = max - min + 1
return Math.floor(secureRandom() * range) + min
}
/**
* Generate cryptographically secure random float in range [min, max]
*
* @param min - Minimum value (inclusive)
* @param max - Maximum value (inclusive)
* @returns Random float in range
* @example
* const multiplier = secureRandomFloat(0.8, 1.2) // Random 0.8-1.2
*/
export function secureRandomFloat(min: number, max: number): number {
if (min > max) {
[min, max] = [max, min]
}
return secureRandom() * (max - min) + min
}
/**
* Generate cryptographically secure boolean with probability
*
* @param probability - Probability of true [0, 1] (default: 0.5)
* @returns Random boolean
* @example
* if (secureRandomBool(0.3)) { // 30% chance
* doSomething()
* }
*/
export function secureRandomBool(probability: number = 0.5): boolean {
return secureRandom() < probability
}
/**
* Pick random element from array
*
* @param array - Array to pick from
* @returns Random element or undefined if empty
* @example
* const item = secureRandomPick(['a', 'b', 'c']) // 'b'
*/
export function secureRandomPick<T>(array: T[]): T | undefined {
if (array.length === 0) return undefined
return array[secureRandomInt(0, array.length - 1)]
}
/**
* Shuffle array using Fisher-Yates with crypto randomness
*
* @param array - Array to shuffle (not modified)
* @returns New shuffled array
* @example
* const shuffled = secureRandomShuffle([1, 2, 3, 4, 5])
*/
export function secureRandomShuffle<T>(array: T[]): T[] {
const result = [...array]
for (let i = result.length - 1; i > 0; i--) {
const j = secureRandomInt(0, i)
;[result[i], result[j]] = [result[j]!, result[i]!]
}
return result
}
/**
* Generate Gaussian-distributed random number (for natural variance)
*
* Uses Box-Muller transform to generate normally distributed values
* Human behavior follows Gaussian distributions (reaction times, typing speeds)
*
* @param mean - Mean of distribution
* @param stdDev - Standard deviation
* @returns Random value from Gaussian distribution
* @example
* const reactionTime = secureGaussian(250, 50) // ~250ms ± 50ms
*/
export function secureGaussian(mean: number, stdDev: number): number {
// Box-Muller transform
const u1 = secureRandom()
const u2 = secureRandom()
// Avoid log(0)
const safeU1 = Math.max(u1, 1e-10)
const z0 = Math.sqrt(-2 * Math.log(safeU1)) * Math.cos(2 * Math.PI * u2)
return z0 * stdDev + mean
}
/**
* Generate value with natural human variance
* Combines Gaussian with occasional outliers (fatigue, distraction)
*
* @param base - Base value
* @param variance - Variance percentage (0.1 = ±10%)
* @param outlierProb - Probability of outlier (default: 0.05 = 5%)
* @returns Value with human-like variance
* @example
* const delay = humanVariance(200, 0.3) // 200ms ± 30% with occasional outliers
*/
export function humanVariance(base: number, variance: number, outlierProb: number = 0.05): number {
// 5% chance of outlier (human distraction, fatigue)
if (secureRandomBool(outlierProb)) {
// Outlier: 1.5x to 3x the base value
return base * secureRandomFloat(1.5, 3)
}
// Normal: Gaussian distribution around base
const stdDev = base * variance
const value = secureGaussian(base, stdDev)
// Ensure positive
return Math.max(value, base * 0.1)
}
/**
* Generate delay with natural typing rhythm
* Simulates human typing speed variations
*
* @param baseMs - Base delay in milliseconds
* @returns Delay with typing-like variance
*/
export function typingDelay(baseMs: number): number {
// Typing follows gamma distribution (skewed right)
// Approximate with shifted Gaussian
const variance = 0.4 // 40% variance
let delay = secureGaussian(baseMs, baseMs * variance)
// Add occasional "thinking" pause (5% chance)
if (secureRandomBool(0.05)) {
delay += secureRandomInt(200, 800)
}
// Add skew
if (secureRandomBool(0.15)) {
delay *= secureRandomFloat(1.2, 1.8)
}
return Math.max(delay, baseMs * 0.2)
}
/**
* Generate realistic mouse movement step count
* Humans vary in mouse precision
*
* @param distance - Distance to move (pixels)
* @returns Number of steps for natural movement
*/
export function mouseSteps(distance: number): number {
// More distance = more steps, but with variance
const baseSteps = Math.sqrt(distance) / 3
return Math.max(2, Math.round(humanVariance(baseSteps, 0.5)))
}
export default {
random: secureRandom,
int: secureRandomInt,
float: secureRandomFloat,
bool: secureRandomBool,
pick: secureRandomPick,
shuffle: secureRandomShuffle,
gaussian: secureGaussian,
humanVariance,
typingDelay,
mouseSteps
}