arn-browser 0.0.6 → 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,37 @@ 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
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Simple mutex for serializing keyboard operations
|
|
48
|
+
*/
|
|
49
|
+
let typingLock = Promise.resolve();
|
|
50
|
+
async function withTypingLock(fn) {
|
|
51
|
+
const previousLock = typingLock;
|
|
52
|
+
let releaseLock;
|
|
53
|
+
typingLock = new Promise(resolve => { releaseLock = resolve; });
|
|
54
|
+
await previousLock;
|
|
55
|
+
try {
|
|
56
|
+
return await fn();
|
|
57
|
+
} finally {
|
|
58
|
+
releaseLock();
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
23
62
|
/**
|
|
24
63
|
* Creates a HumanLocator that wraps a Playwright Locator
|
|
25
64
|
* with human-like cursor movement for actions
|
|
@@ -34,23 +73,32 @@ function createHumanLocator(cursor, locator) {
|
|
|
34
73
|
}
|
|
35
74
|
|
|
36
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
|
+
|
|
37
82
|
const clicks = options.clickCount || 1;
|
|
38
83
|
const button = options.button || 'left';
|
|
39
84
|
|
|
40
85
|
for (let i = 0; i < clicks; i++) {
|
|
86
|
+
// Variable mouse down/up timing
|
|
87
|
+
const holdTime = randInt(50, 120);
|
|
88
|
+
|
|
41
89
|
if (options.delay) {
|
|
42
90
|
await cursor._page.mouse.down({ button });
|
|
43
91
|
await sleep(options.delay);
|
|
44
92
|
await cursor._page.mouse.up({ button });
|
|
45
93
|
} else {
|
|
46
|
-
await cursor._page.mouse.
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
{ button }
|
|
50
|
-
);
|
|
94
|
+
await cursor._page.mouse.down({ button });
|
|
95
|
+
await sleep(holdTime);
|
|
96
|
+
await cursor._page.mouse.up({ button });
|
|
51
97
|
}
|
|
98
|
+
|
|
52
99
|
if (i < clicks - 1) {
|
|
53
|
-
|
|
100
|
+
// Jittery timing between multi-clicks
|
|
101
|
+
await sleep(randInt(120, 320));
|
|
54
102
|
}
|
|
55
103
|
}
|
|
56
104
|
},
|
|
@@ -59,19 +107,36 @@ function createHumanLocator(cursor, locator) {
|
|
|
59
107
|
if (!cursor.config.humanize) {
|
|
60
108
|
return await locator.dblclick(options);
|
|
61
109
|
}
|
|
110
|
+
|
|
62
111
|
await cursor._moveToLocator(locator, options);
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
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 });
|
|
67
132
|
},
|
|
68
133
|
|
|
69
134
|
async fill(value, options = {}) {
|
|
70
135
|
if (!cursor.config.humanize) {
|
|
71
136
|
return await locator.fill(value, options);
|
|
72
137
|
}
|
|
73
|
-
// Humanize: Move to element,
|
|
74
|
-
await
|
|
138
|
+
// Humanize: Move to element, then fill instantly (Playwright handles focus)
|
|
139
|
+
await cursor._moveToLocator(locator, options);
|
|
75
140
|
return await locator.fill(value, options);
|
|
76
141
|
},
|
|
77
142
|
|
|
@@ -79,19 +144,24 @@ function createHumanLocator(cursor, locator) {
|
|
|
79
144
|
if (!cursor.config.humanize) {
|
|
80
145
|
return await locator.fill(value, options);
|
|
81
146
|
}
|
|
82
|
-
await
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
147
|
+
return await withTypingLock(async () => {
|
|
148
|
+
await this.click(options);
|
|
149
|
+
// Clear field reliably using fill('') before human typing
|
|
150
|
+
await locator.fill('');
|
|
151
|
+
await sleep(randInt(50, 100));
|
|
152
|
+
// Human type with delays
|
|
153
|
+
await cursor._type(value, options);
|
|
154
|
+
});
|
|
87
155
|
},
|
|
88
156
|
|
|
89
157
|
async type(text, options = {}) {
|
|
90
158
|
if (!cursor.config.humanize) {
|
|
91
159
|
return await locator.type(text, options);
|
|
92
160
|
}
|
|
93
|
-
await
|
|
94
|
-
|
|
161
|
+
return await withTypingLock(async () => {
|
|
162
|
+
await this.click(options);
|
|
163
|
+
await cursor._type(text, options);
|
|
164
|
+
});
|
|
95
165
|
},
|
|
96
166
|
|
|
97
167
|
async hover(options = {}) {
|
|
@@ -142,8 +212,11 @@ function createHumanLocator(cursor, locator) {
|
|
|
142
212
|
if (!cursor.config.humanize) {
|
|
143
213
|
return await locator.pressSequentially(text, options);
|
|
144
214
|
}
|
|
145
|
-
await
|
|
146
|
-
|
|
215
|
+
return await withTypingLock(async () => {
|
|
216
|
+
await this.click(options);
|
|
217
|
+
await locator.clear();
|
|
218
|
+
await cursor._type(text, options);
|
|
219
|
+
});
|
|
147
220
|
},
|
|
148
221
|
|
|
149
222
|
// Chainable methods that return new HumanLocators
|
|
@@ -209,7 +282,11 @@ export function createCursor(page, options = {}) {
|
|
|
209
282
|
humanize: options.humanize ?? true, // Enable human cursor by default
|
|
210
283
|
maxTime: options.maxTime ?? 1.5,
|
|
211
284
|
minTime: options.minTime ?? 0.5,
|
|
212
|
-
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
|
|
213
290
|
};
|
|
214
291
|
|
|
215
292
|
// Internal state
|
|
@@ -218,6 +295,10 @@ export function createCursor(page, options = {}) {
|
|
|
218
295
|
originCoordinates: [0, 0],
|
|
219
296
|
config,
|
|
220
297
|
|
|
298
|
+
/**
|
|
299
|
+
* Move cursor to locator with human-like behavior
|
|
300
|
+
* Includes overshoot/correction and pre-movement jitter
|
|
301
|
+
*/
|
|
221
302
|
async _moveToLocator(locator, options = {}) {
|
|
222
303
|
await locator.scrollIntoViewIfNeeded();
|
|
223
304
|
const bounds = await locator.boundingBox();
|
|
@@ -226,19 +307,65 @@ export function createCursor(page, options = {}) {
|
|
|
226
307
|
throw new Error('Element not found or not visible');
|
|
227
308
|
}
|
|
228
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
|
|
229
319
|
let xOffset, yOffset;
|
|
230
320
|
if (options.position) {
|
|
231
321
|
xOffset = options.position.x;
|
|
232
322
|
yOffset = options.position.y;
|
|
233
323
|
} else {
|
|
234
|
-
|
|
235
|
-
|
|
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);
|
|
236
327
|
}
|
|
237
328
|
|
|
238
329
|
const destination = [bounds.x + xOffset, bounds.y + yOffset];
|
|
239
|
-
|
|
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;
|
|
240
364
|
},
|
|
241
365
|
|
|
366
|
+
/**
|
|
367
|
+
* Move cursor to a specific point with variable speed and micro-pauses
|
|
368
|
+
*/
|
|
242
369
|
async _moveToPoint(destination, options = {}) {
|
|
243
370
|
const viewport = page.viewportSize() || { width: 1280, height: 720 };
|
|
244
371
|
|
|
@@ -256,6 +383,11 @@ export function createCursor(page, options = {}) {
|
|
|
256
383
|
params.distortionFrequency = 1;
|
|
257
384
|
}
|
|
258
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
|
+
|
|
259
391
|
const curve = new HumanizeMouseTrajectory(
|
|
260
392
|
this.originCoordinates,
|
|
261
393
|
destination,
|
|
@@ -282,10 +414,34 @@ export function createCursor(page, options = {}) {
|
|
|
282
414
|
const baseDelay = totalTime / curve.points.length;
|
|
283
415
|
const speedMultiplier = options.moveSpeed || 1.0;
|
|
284
416
|
|
|
285
|
-
|
|
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
|
+
|
|
286
436
|
await page.mouse.move(point[0], point[1]);
|
|
287
|
-
|
|
288
|
-
|
|
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));
|
|
289
445
|
}
|
|
290
446
|
}
|
|
291
447
|
|
|
@@ -293,13 +449,31 @@ export function createCursor(page, options = {}) {
|
|
|
293
449
|
return destination;
|
|
294
450
|
},
|
|
295
451
|
|
|
452
|
+
/**
|
|
453
|
+
* Human-like typing with variable delays and occasional pauses
|
|
454
|
+
*/
|
|
296
455
|
async _type(text, options = {}) {
|
|
297
456
|
const minDelay = options.minDelay ?? 50;
|
|
298
457
|
const maxDelay = options.maxDelay ?? 150;
|
|
299
458
|
|
|
300
|
-
for (
|
|
459
|
+
for (let i = 0; i < text.length; i++) {
|
|
460
|
+
const char = text[i];
|
|
301
461
|
await page.keyboard.type(char);
|
|
302
|
-
|
|
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);
|
|
303
477
|
}
|
|
304
478
|
},
|
|
305
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":
|