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,6 +1,6 @@
1
1
  {
2
2
  "name": "arn-browser",
3
- "version": "0.0.6",
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,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.click(
47
- cursor.originCoordinates[0],
48
- cursor.originCoordinates[1],
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
- await sleep(randInt(170, 280));
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
- await cursor._page.mouse.dblclick(
64
- cursor.originCoordinates[0],
65
- cursor.originCoordinates[1]
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, click, then fill instantly
74
- await this.click(options);
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 this.click(options);
83
- await cursor._page.keyboard.press('Control+A');
84
- await sleep(randInt(50, 100));
85
- // Human type with delays
86
- await cursor._type(value, options);
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 this.click(options);
94
- await cursor._type(text, options);
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 this.click(options);
146
- await cursor._type(text, options);
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
- xOffset = bounds.width * (randInt(25, 75) / 100);
235
- 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);
236
327
  }
237
328
 
238
329
  const destination = [bounds.x + xOffset, bounds.y + yOffset];
239
- 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;
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
- 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
+
286
436
  await page.mouse.move(point[0], point[1]);
287
- if (baseDelay > 0) {
288
- 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));
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 (const char of text) {
459
+ for (let i = 0; i < text.length; i++) {
460
+ const char = text[i];
301
461
  await page.keyboard.type(char);
302
- 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);
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
- // 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":