arn-browser 0.0.7 → 0.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json
CHANGED
|
@@ -1,10 +1,18 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* HumanCursor - Human-like cursor movements for Playwright
|
|
3
3
|
* Drop-in replacement for Page with human cursor for click/fill/type/hover
|
|
4
|
+
*
|
|
5
|
+
* Anti-Bot Detection Features:
|
|
6
|
+
* - Micro-pauses and hesitations during movement
|
|
7
|
+
* - Variable (non-linear) movement speed
|
|
8
|
+
* - Overshoot and correction behavior
|
|
9
|
+
* - Human-like click timing with jitter
|
|
10
|
+
* - Gaussian-weighted element targeting
|
|
11
|
+
* - Pre-movement hover and assessment behavior
|
|
4
12
|
*/
|
|
5
13
|
|
|
6
14
|
import { HumanizeMouseTrajectory } from './bezier.js';
|
|
7
|
-
import { generateRandomCurveParameters
|
|
15
|
+
import { generateRandomCurveParameters } from './randomizer.js';
|
|
8
16
|
|
|
9
17
|
/**
|
|
10
18
|
* Random integer between min and max (inclusive)
|
|
@@ -20,6 +28,21 @@ function sleep(ms) {
|
|
|
20
28
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
21
29
|
}
|
|
22
30
|
|
|
31
|
+
/**
|
|
32
|
+
* Gaussian random number generator (Box-Muller transform)
|
|
33
|
+
* Returns value centered around mean with given standard deviation
|
|
34
|
+
* @param {number} mean - Center of distribution (default: 0.5)
|
|
35
|
+
* @param {number} stdDev - Standard deviation (default: 0.15)
|
|
36
|
+
* @returns {number} - Value clamped between 0.1 and 0.9
|
|
37
|
+
*/
|
|
38
|
+
function gaussianRandom(mean = 0.5, stdDev = 0.15) {
|
|
39
|
+
let u1 = Math.random();
|
|
40
|
+
if (u1 === 0) u1 = 0.000001; // Avoid log(0)
|
|
41
|
+
const u2 = Math.random();
|
|
42
|
+
const z0 = Math.sqrt(-2.0 * Math.log(u1)) * Math.cos(2.0 * Math.PI * u2);
|
|
43
|
+
return Math.max(0.1, Math.min(0.9, z0 * stdDev + mean));
|
|
44
|
+
}
|
|
45
|
+
|
|
23
46
|
/**
|
|
24
47
|
* Simple mutex for serializing keyboard operations
|
|
25
48
|
*/
|
|
@@ -50,23 +73,32 @@ function createHumanLocator(cursor, locator) {
|
|
|
50
73
|
}
|
|
51
74
|
|
|
52
75
|
await cursor._moveToLocator(locator, options);
|
|
76
|
+
|
|
77
|
+
// Pre-click hover - humans don't click instantly after arriving
|
|
78
|
+
if (Math.random() < 0.35) {
|
|
79
|
+
await sleep(randInt(30, 120));
|
|
80
|
+
}
|
|
81
|
+
|
|
53
82
|
const clicks = options.clickCount || 1;
|
|
54
83
|
const button = options.button || 'left';
|
|
55
84
|
|
|
56
85
|
for (let i = 0; i < clicks; i++) {
|
|
86
|
+
// Variable mouse down/up timing
|
|
87
|
+
const holdTime = randInt(50, 120);
|
|
88
|
+
|
|
57
89
|
if (options.delay) {
|
|
58
90
|
await cursor._page.mouse.down({ button });
|
|
59
91
|
await sleep(options.delay);
|
|
60
92
|
await cursor._page.mouse.up({ button });
|
|
61
93
|
} else {
|
|
62
|
-
await cursor._page.mouse.
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
{ button }
|
|
66
|
-
);
|
|
94
|
+
await cursor._page.mouse.down({ button });
|
|
95
|
+
await sleep(holdTime);
|
|
96
|
+
await cursor._page.mouse.up({ button });
|
|
67
97
|
}
|
|
98
|
+
|
|
68
99
|
if (i < clicks - 1) {
|
|
69
|
-
|
|
100
|
+
// Jittery timing between multi-clicks
|
|
101
|
+
await sleep(randInt(120, 320));
|
|
70
102
|
}
|
|
71
103
|
}
|
|
72
104
|
},
|
|
@@ -75,11 +107,28 @@ function createHumanLocator(cursor, locator) {
|
|
|
75
107
|
if (!cursor.config.humanize) {
|
|
76
108
|
return await locator.dblclick(options);
|
|
77
109
|
}
|
|
110
|
+
|
|
78
111
|
await cursor._moveToLocator(locator, options);
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
112
|
+
|
|
113
|
+
// Pre-click hover
|
|
114
|
+
if (Math.random() < 0.25) {
|
|
115
|
+
await sleep(randInt(40, 100));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const button = options.button || 'left';
|
|
119
|
+
|
|
120
|
+
// First click with variable hold
|
|
121
|
+
await cursor._page.mouse.down({ button });
|
|
122
|
+
await sleep(randInt(40, 90));
|
|
123
|
+
await cursor._page.mouse.up({ button });
|
|
124
|
+
|
|
125
|
+
// Gap between clicks - humans vary widely here
|
|
126
|
+
await sleep(randInt(60, 180));
|
|
127
|
+
|
|
128
|
+
// Second click
|
|
129
|
+
await cursor._page.mouse.down({ button });
|
|
130
|
+
await sleep(randInt(35, 85));
|
|
131
|
+
await cursor._page.mouse.up({ button });
|
|
83
132
|
},
|
|
84
133
|
|
|
85
134
|
async fill(value, options = {}) {
|
|
@@ -233,7 +282,11 @@ export function createCursor(page, options = {}) {
|
|
|
233
282
|
humanize: options.humanize ?? true, // Enable human cursor by default
|
|
234
283
|
maxTime: options.maxTime ?? 1.5,
|
|
235
284
|
minTime: options.minTime ?? 0.5,
|
|
236
|
-
showCursor: options.showCursor ?? true // Show cursor by default
|
|
285
|
+
showCursor: options.showCursor ?? true, // Show cursor by default
|
|
286
|
+
// Anti-detection tuning
|
|
287
|
+
overshootProbability: options.overshootProbability ?? 0.15,
|
|
288
|
+
microPauseProbability: options.microPauseProbability ?? 0.08,
|
|
289
|
+
preMoveJitterProbability: options.preMoveJitterProbability ?? 0.10
|
|
237
290
|
};
|
|
238
291
|
|
|
239
292
|
// Internal state
|
|
@@ -242,6 +295,10 @@ export function createCursor(page, options = {}) {
|
|
|
242
295
|
originCoordinates: [0, 0],
|
|
243
296
|
config,
|
|
244
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Move cursor to locator with human-like behavior
|
|
300
|
+
* Includes overshoot/correction and pre-movement jitter
|
|
301
|
+
*/
|
|
245
302
|
async _moveToLocator(locator, options = {}) {
|
|
246
303
|
await locator.scrollIntoViewIfNeeded();
|
|
247
304
|
const bounds = await locator.boundingBox();
|
|
@@ -250,19 +307,65 @@ export function createCursor(page, options = {}) {
|
|
|
250
307
|
throw new Error('Element not found or not visible');
|
|
251
308
|
}
|
|
252
309
|
|
|
310
|
+
// Pre-movement jitter - small "looking around" before heading to target
|
|
311
|
+
if (Math.random() < config.preMoveJitterProbability) {
|
|
312
|
+
const jitterX = this.originCoordinates[0] + randInt(-40, 40);
|
|
313
|
+
const jitterY = this.originCoordinates[1] + randInt(-25, 25);
|
|
314
|
+
await this._moveToPoint([jitterX, jitterY], { steady: true, quick: true });
|
|
315
|
+
await sleep(randInt(60, 180));
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// Gaussian-weighted targeting - clusters clicks naturally around center
|
|
253
319
|
let xOffset, yOffset;
|
|
254
320
|
if (options.position) {
|
|
255
321
|
xOffset = options.position.x;
|
|
256
322
|
yOffset = options.position.y;
|
|
257
323
|
} else {
|
|
258
|
-
|
|
259
|
-
|
|
324
|
+
// Use Gaussian distribution for more natural click targeting
|
|
325
|
+
xOffset = bounds.width * gaussianRandom(0.5, 0.18);
|
|
326
|
+
yOffset = bounds.height * gaussianRandom(0.5, 0.18);
|
|
260
327
|
}
|
|
261
328
|
|
|
262
329
|
const destination = [bounds.x + xOffset, bounds.y + yOffset];
|
|
263
|
-
|
|
330
|
+
|
|
331
|
+
// Calculate distance for overshoot decision
|
|
332
|
+
const distance = Math.sqrt(
|
|
333
|
+
Math.pow(destination[0] - this.originCoordinates[0], 2) +
|
|
334
|
+
Math.pow(destination[1] - this.originCoordinates[1], 2)
|
|
335
|
+
);
|
|
336
|
+
|
|
337
|
+
// Overshoot and correction for longer movements
|
|
338
|
+
const shouldOvershoot = Math.random() < config.overshootProbability && distance > 80;
|
|
339
|
+
|
|
340
|
+
if (shouldOvershoot) {
|
|
341
|
+
// Calculate overshoot direction and magnitude
|
|
342
|
+
const overshootMagnitude = randInt(5, 18);
|
|
343
|
+
const angle = Math.atan2(
|
|
344
|
+
destination[1] - this.originCoordinates[1],
|
|
345
|
+
destination[0] - this.originCoordinates[0]
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
// Overshoot past the target in the direction of movement
|
|
349
|
+
const overshootX = destination[0] + Math.cos(angle) * overshootMagnitude;
|
|
350
|
+
const overshootY = destination[1] + Math.sin(angle) * overshootMagnitude;
|
|
351
|
+
|
|
352
|
+
await this._moveToPoint([overshootX, overshootY], options);
|
|
353
|
+
|
|
354
|
+
// Brief pause - "oops, went too far"
|
|
355
|
+
await sleep(randInt(40, 100));
|
|
356
|
+
|
|
357
|
+
// Correction movement - steadier, more direct
|
|
358
|
+
await this._moveToPoint(destination, { ...options, steady: true, quick: true });
|
|
359
|
+
} else {
|
|
360
|
+
await this._moveToPoint(destination, options);
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return destination;
|
|
264
364
|
},
|
|
265
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Move cursor to a specific point with variable speed and micro-pauses
|
|
368
|
+
*/
|
|
266
369
|
async _moveToPoint(destination, options = {}) {
|
|
267
370
|
const viewport = page.viewportSize() || { width: 1280, height: 720 };
|
|
268
371
|
|
|
@@ -280,6 +383,11 @@ export function createCursor(page, options = {}) {
|
|
|
280
383
|
params.distortionFrequency = 1;
|
|
281
384
|
}
|
|
282
385
|
|
|
386
|
+
// Reduce points for quick movements (corrections)
|
|
387
|
+
if (options.quick) {
|
|
388
|
+
params.targetPoints = Math.max(20, Math.floor(params.targetPoints * 0.4));
|
|
389
|
+
}
|
|
390
|
+
|
|
283
391
|
const curve = new HumanizeMouseTrajectory(
|
|
284
392
|
this.originCoordinates,
|
|
285
393
|
destination,
|
|
@@ -306,10 +414,34 @@ export function createCursor(page, options = {}) {
|
|
|
306
414
|
const baseDelay = totalTime / curve.points.length;
|
|
307
415
|
const speedMultiplier = options.moveSpeed || 1.0;
|
|
308
416
|
|
|
309
|
-
|
|
417
|
+
// Variable speed movement with micro-pauses
|
|
418
|
+
const pointCount = curve.points.length;
|
|
419
|
+
for (let i = 0; i < pointCount; i++) {
|
|
420
|
+
const point = curve.points[i];
|
|
421
|
+
const progress = i / pointCount;
|
|
422
|
+
|
|
423
|
+
// Variable speed: accelerate from start, random variations in middle, decelerate at end
|
|
424
|
+
let speedFactor = 1;
|
|
425
|
+
if (progress < 0.15) {
|
|
426
|
+
// Accelerating phase - start slower
|
|
427
|
+
speedFactor = 0.6 + progress * 2.5;
|
|
428
|
+
} else if (progress > 0.85) {
|
|
429
|
+
// Decelerating phase - slow down approaching target
|
|
430
|
+
speedFactor = 0.6 + (1 - progress) * 2.5;
|
|
431
|
+
} else {
|
|
432
|
+
// Middle phase - random speed bursts
|
|
433
|
+
speedFactor = 0.7 + Math.random() * 0.6;
|
|
434
|
+
}
|
|
435
|
+
|
|
310
436
|
await page.mouse.move(point[0], point[1]);
|
|
311
|
-
|
|
312
|
-
|
|
437
|
+
|
|
438
|
+
// Calculate delay with speed variation
|
|
439
|
+
const adjustedDelay = (baseDelay / speedMultiplier) * (1 / speedFactor);
|
|
440
|
+
await sleep(adjustedDelay + Math.random() * 3);
|
|
441
|
+
|
|
442
|
+
// Micro-pause - random hesitations during movement (humans aren't smooth)
|
|
443
|
+
if (Math.random() < config.microPauseProbability && progress > 0.2 && progress < 0.8) {
|
|
444
|
+
await sleep(randInt(40, 180));
|
|
313
445
|
}
|
|
314
446
|
}
|
|
315
447
|
|
|
@@ -317,13 +449,31 @@ export function createCursor(page, options = {}) {
|
|
|
317
449
|
return destination;
|
|
318
450
|
},
|
|
319
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Human-like typing with variable delays and occasional pauses
|
|
454
|
+
*/
|
|
320
455
|
async _type(text, options = {}) {
|
|
321
456
|
const minDelay = options.minDelay ?? 50;
|
|
322
457
|
const maxDelay = options.maxDelay ?? 150;
|
|
323
458
|
|
|
324
|
-
for (
|
|
459
|
+
for (let i = 0; i < text.length; i++) {
|
|
460
|
+
const char = text[i];
|
|
325
461
|
await page.keyboard.type(char);
|
|
326
|
-
|
|
462
|
+
|
|
463
|
+
// Base delay between characters
|
|
464
|
+
let delay = randInt(minDelay, maxDelay);
|
|
465
|
+
|
|
466
|
+
// Longer pause after punctuation (thinking)
|
|
467
|
+
if (['.', ',', '!', '?', ';', ':'].includes(char)) {
|
|
468
|
+
delay += randInt(80, 200);
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
// Occasional longer pause mid-word (thinking/hesitation)
|
|
472
|
+
if (Math.random() < 0.03) {
|
|
473
|
+
delay += randInt(100, 300);
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
await sleep(delay);
|
|
327
477
|
}
|
|
328
478
|
},
|
|
329
479
|
|
|
@@ -113,6 +113,23 @@ export interface CreateCursorOptions {
|
|
|
113
113
|
minTime?: number;
|
|
114
114
|
/** Auto-show cursor indicator after goto (default: true) */
|
|
115
115
|
showCursor?: boolean;
|
|
116
|
+
|
|
117
|
+
// Anti-bot detection tuning options
|
|
118
|
+
/**
|
|
119
|
+
* Probability of overshoot and correction behavior (0-1)
|
|
120
|
+
* Higher values = more overshoots (default: 0.15)
|
|
121
|
+
*/
|
|
122
|
+
overshootProbability?: number;
|
|
123
|
+
/**
|
|
124
|
+
* Probability of micro-pauses during movement (0-1)
|
|
125
|
+
* Higher values = more hesitations (default: 0.08)
|
|
126
|
+
*/
|
|
127
|
+
microPauseProbability?: number;
|
|
128
|
+
/**
|
|
129
|
+
* Probability of pre-movement jitter/looking around (0-1)
|
|
130
|
+
* Higher values = more exploratory movement (default: 0.10)
|
|
131
|
+
*/
|
|
132
|
+
preMoveJitterProbability?: number;
|
|
116
133
|
}
|
|
117
134
|
|
|
118
135
|
// ============= HumanPage =============
|
|
@@ -196,9 +196,11 @@ export async function launchBrowser({
|
|
|
196
196
|
let browserInstance;
|
|
197
197
|
|
|
198
198
|
// Humanize is ON by default for non-camoufox browsers
|
|
199
|
-
//
|
|
200
|
-
|
|
201
|
-
const effectiveHumanizeOptions =
|
|
199
|
+
// Even when disabled, we still wrap to provide custom methods like fillSequentially
|
|
200
|
+
// The HumanCursor wrapper checks the humanize flag internally and falls back to native Playwright
|
|
201
|
+
const effectiveHumanizeOptions = which_browser !== "camoufox"
|
|
202
|
+
? { humanize: humanize_options?.humanize !== false, ...humanize_options }
|
|
203
|
+
: null;
|
|
202
204
|
|
|
203
205
|
switch (which_browser) {
|
|
204
206
|
case "chromium":
|