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,6 +1,6 @@
1
1
  {
2
2
  "name": "arn-browser",
3
- "version": "0.0.7",
3
+ "version": "0.0.8",
4
4
  "description": "A lightweight, browser autmation helper.",
5
5
  "main": "src/index.js",
6
6
  "types": "src/index.d.ts",
@@ -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, calculateAbsoluteOffset } from './randomizer.js';
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.click(
63
- cursor.originCoordinates[0],
64
- cursor.originCoordinates[1],
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
- await sleep(randInt(170, 280));
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
- await cursor._page.mouse.dblclick(
80
- cursor.originCoordinates[0],
81
- cursor.originCoordinates[1]
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
- xOffset = bounds.width * (randInt(25, 75) / 100);
259
- yOffset = bounds.height * (randInt(25, 75) / 100);
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
- return await this._moveToPoint(destination, options);
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
- for (const point of curve.points) {
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
- if (baseDelay > 0) {
312
- await sleep((baseDelay / speedMultiplier) + Math.random() * 2);
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 (const char of text) {
459
+ for (let i = 0; i < text.length; i++) {
460
+ const char = text[i];
325
461
  await page.keyboard.type(char);
326
- await sleep(randInt(minDelay, maxDelay));
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
- // Only disabled if explicitly set to false: humanize_options: { humanize: false }
200
- const shouldHumanize = which_browser !== "camoufox" && humanize_options?.humanize !== false;
201
- const effectiveHumanizeOptions = shouldHumanize ? humanize_options : null;
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":